Repository: prebid/prebid-server
Pull request #1145 (2019-12-13 21:14:33)

Add custom JSON req/resp data to the analytics logging…
… for the /openrtb2/video endpoint.


Patch:
diff --git a/adapters/applogy/applogy.go b/adapters/applogy/applogy.go
new file mode 100644
index 00000000..a1215e24
--- /dev/null
+++ b/adapters/applogy/applogy.go
@@ -0,0 +1,160 @@
+package applogy
+
+import (
+	"encoding/json"
+	"errors"
+	"net/http"
+	"strconv"
+
+	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+type ApplogyAdapter struct {
+	endpoint string
+}
+
+func (a *ApplogyAdapter) MakeRequests(request *openrtb.BidRequest, _ *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	headers := http.Header{}
+	headers.Add("Content-Type", "application/json;charset=utf-8")
+	headers.Add("Accept", "application/json")
+	impressions := request.Imp
+	result := make([]*adapters.RequestData, 0, len(impressions))
+	errs := make([]error, 0, len(impressions))
+
+	for i, impression := range impressions {
+		if impression.Banner == nil && impression.Video == nil && impression.Native == nil {
+			errs = append(errs, &errortypes.BadInput{
+				Message: "Applogy only supports banner, video or native ads",
+			})
+			continue
+		}
+		if impression.Banner != nil {
+			banner := impression.Banner
+			if banner.W == nil || banner.H == nil || *banner.W == 0 || *banner.H == 0 {
+				if len(banner.Format) == 0 {
+					errs = append(errs, &errortypes.BadInput{
+						Message: "banner size information missing",
+					})
+					continue
+				}
+				format := banner.Format[0]
+				banner.W = &format.W
+				banner.H = &format.H
+			}
+		}
+		if len(impression.Ext) == 0 {
+			errs = append(errs, errors.New("impression extensions required"))
+			continue
+		}
+		var bidderExt adapters.ExtImpBidder
+		err := json.Unmarshal(impression.Ext, &bidderExt)
+		if err != nil {
+			errs = append(errs, err)
+			continue
+		}
+		if len(bidderExt.Bidder) == 0 {
+			errs = append(errs, errors.New("bidder required"))
+			continue
+		}
+		var impressionExt openrtb_ext.ExtImpApplogy
+		err = json.Unmarshal(bidderExt.Bidder, &impressionExt)
+		if err != nil {
+			errs = append(errs, err)
+			continue
+		}
+		if impressionExt.Token == "" {
+			errs = append(errs, errors.New("Applogy token required"))
+			continue
+		}
+		request.Imp = impressions[i : i+1]
+		body, err := json.Marshal(request)
+		if err != nil {
+			errs = append(errs, err)
+			continue
+		}
+		result = append(result, &adapters.RequestData{
+			Method:  "POST",
+			Uri:     a.endpoint + "/" + impressionExt.Token,
+			Body:    body,
+			Headers: headers,
+		})
+	}
+
+	request.Imp = impressions
+
+	if len(result) == 0 {
+		return nil, errs
+	}
+	return result, errs
+}
+
+func (a *ApplogyAdapter) MakeBids(request *openrtb.BidRequest, _ *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	var errs []error
+
+	switch responseData.StatusCode {
+	case http.StatusNoContent:
+		return nil, nil
+	case http.StatusBadRequest:
+		return nil, []error{&errortypes.BadInput{
+			Message: "unexpected status code: " + strconv.Itoa(responseData.StatusCode),
+		}}
+	case http.StatusOK:
+		break
+	default:
+		return nil, []error{&errortypes.BadServerResponse{
+			Message: "unexpected status code: " + strconv.Itoa(responseData.StatusCode),
+		}}
+	}
+
+	var bidResponse openrtb.BidResponse
+	err := json.Unmarshal(responseData.Body, &bidResponse)
+	if err != nil {
+		return nil, []error{&errortypes.BadServerResponse{
+			Message: err.Error(),
+		}}
+	}
+
+	response := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
+
+	for _, seatBid := range bidResponse.SeatBid {
+		for _, bid := range seatBid.Bid {
+			bid := bid // pin https://github.com/kyoh86/scopelint#whats-this
+			var bidType openrtb_ext.BidType
+			for _, impression := range request.Imp {
+				if impression.ID != bid.ImpID {
+					continue
+				}
+				switch {
+				case impression.Banner != nil:
+					bidType = openrtb_ext.BidTypeBanner
+				case impression.Video != nil:
+					bidType = openrtb_ext.BidTypeVideo
+				case impression.Native != nil:
+					bidType = openrtb_ext.BidTypeNative
+				}
+				break
+			}
+			if bidType == "" {
+				errs = append(errs, &errortypes.BadServerResponse{
+					Message: "ignoring bid id=" + bid.ID + ", request doesn't contain any valid impression with id=" + bid.ImpID,
+				})
+				continue
+			}
+			response.Bids = append(response.Bids, &adapters.TypedBid{
+				Bid:     &bid,
+				BidType: bidType,
+			})
+		}
+	}
+
+	return response, errs
+}
+
+func NewApplogyBidder(endpoint string) *ApplogyAdapter {
+	return &ApplogyAdapter{
+		endpoint: endpoint,
+	}
+}
diff --git a/adapters/gamoshi/gamoshi.go b/adapters/gamoshi/gamoshi.go
index 016feb17..af915f68 100644
--- a/adapters/gamoshi/gamoshi.go
+++ b/adapters/gamoshi/gamoshi.go
@@ -101,7 +101,7 @@ func (a *GamoshiAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adap
 	if len(thisURI) == 0 {
 		thisURI = "https://rtb.gamoshi.io"
 	}
-	thisURI = thisURI + "/r/" + gamoshiExt.SupplyPartnerId + "/bidr?reqformat=RTB_JSON"
+	thisURI = thisURI + "/r/" + gamoshiExt.SupplyPartnerId + "/bidr?bidder=prebid-server"
 	headers := http.Header{}
 	headers.Add("Content-Type", "application/json;charset=utf-8")
 	headers.Add("Accept", "application/json")
diff --git a/adapters/marsmedia/marsmedia.go b/adapters/marsmedia/marsmedia.go
new file mode 100644
index 00000000..359ebb7b
--- /dev/null
+++ b/adapters/marsmedia/marsmedia.go
@@ -0,0 +1,169 @@
+package marsmedia
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"strconv"
+
+	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+type MarsmediaAdapter struct {
+	URI string
+}
+
+func (a *MarsmediaAdapter) MakeRequests(requestIn *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+
+	request := *requestIn
+
+	if len(request.Imp) == 0 {
+		return nil, []error{&errortypes.BadInput{
+			Message: "No impression in the bid request",
+		}}
+	}
+
+	var bidderExt adapters.ExtImpBidder
+	if err := json.Unmarshal(request.Imp[0].Ext, &bidderExt); err != nil {
+		return nil, []error{&errortypes.BadInput{
+			Message: "ext.bidder not provided",
+		}}
+	}
+
+	var marsmediaExt openrtb_ext.ExtImpMarsmedia
+	if err := json.Unmarshal(bidderExt.Bidder, &marsmediaExt); err != nil {
+		return nil, []error{&errortypes.BadInput{
+			Message: "ext.bidder.zone not provided",
+		}}
+	}
+
+	if marsmediaExt.ZoneID == "" {
+		return nil, []error{&errortypes.BadInput{
+			Message: "Zone is empty",
+		}}
+	}
+
+	validImpExists := false
+	for i := 0; i < len(request.Imp); i++ {
+		if request.Imp[i].Banner != nil {
+			bannerCopy := *requestIn.Imp[i].Banner
+			if len(bannerCopy.Format) > 0 {
+				firstFormat := bannerCopy.Format[0]
+				bannerCopy.W = &(firstFormat.W)
+				bannerCopy.H = &(firstFormat.H)
+				request.Imp[i].Banner = &bannerCopy
+				validImpExists = true
+			} else if bannerCopy.W != nil && bannerCopy.H != nil {
+				validImpExists = true
+			} else {
+				return nil, []error{&errortypes.BadInput{
+					Message: "No valid banner format in the bid request",
+				}}
+			}
+		} else if request.Imp[i].Video != nil {
+			validImpExists = true
+		}
+	}
+	if !validImpExists {
+		return nil, []error{&errortypes.BadInput{
+			Message: "No valid impression in the bid request",
+		}}
+	}
+
+	request.AT = 1 //Defaulting to first price auction for all prebid requests
+
+	reqJSON, err := json.Marshal(request)
+	if err != nil {
+		return nil, []error{&errortypes.BadInput{
+			Message: fmt.Sprintf("Json not encoded. err: %s", err),
+		}}
+	}
+
+	uri := a.URI + "&zone=" + marsmediaExt.ZoneID
+	headers := http.Header{}
+	headers.Add("Content-Type", "application/json;charset=utf-8")
+	headers.Add("Accept", "application/json")
+	headers.Add("x-openrtb-version", "2.5")
+
+	if request.Device != nil {
+		addHeaderIfNonEmpty(headers, "User-Agent", request.Device.UA)
+		addHeaderIfNonEmpty(headers, "X-Forwarded-For", request.Device.IP)
+		addHeaderIfNonEmpty(headers, "Accept-Language", request.Device.Language)
+		if request.Device.DNT != nil {
+			addHeaderIfNonEmpty(headers, "DNT", strconv.Itoa(int(*request.Device.DNT)))
+		}
+	}
+
+	return []*adapters.RequestData{{
+		Method:  "POST",
+		Uri:     uri,
+		Body:    reqJSON,
+		Headers: headers,
+	}}, []error{}
+}
+
+func (a *MarsmediaAdapter) MakeBids(internalRequest *openrtb.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. ", 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 bidResp openrtb.BidResponse
+	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
+		return nil, []error{&errortypes.BadServerResponse{
+			Message: fmt.Sprintf("Bad server response: %d. ", err),
+		}}
+	}
+
+	bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(bidResp.SeatBid[0].Bid))
+	sb := bidResp.SeatBid[0]
+	for i := 0; i < len(sb.Bid); i++ {
+		bid := sb.Bid[i]
+		bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
+			Bid:     &bid,
+			BidType: getMediaTypeForImp(bid.ImpID, internalRequest.Imp),
+		})
+	}
+	return bidResponse, nil
+}
+
+func addHeaderIfNonEmpty(headers http.Header, headerName string, headerValue string) {
+	if len(headerValue) > 0 {
+		headers.Add(headerName, headerValue)
+	}
+}
+
+// getMediaTypeForImp figures out which media type this bid is for.
+func getMediaTypeForImp(impId string, imps []openrtb.Imp) openrtb_ext.BidType {
+	mediaType := openrtb_ext.BidTypeBanner //default type
+	for _, imp := range imps {
+		if imp.ID == impId {
+			if imp.Video != nil {
+				mediaType = openrtb_ext.BidTypeVideo
+			}
+			return mediaType
+		}
+	}
+	return mediaType
+}
+
+func NewMarsmediaBidder(endpoint string) *MarsmediaAdapter {
+	return &MarsmediaAdapter{
+		URI: endpoint,
+	}
+}
diff --git a/adapters/marsmedia/usersync.go b/adapters/marsmedia/usersync.go
new file mode 100644
index 00000000..50996f32
--- /dev/null
+++ b/adapters/marsmedia/usersync.go
@@ -0,0 +1,12 @@
+package marsmedia
+
+import (
+	"text/template"
+
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/usersync"
+)
+
+func NewMarsmediaSyncer(temp *template.Template) usersync.Usersyncer {
+	return adapters.NewSyncer("marsmedia", 0, temp, adapters.SyncTypeRedirect)
+}
diff --git a/adapters/rubicon/rubicon.go b/adapters/rubicon/rubicon.go
index 7f8de46b..e7461c48 100644
--- a/adapters/rubicon/rubicon.go
+++ b/adapters/rubicon/rubicon.go
@@ -5,13 +5,14 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
-	"github.com/golang/glog"
-	"github.com/prebid/prebid-server/pbs"
 	"io/ioutil"
 	"net/http"
 	"net/url"
 	"strings"
 
+	"github.com/golang/glog"
+	"github.com/prebid/prebid-server/pbs"
+
 	"golang.org/x/net/context/ctxhttp"
 
 	"github.com/mxmCherry/openrtb"
@@ -685,6 +686,13 @@ func (a *RubiconAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adap
 
 		isVideo := isVideo(thisImp)
 		if isVideo {
+			if rubiconExt.Video.VideoSizeID == 0 {
+				errs = append(errs, &errortypes.BadInput{
+					Message: fmt.Sprintf("imp[%d].ext.bidder.rubicon.video.size_id must be defined for video impression", i),
+				})
+				continue
+			}
+
 			videoCopy := *thisImp.Video
 			videoExt := rubiconVideoExt{Skip: rubiconExt.Video.Skip, SkipDelay: rubiconExt.Video.SkipDelay, RP: rubiconVideoExtRP{SizeID: rubiconExt.Video.VideoSizeID}}
 			videoCopy.Ext, err = json.Marshal(&videoExt)
diff --git a/adapters/syncer.go b/adapters/syncer.go
index d86e3239..c212a436 100644
--- a/adapters/syncer.go
+++ b/adapters/syncer.go
@@ -46,7 +46,7 @@ func (s *Syncer) GetUsersyncInfo(privacyPolicies privacy.Policies) (*usersync.Us
 	syncURL, err := macros.ResolveMacros(*s.urlTemplate, macros.UserSyncTemplateParams{
 		GDPR:        privacyPolicies.GDPR.Signal,
 		GDPRConsent: privacyPolicies.GDPR.Consent,
-		USPrivacy:   privacyPolicies.CCPA.Signal,
+		USPrivacy:   privacyPolicies.CCPA.Value,
 	})
 	if err != nil {
 		return nil, err
diff --git a/adapters/triplelift_native/triplelift_native.go b/adapters/triplelift_native/triplelift_native.go
index a2576032..c3cb6b24 100644
--- a/adapters/triplelift_native/triplelift_native.go
+++ b/adapters/triplelift_native/triplelift_native.go
@@ -28,8 +28,6 @@ type TripleliftNativeExtInfo struct {
 	// Array is used for deserialization.
 	PublisherWhitelist []string `json:"publisher_whitelist"`
 
-	Endpoint string `json:"endpoint"`
-
 	// Map is used for optimized memory access and should be constructed after deserialization.
 	PublisherWhitelistMap map[string]bool
 }
@@ -198,5 +196,5 @@ func NewTripleliftNativeBidder(client *http.Client, endpoint string, extraInfo s
 
 	return &TripleliftNativeAdapter{
 		extInfo:  extInfo,
-		endpoint: extInfo.Endpoint}
+		endpoint: endpoint}
 }
diff --git a/analytics/config/config.go b/analytics/config/config.go
index 3073c6ca..7be7c8ec 100644
--- a/analytics/config/config.go
+++ b/analytics/config/config.go
@@ -29,6 +29,12 @@ func (ea enabledAnalytics) LogAuctionObject(ao *analytics.AuctionObject) {
 	}
 }
 
+func (ea enabledAnalytics) LogVideoObject(vo *analytics.VideoObject) {
+	for _, module := range ea {
+		module.LogVideoObject(vo)
+	}
+}
+
 func (ea enabledAnalytics) LogCookieSyncObject(cso *analytics.CookieSyncObject) {
 	for _, module := range ea {
 		module.LogCookieSyncObject(cso)
diff --git a/analytics/core.go b/analytics/core.go
index 3f16ba64..6fd5139f 100644
--- a/analytics/core.go
+++ b/analytics/core.go
@@ -2,6 +2,7 @@ package analytics
 
 import (
 	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/usersync"
 )
 
@@ -15,6 +16,7 @@ import (
 
 type PBSAnalyticsModule interface {
 	LogAuctionObject(*AuctionObject)
+	LogVideoObject(*VideoObject)
 	LogCookieSyncObject(*CookieSyncObject)
 	LogSetUIDObject(*SetUIDObject)
 	LogAmpObject(*AmpObject)
@@ -38,6 +40,16 @@ type AmpObject struct {
 	Origin             string
 }
 
+//Loggable object of a transaction at /openrtb2/video endpoint
+type VideoObject struct {
+	Status        int
+	Errors        []error
+	Request       *openrtb.BidRequest
+	Response      *openrtb.BidResponse
+	VideoRequest  *openrtb_ext.BidRequestVideo
+	VideoResponse *openrtb_ext.BidResponseVideo
+}
+
 //Loggable object of a transaction at /setuid
 type SetUIDObject struct {
 	Status  int
diff --git a/analytics/filesystem/file_module.go b/analytics/filesystem/file_module.go
index b9438763..54d492ad 100644
--- a/analytics/filesystem/file_module.go
+++ b/analytics/filesystem/file_module.go
@@ -14,6 +14,7 @@ type RequestType string
 const (
 	COOKIE_SYNC RequestType = "/cookie_sync"
 	AUCTION     RequestType = "/openrtb2/auction"
+	VIDEO       RequestType = "/openrtb2/video"
 	SETUID      RequestType = "/set_uid"
 	AMP         RequestType = "/openrtb2/amp"
 )
@@ -32,6 +33,15 @@ func (f *FileLogger) LogAuctionObject(ao *analytics.AuctionObject) {
 	f.Logger.Flush()
 }
 
+//Writes VideoObject to file
+func (f *FileLogger) LogVideoObject(vo *analytics.VideoObject) {
+	//Code to parse the object and log in a way required
+	var b bytes.Buffer
+	b.WriteString(jsonifyVideoObject(vo))
+	f.Logger.Debug(b.String())
+	f.Logger.Flush()
+}
+
 //Logs SetUIDObject to file
 func (f *FileLogger) LogSetUIDObject(so *analytics.SetUIDObject) {
 	//Code to parse the object and log in a way required
@@ -98,6 +108,23 @@ func jsonifyAuctionObject(ao *analytics.AuctionObject) string {
 	}
 }
 
+func jsonifyVideoObject(vo *analytics.VideoObject) string {
+	type alias analytics.VideoObject
+	b, err := json.Marshal(&struct {
+		Type RequestType `json:"type"`
+		*alias
+	}{
+		Type:  VIDEO,
+		alias: (*alias)(vo),
+	})
+
+	if err == nil {
+		return string(b)
+	} else {
+		return fmt.Sprintf("Transactional Logs Error: Video object badly formed %v", err)
+	}
+}
+
 func jsonifyCookieSync(cso *analytics.CookieSyncObject) string {
 	type alias analytics.CookieSyncObject
 
diff --git a/config/config.go b/config/config.go
index 02b0415f..d586021f 100644
--- a/config/config.go
+++ b/config/config.go
@@ -47,6 +47,7 @@ type Configuration struct {
 	Analytics            Analytics          `mapstructure:"analytics"`
 	AMPTimeoutAdjustment int64              `mapstructure:"amp_timeout_adjustment_ms"`
 	GDPR                 GDPR               `mapstructure:"gdpr"`
+	CCPA                 CCPA               `mapstructure:"ccpa"`
 	CurrencyConverter    CurrencyConverter  `mapstructure:"currency_converter"`
 	DefReqConfig         DefReqConfig       `mapstructure:"default_request"`
 
@@ -160,6 +161,10 @@ func (t *GDPRTimeouts) ActiveTimeout() time.Duration {
 	return time.Duration(t.ActiveVendorlistFetch) * time.Millisecond
 }
 
+type CCPA struct {
+	Enforce bool `mapstructure:"enforce"`
+}
+
 type Analytics struct {
 	File FileLogs `mapstructure:"file"`
 }
@@ -202,6 +207,7 @@ const (
 	dummyPublisherID string = "12"
 	dummyGDPR        string = "0"
 	dummyGDPRConsent string = "someGDPRConsentString"
+	dummyCCPA        string = "1NYN"
 )
 
 type Adapter struct {
@@ -216,8 +222,9 @@ type Adapter struct {
 	//
 	// This value will be interpreted as a Golang Template. At runtime, the following Template variables will be replaced.
 	//
-	//   {{.GDPR}} -- This will be replaced with the "gdpr" property sent to /cookie_sync
-	//   {{.Consent}} -- This will be replaced with the "consent" property sent to /cookie_sync
+	//   {{.GDPR}}      -- This will be replaced with the "gdpr" property sent to /cookie_sync
+	//   {{.Consent}}   -- This will be replaced with the "consent" property sent to /cookie_sync
+	//   {{.USPrivacy}} -- This will be replaced with the "us_privacy" property sent to /cookie_sync
 	//
 	// For more info on templates, see: https://golang.org/pkg/text/template/
 	UserSyncURL string `mapstructure:"usersync_url"`
@@ -275,7 +282,12 @@ func validateAdapterUserSyncURL(userSyncURL string, adapterName string, errs con
 			return append(errs, fmt.Errorf("Invalid user sync URL template: %s for adapter: %s. %v", userSyncURL, adapterName, err))
 		}
 		// Resolve macros (if any) in the user_sync URL
-		resolvedUserSyncURL, err := macros.ResolveMacros(*userSyncTemplate, macros.UserSyncTemplateParams{GDPR: dummyGDPR, GDPRConsent: dummyGDPRConsent})
+		dummyMacroValues := macros.UserSyncTemplateParams{
+			GDPR:        dummyGDPR,
+			GDPRConsent: dummyGDPRConsent,
+			USPrivacy:   dummyCCPA,
+		}
+		resolvedUserSyncURL, err := macros.ResolveMacros(*userSyncTemplate, dummyMacroValues)
 		if err != nil {
 			return append(errs, fmt.Errorf("Unable to resolve user sync URL: %s for adapter: %s. %v", userSyncURL, adapterName, err))
 		}
@@ -490,13 +502,14 @@ func (cfg *Configuration) setDerivedDefaults() {
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderEPlanning, "https://ads.us.e-planning.net/uspd/1/?du=https%3A%2F%2Fads.us.e-planning.net%2Fgetuid%2F1%2F5a1ad71d2d53a0f5%3F"+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Deplanning%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID")
 	// openrtb_ext.BidderFacebook doesn't have a good default.
 	// openrtb_ext.BidderGamma doesn't have a good default.
-	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderGamoshi, "https://rtb.gamoshi.io/pix/0000/scm?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&rurl="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dgamoshi%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5Bgusr%5D")
-	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderGrid, "https://x.bidswitch.net/check_uuid/"+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dgrid%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BBSW_UUID%7D?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}")
+	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderGamoshi, "https://rtb.gamoshi.io/user_sync_prebid?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&rurl="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dgamoshi%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5Bgusr%5D")
+	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderGrid, "https://x.bidswitch.net/check_uuid/"+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dgrid%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BBSW_UUID%7D?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderGumGum, "https://rtb.gumgum.com/usync/prbds2s?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&r="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dgumgum%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderImprovedigital, "https://ad.360yield.com/server_match?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&r="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dimprovedigital%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7BPUB_USER_ID%7D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderIx, "https://ssum.casalemedia.com/usermatchredir?s=184932&cb="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dix%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderLifestreet, "https://ads.lfstmedia.com/idsync/137062?synced=1&ttl=1s&rurl="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dlifestreet%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%24visitor_cookie%24%24")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderLockerDome, "https://lockerdome.com/usync/prebidserver?pid="+cfg.Adapters["lockerdome"].PlatformID+"&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dlockerdome%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7B%7Buid%7D%7D")
+	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderMarsmedia, "https://dmp.rtbsrv.com/dmp/profiles/cm?p_id=179&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dmarsmedia%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUUID%7D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderMgid, "https://cm.mgid.com/m?cdsp=363893&adu="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dmgid%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Bmuidn%7D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderOpenx, "https://rtb.openx.net/sync/prebid?r="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dopenx%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUID%7D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderPubmatic, "https://ads.pubmatic.com/AdServer/js/user_sync.html?predirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dpubmatic%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D")
@@ -657,6 +670,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.adpone.endpoint", "http://rtb.adpone.com/bid-request?src=prebid_server")
 	v.SetDefault("adapters.adtelligent.endpoint", "http://hb.adtelligent.com/auction")
 	v.SetDefault("adapters.advangelists.endpoint", "http://nep.advangelists.com/xp/get?pubid={{.PublisherID}}")
+	v.SetDefault("adapters.applogy.endpoint", "http://rtb.applogy.com/v1/prebid")
 	v.SetDefault("adapters.appnexus.endpoint", "http://ib.adnxs.com/openrtb2") // Docs: https://wiki.appnexus.com/display/supply/Incoming+Bid+Request+from+SSPs
 	v.SetDefault("adapters.appnexus.platform_id", "5")
 	v.SetDefault("adapters.beachfront.endpoint", "https://display.bfmio.com/prebid_display")
@@ -676,9 +690,10 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.kubient.endpoint", "http://kbntx.ch/prebid")
 	v.SetDefault("adapters.lifestreet.endpoint", "https://prebid.s2s.lfstmedia.com/adrequest")
 	v.SetDefault("adapters.lockerdome.endpoint", "https://lockerdome.com/ladbid/prebidserver/openrtb2")
+	v.SetDefault("adapters.marsmedia.endpoint", "https://bid306.rtbsrv.com/bidder/?bid=f3xtet")
 	v.SetDefault("adapters.mgid.endpoint", "https://prebid.mgid.com/prebid/")
 	v.SetDefault("adapters.openx.endpoint", "http://rtb.openx.net/prebid")
-	v.SetDefault("adapters.pubmatic.endpoint", "http://hbopenbid.pubmatic.com/translator?source=prebid-server")
+	v.SetDefault("adapters.pubmatic.endpoint", "https://hbopenbid.pubmatic.com/translator?source=prebid-server")
 	v.SetDefault("adapters.pubnative.endpoint", "http://dsp.pubnative.net/bid/v1/request")
 	v.SetDefault("adapters.pulsepoint.endpoint", "http://bid.contextweb.com/header/s/ortb/prebid-s2s")
 	v.SetDefault("adapters.rhythmone.endpoint", "http://tag.1rx.io/rmp")
@@ -707,6 +722,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("gdpr.timeouts_ms.init_vendorlist_fetches", 0)
 	v.SetDefault("gdpr.timeouts_ms.active_vendorlist_fetch", 0)
 	v.SetDefault("gdpr.non_standard_publishers", []string{""})
+	v.SetDefault("ccpa.enforce", false)
 	v.SetDefault("currency_converter.fetch_url", "https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json")
 	v.SetDefault("currency_converter.fetch_interval_seconds", 1800) // fetch currency rates every 30 minutes
 	v.SetDefault("default_request.type", "")
diff --git a/docs/developers/stored-requests.md b/docs/developers/stored-requests.md
index 4b54b4e9..8b717716 100644
--- a/docs/developers/stored-requests.md
+++ b/docs/developers/stored-requests.md
@@ -34,7 +34,7 @@ Add the file `stored_requests/data/by_id/stored_imps/{id}.json` and populate it
   },
   "ext": {
     "appnexus": {
-      "placementId": 10433394
+      "placementId": 12883451
     }
   }
 }
@@ -88,7 +88,7 @@ You can also store _part_ of the Imp on the server. For example:
   },
   "ext": {
     "appnexus": {
-      "placementId": 10433394
+      "placementId": 12883451
     }
   }
 }
diff --git a/endpoints/cookie_sync.go b/endpoints/cookie_sync.go
index 475416e8..9787a8f7 100644
--- a/endpoints/cookie_sync.go
+++ b/endpoints/cookie_sync.go
@@ -32,6 +32,7 @@ func NewCookieSyncEndpoint(syncers map[openrtb_ext.BidderName]usersync.Usersynce
 		syncPermissions: syncPermissions,
 		metrics:         metrics,
 		pbsAnalytics:    pbsAnalytics,
+		enforceCCPA:     cfg.CCPA.Enforce,
 	}
 	return deps.Endpoint
 }
@@ -43,6 +44,7 @@ type cookieSyncDeps struct {
 	syncPermissions gdpr.Permissions
 	metrics         pbsmetrics.MetricsEngine
 	pbsAnalytics    analytics.PBSAnalyticsModule
+	enforceCCPA     bool
 }
 
 func (deps *cookieSyncDeps) Endpoint(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
@@ -103,15 +105,26 @@ func (deps *cookieSyncDeps) Endpoint(w http.ResponseWriter, r *http.Request, _ h
 		}
 	}
 
+	privacyPolicy := privacy.Policies{
+		GDPR: gdprPolicy.Policy{
+			Signal:  gdprToString(parsedReq.GDPR),
+			Consent: parsedReq.Consent,
+		},
+		CCPA: ccpa.Policy{
+			Value: parsedReq.USPrivacy,
+		},
+	}
+
 	parsedReq.filterExistingSyncs(deps.syncers, userSyncCookie, needSyncupForSameSite)
+
 	adapterSyncs := make(map[openrtb_ext.BidderName]bool)
+	// assume all bidders will be privacy blocked
 	for _, b := range parsedReq.Bidders {
-		// assume all bidders will be GDPR blocked
 		adapterSyncs[openrtb_ext.BidderName(b)] = true
 	}
-	parsedReq.filterForGDPR(deps.syncPermissions)
+	parsedReq.filterForPrivacy(deps.syncPermissions, privacyPolicy, deps.enforceCCPA)
+	// surviving bidders are not privacy blocked
 	for _, b := range parsedReq.Bidders {
-		// surviving bidders are not GDPR blocked
 		adapterSyncs[openrtb_ext.BidderName(b)] = false
 	}
 	for b, g := range adapterSyncs {
@@ -125,15 +138,6 @@ func (deps *cookieSyncDeps) Endpoint(w http.ResponseWriter, r *http.Request, _ h
 	}
 	for i := 0; i < len(parsedReq.Bidders); i++ {
 		bidder := parsedReq.Bidders[i]
-		privacyPolicy := privacy.Policies{
-			GDPR: gdprPolicy.Policy{
-				Signal:  gdprToString(parsedReq.GDPR),
-				Consent: parsedReq.Consent,
-			},
-			CCPA: ccpa.Policy{
-				Signal: parsedReq.USPrivacy,
-			},
-		}
 		syncInfo, err := deps.syncers[openrtb_ext.BidderName(bidder)].GetUsersyncInfo(privacyPolicy)
 		if err == nil {
 			newSync := &usersync.CookieSyncBidders{
@@ -219,7 +223,12 @@ func (req *cookieSyncRequest) filterExistingSyncs(valid map[openrtb_ext.BidderNa
 	}
 }
 
-func (req *cookieSyncRequest) filterForGDPR(permissions gdpr.Permissions) {
+func (req *cookieSyncRequest) filterForPrivacy(permissions gdpr.Permissions, privacyPolicies privacy.Policies, enforceCCPA bool) {
+	if enforceCCPA && privacyPolicies.CCPA.ShouldEnforce() {
+		req.Bidders = nil
+		return
+	}
+
 	if req.GDPR != nil && *req.GDPR == 0 {
 		return
 	}
diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go
index 6b64238f..d92f9d0a 100644
--- a/endpoints/openrtb2/amp_auction.go
+++ b/endpoints/openrtb2/amp_auction.go
@@ -384,13 +384,12 @@ func (deps *endpointDeps) overrideWithParams(httpRequest *http.Request, req *ope
 
 	privacyPolicies := privacy.Policies{
 		GDPR: gdpr.Policy{
-			Consent: getQueryParam(httpRequest, "gdpr_consent"),
+			Consent: httpRequest.URL.Query().Get("gdpr_consent"),
 		},
 		CCPA: ccpa.Policy{
-			Signal: getQueryParam(httpRequest, "us_privacy"),
+			Value: httpRequest.URL.Query().Get("us_privacy"),
 		},
 	}
-
 	if err := privacyPolicies.Write(req); err != nil {
 		return err
 	}
@@ -402,17 +401,6 @@ func (deps *endpointDeps) overrideWithParams(httpRequest *http.Request, req *ope
 	return nil
 }
 
-func getQueryParam(httpRequest *http.Request, name string) string {
-	values, ok := httpRequest.URL.Query()[name]
-
-	if !ok || len(values) == 0 {
-		return ""
-	}
-
-	// return first value of the query param, matching the behavior of httpRequest.FormValue
-	return values[0]
-}
-
 func makeFormatReplacement(overrideWidth uint64, overrideHeight uint64, width uint64, height uint64, multisize string) []openrtb.Format {
 	if overrideWidth != 0 && overrideHeight != 0 {
 		return []openrtb.Format{{
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index 61ea929e..a0ed19e5 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -27,6 +27,7 @@ import (
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/pbsmetrics"
 	"github.com/prebid/prebid-server/prebid"
+	"github.com/prebid/prebid-server/privacy/ccpa"
 	"github.com/prebid/prebid-server/stored_requests"
 	"github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher"
 	"github.com/prebid/prebid-server/usersync"
@@ -301,6 +302,16 @@ func (deps *endpointDeps) validateRequest(req *openrtb.BidRequest) []error {
 		return errL
 	}
 
+	ccpaPolicy, ccpaPolicyErr := ccpa.ReadPolicy(req)
+	if ccpaPolicyErr != nil {
+		errL = append(errL, ccpaPolicyErr)
+		return errL
+	}
+
+	if err := ccpaPolicy.Validate(); err != nil {
+		errL = append(errL, &errortypes.Warning{Message: fmt.Sprintf("CCPA value is invalid and will be ignored. (%s)", err.Error())})
+	}
+
 	impIDs := make(map[string]int, len(req.Imp))
 	for index := range req.Imp {
 		imp := &req.Imp[index]
diff --git a/endpoints/openrtb2/sample-requests/account-required/no-acct.json b/endpoints/openrtb2/sample-requests/account-required/no-acct.json
index 4ba8dec5..d84d7970 100644
--- a/endpoints/openrtb2/sample-requests/account-required/no-acct.json
+++ b/endpoints/openrtb2/sample-requests/account-required/no-acct.json
@@ -28,7 +28,7 @@
           },
           "ext": {
             "appnexus": {
-              "placementId": 10433394
+              "placementId": 12883451
             }
           }
         }
diff --git a/endpoints/openrtb2/sample-requests/account-required/with-acct.json b/endpoints/openrtb2/sample-requests/account-required/with-acct.json
index d1ab715c..fb4c6313 100644
--- a/endpoints/openrtb2/sample-requests/account-required/with-acct.json
+++ b/endpoints/openrtb2/sample-requests/account-required/with-acct.json
@@ -29,7 +29,7 @@
           },
           "ext": {
             "appnexus": {
-              "placementId": 10433394
+              "placementId": 12883451
             }
           }
         }
diff --git a/endpoints/openrtb2/sample-requests/aliased/simple.json b/endpoints/openrtb2/sample-requests/aliased/simple.json
index e656be43..e7f6ba21 100644
--- a/endpoints/openrtb2/sample-requests/aliased/simple.json
+++ b/endpoints/openrtb2/sample-requests/aliased/simple.json
@@ -13,7 +13,7 @@
         },
         "ext": {
           "test1": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       }
diff --git a/endpoints/openrtb2/sample-requests/aliased/site.json b/endpoints/openrtb2/sample-requests/aliased/site.json
index 981b69b5..cf7e9a77 100644
--- a/endpoints/openrtb2/sample-requests/aliased/site.json
+++ b/endpoints/openrtb2/sample-requests/aliased/site.json
@@ -23,10 +23,10 @@
         },
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           },
           "test1": {
-            "placementId": 10433394
+            "placementId": 12883451
           },
           "test2": {
             "accountId": 1001,
diff --git a/endpoints/openrtb2/sample-requests/blacklisted/blacklisted-acct.json b/endpoints/openrtb2/sample-requests/blacklisted/blacklisted-acct.json
index 8977480d..ee04a946 100644
--- a/endpoints/openrtb2/sample-requests/blacklisted/blacklisted-acct.json
+++ b/endpoints/openrtb2/sample-requests/blacklisted/blacklisted-acct.json
@@ -42,7 +42,7 @@
           },
           "ext": {
             "appnexus": {
-              "placementId": 10433394
+              "placementId": 12883451
             },
             "districtm": {
               "placementId": 105
diff --git a/endpoints/openrtb2/sample-requests/blacklisted/blacklisted-app.json b/endpoints/openrtb2/sample-requests/blacklisted/blacklisted-app.json
index 219595e4..1ace4b53 100644
--- a/endpoints/openrtb2/sample-requests/blacklisted/blacklisted-app.json
+++ b/endpoints/openrtb2/sample-requests/blacklisted/blacklisted-app.json
@@ -39,7 +39,7 @@
         },
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           },
           "districtm": {
             "placementId": 105
diff --git a/endpoints/openrtb2/sample-requests/disabled/bad/bad-alias.json b/endpoints/openrtb2/sample-requests/disabled/bad/bad-alias.json
index 8299f96c..096c028c 100644
--- a/endpoints/openrtb2/sample-requests/disabled/bad/bad-alias.json
+++ b/endpoints/openrtb2/sample-requests/disabled/bad/bad-alias.json
@@ -15,7 +15,7 @@
         },
         "ext": {
           "test1": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       }
diff --git a/endpoints/openrtb2/sample-requests/disabled/bad/bad-bidder.json b/endpoints/openrtb2/sample-requests/disabled/bad/bad-bidder.json
index f0313824..1fa21b20 100644
--- a/endpoints/openrtb2/sample-requests/disabled/bad/bad-bidder.json
+++ b/endpoints/openrtb2/sample-requests/disabled/bad/bad-bidder.json
@@ -15,7 +15,7 @@
         },
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       }
diff --git a/endpoints/openrtb2/sample-requests/disabled/good/partial.json b/endpoints/openrtb2/sample-requests/disabled/good/partial.json
index 0f6bf1dd..fe0c492b 100644
--- a/endpoints/openrtb2/sample-requests/disabled/good/partial.json
+++ b/endpoints/openrtb2/sample-requests/disabled/good/partial.json
@@ -23,7 +23,7 @@
         },
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           },
           "rubicon": {
             "accountId": 1001,
diff --git a/endpoints/openrtb2/sample-requests/invalid-stored/bad_incoming_1.json b/endpoints/openrtb2/sample-requests/invalid-stored/bad_incoming_1.json
index ccb54c05..2a647d7d 100644
--- a/endpoints/openrtb2/sample-requests/invalid-stored/bad_incoming_1.json
+++ b/endpoints/openrtb2/sample-requests/invalid-stored/bad_incoming_1.json
@@ -24,7 +24,7 @@
           },
           "ext": {
             "appnexus": {
-              "placementId": 10433394
+              "placementId": 12883451
             }
           }
         }
diff --git a/endpoints/openrtb2/sample-requests/invalid-stored/bad_incoming_2.json b/endpoints/openrtb2/sample-requests/invalid-stored/bad_incoming_2.json
index 6023f869..d18a20d7 100644
--- a/endpoints/openrtb2/sample-requests/invalid-stored/bad_incoming_2.json
+++ b/endpoints/openrtb2/sample-requests/invalid-stored/bad_incoming_2.json
@@ -24,7 +24,7 @@
           },
           "ext": {
             "appnexus": {
-              "placementId": 10433394
+              "placementId": 12883451
             }
           }
         }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/alias-bidder-self.json b/endpoints/openrtb2/sample-requests/invalid-whole/alias-bidder-self.json
index f081b365..666253ec 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/alias-bidder-self.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/alias-bidder-self.json
@@ -13,7 +13,7 @@
         },
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/alias-unknown-core.json b/endpoints/openrtb2/sample-requests/invalid-whole/alias-unknown-core.json
index acf3bc0d..6c1925d6 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/alias-unknown-core.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/alias-unknown-core.json
@@ -15,7 +15,7 @@
         },
         "ext": {
           "unknown": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/app-bad-ext.json b/endpoints/openrtb2/sample-requests/invalid-whole/app-bad-ext.json
index 2516a268..672b0572 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/app-bad-ext.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/app-bad-ext.json
@@ -32,7 +32,7 @@
                 },
                 "ext": {
                 "appnexus": {
-                    "placementId": 10433394
+                    "placementId": 12883451
                 }
                 }
             }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/banner-h-only.json b/endpoints/openrtb2/sample-requests/invalid-whole/banner-h-only.json
index 7040e8df..515824e6 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/banner-h-only.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/banner-h-only.json
@@ -13,7 +13,7 @@
         },
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/banner-h-zero.json b/endpoints/openrtb2/sample-requests/invalid-whole/banner-h-zero.json
index d98cfba1..f18f63e5 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/banner-h-zero.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/banner-h-zero.json
@@ -14,7 +14,7 @@
         },
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/banner-w-only.json b/endpoints/openrtb2/sample-requests/invalid-whole/banner-w-only.json
index 52a7bdd2..70739a65 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/banner-w-only.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/banner-w-only.json
@@ -13,7 +13,7 @@
         },
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/banner-w-zero.json b/endpoints/openrtb2/sample-requests/invalid-whole/banner-w-zero.json
index 0b927f26..b3453ab4 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/banner-w-zero.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/banner-w-zero.json
@@ -14,7 +14,7 @@
         },
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/bid-adjustment-invalid-bidder.json b/endpoints/openrtb2/sample-requests/invalid-whole/bid-adjustment-invalid-bidder.json
index 985f9912..569e16d2 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/bid-adjustment-invalid-bidder.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/bid-adjustment-invalid-bidder.json
@@ -13,7 +13,7 @@
         },
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/bid-adjustment-negative.json b/endpoints/openrtb2/sample-requests/invalid-whole/bid-adjustment-negative.json
index 98499800..4db6ee09 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/bid-adjustment-negative.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/bid-adjustment-negative.json
@@ -13,7 +13,7 @@
         },
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/cache-nothing.json b/endpoints/openrtb2/sample-requests/invalid-whole/cache-nothing.json
index 605040c6..d4b87549 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/cache-nothing.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/cache-nothing.json
@@ -15,7 +15,7 @@
         },
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/digitrust.json b/endpoints/openrtb2/sample-requests/invalid-whole/digitrust.json
index 2b65ed19..1be93853 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/digitrust.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/digitrust.json
@@ -25,7 +25,7 @@
         },
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/format-empty-array.json b/endpoints/openrtb2/sample-requests/invalid-whole/format-empty-array.json
index e159601b..15e41cc5 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/format-empty-array.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/format-empty-array.json
@@ -13,7 +13,7 @@
         },
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/imp-id-duplicates.json b/endpoints/openrtb2/sample-requests/invalid-whole/imp-id-duplicates.json
index 095542ef..53517c26 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/imp-id-duplicates.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/imp-id-duplicates.json
@@ -18,7 +18,7 @@
         },
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       },
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/interstital-bad-perc.json b/endpoints/openrtb2/sample-requests/invalid-whole/interstital-bad-perc.json
index f04f3d8a..6854ea9a 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/interstital-bad-perc.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/interstital-bad-perc.json
@@ -20,7 +20,7 @@
             "instl": 1,
             "ext": {
             "appnexus": {
-                "placementId": 10433394
+                "placementId": 12883451
             }
             }
         }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/interstitial-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/interstitial-empty.json
index 5fa28059..a69f287d 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/interstitial-empty.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/interstitial-empty.json
@@ -14,7 +14,7 @@
             "instl": 1,
             "ext": {
             "appnexus": {
-                "placementId": 10433394
+                "placementId": 12883451
             }
             }
         }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/no-site-or-app.json b/endpoints/openrtb2/sample-requests/invalid-whole/no-site-or-app.json
index 1cbfdd50..c56dae32 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/no-site-or-app.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/no-site-or-app.json
@@ -12,7 +12,7 @@
         },
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-invalid.json b/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-invalid.json
index 6737295a..dff3023c 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-invalid.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-invalid.json
@@ -17,7 +17,7 @@
         "id": "/19968336/header-bid-tag-0",
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         },
         "banner": {
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 3abc88c7..ce887889 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
@@ -17,7 +17,7 @@
         "id": "/19968336/header-bid-tag-0",
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         },
         "banner": {
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-malformed.json b/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-malformed.json
new file mode 100644
index 00000000..a403103d
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-malformed.json
@@ -0,0 +1,45 @@
+{
+    "message": "Invalid request: request.regs.ext is invalid: json: cannot unmarshal string into Go value of type openrtb_ext.ExtRegs\n",
+    "requestPayload": {
+      "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5",
+      "site": {
+        "page": "prebid.org",
+        "publisher": {
+        "id": "a3de7af2-a86a-4043-a77b-c7e86744155e"
+        }
+      },
+      "source": {
+        "tid": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5"
+      },
+      "tmax": 1000,
+      "imp": [
+        {
+          "id": "/19968336/header-bid-tag-0",
+          "ext": {
+            "appnexus": {
+              "placementId": 12883451
+            }
+          },
+          "banner": {
+            "format": [
+              {
+               "w": 300,
+                "h": 250
+              },
+              {
+                "w": 300,
+                "h": 300
+              }
+            ]
+          }
+        }
+      ],
+      "regs": {
+        "ext": "malformed"
+      },
+      "user": {
+        "ext": {}
+      }
+    }
+  }
+  
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/site-app-both.json b/endpoints/openrtb2/sample-requests/invalid-whole/site-app-both.json
index abbcdac3..4b643705 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/site-app-both.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/site-app-both.json
@@ -16,7 +16,7 @@
         },
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/site-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/site-empty.json
index 430b25f6..3d53314d 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/site-empty.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/site-empty.json
@@ -13,7 +13,7 @@
         },
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/site-ext-amp.json b/endpoints/openrtb2/sample-requests/invalid-whole/site-ext-amp.json
index 17cd68b9..bebe4625 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/site-ext-amp.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/site-ext-amp.json
@@ -28,7 +28,7 @@
         },
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/storedrequest-id-int.json b/endpoints/openrtb2/sample-requests/invalid-whole/storedrequest-id-int.json
index 3fe94a99..5d510d21 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/storedrequest-id-int.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/storedrequest-id-int.json
@@ -15,7 +15,7 @@
         },
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/unknown-bidder.json b/endpoints/openrtb2/sample-requests/invalid-whole/unknown-bidder.json
index f11be6fd..3914ae7a 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/unknown-bidder.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/unknown-bidder.json
@@ -24,7 +24,7 @@
           },
           "ext": {
             "appnexus": {
-              "placementId": 10433394
+              "placementId": 12883451
             },
             "unknownbidder": {
                 "param1": "foobar",
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 c7a6a53b..5bc0ed33 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
@@ -17,7 +17,7 @@
         "id": "/19968336/header-bid-tag-0",
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         },
         "banner": {
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-eids-uids-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-eids-uids-empty.json
index e9914e3b..ebbb4e27 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-eids-uids-empty.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-eids-uids-empty.json
@@ -17,7 +17,7 @@
         "id": "/19968336/header-bid-tag-0",
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         },
         "banner": {
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-empty.json
index 4f66917f..3d73d731 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-empty.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-empty.json
@@ -17,7 +17,7 @@
         "id": "/19968336/header-bid-tag-0",
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         },
         "banner": {
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-id-uids-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-id-uids-empty.json
index 1ca50079..bbd0dadf 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-id-uids-empty.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-id-uids-empty.json
@@ -17,7 +17,7 @@
         "id": "/19968336/header-bid-tag-0",
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         },
         "banner": {
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-source-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-source-empty.json
index 85262980..5efff062 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-source-empty.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-source-empty.json
@@ -17,7 +17,7 @@
         "id": "/19968336/header-bid-tag-0",
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         },
         "banner": {
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-source-unique.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-source-unique.json
index 52565d47..e508b113 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-source-unique.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-source-unique.json
@@ -17,7 +17,7 @@
         "id": "/19968336/header-bid-tag-0",
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         },
         "banner": {
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-uids-id-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-uids-id-empty.json
index 33a33b9f..3a9659b7 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-uids-id-empty.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-uids-id-empty.json
@@ -17,7 +17,7 @@
         "id": "/19968336/header-bid-tag-0",
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         },
         "banner": {
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-prebid-buyeruids-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-prebid-buyeruids-empty.json
index d967463d..44bee775 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-prebid-buyeruids-empty.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-prebid-buyeruids-empty.json
@@ -18,7 +18,7 @@
         },
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-prebid-buyeruids-unknown.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-prebid-buyeruids-unknown.json
index bad67e2d..78773066 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-prebid-buyeruids-unknown.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-prebid-buyeruids-unknown.json
@@ -18,7 +18,7 @@
         },
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-prebid-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-prebid-empty.json
index 8113a70b..f2e49751 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-prebid-empty.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-prebid-empty.json
@@ -18,7 +18,7 @@
         },
         "ext": {
           "appnexus":{
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-badtype.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-badtype.json
index 3abc88c7..ce887889 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-badtype.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-badtype.json
@@ -17,7 +17,7 @@
         "id": "/19968336/header-bid-tag-0",
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         },
         "banner": {
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-invalid.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-invalid.json
index b2b55484..0729a22d 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-invalid.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-invalid.json
@@ -17,7 +17,7 @@
         "id": "/19968336/header-bid-tag-0",
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         },
         "banner": {
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/all-ext.json b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/all-ext.json
index a111e987..3e2beede 100644
--- a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/all-ext.json
+++ b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/all-ext.json
@@ -18,7 +18,8 @@
     },
     "regs": {
       "ext": {
-        "gdpr": 1
+        "gdpr": 1,
+        "us_privacy": "1NYN"
       }
     },
     "imp": [
@@ -38,7 +39,7 @@
         },
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           },
           "districtm": {
             "placementId": 105
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/aliased-buyeruids.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/aliased-buyeruids.json
index 1eb74614..82125592 100644
--- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/aliased-buyeruids.json
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/aliased-buyeruids.json
@@ -16,7 +16,7 @@
       },
       "ext": {
         "appnexus": {
-          "placementId": 10433394
+          "placementId": 12883451
         }
       }
     }
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/aliases.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/aliases.json
index 6c8608ba..f6137e4a 100644
--- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/aliases.json
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/aliases.json
@@ -13,7 +13,7 @@
       },
       "ext": {
         "unknown": {
-          "placementId": 10433394
+          "placementId": 12883451
         }
       }
     }
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/app.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/app.json
index cad5e852..66e05d76 100644
--- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/app.json
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/app.json
@@ -28,7 +28,7 @@
       },
       "ext": {
         "appnexus": {
-          "placementId": 10433394
+          "placementId": 12883451
         }
       }
     }
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/bid-adjustments.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/bid-adjustments.json
index 2ce81b97..0cf52a89 100644
--- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/bid-adjustments.json
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/bid-adjustments.json
@@ -13,7 +13,7 @@
       },
       "ext": {
         "unknown": {
-          "placementId": 10433394
+          "placementId": 12883451
         }
       }
     }
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/cache-bids.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/cache-bids.json
index 884ec898..a4c93b3d 100644
--- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/cache-bids.json
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/cache-bids.json
@@ -13,7 +13,7 @@
       },
       "ext": {
         "appnexus": {
-          "placementId": 10433394
+          "placementId": 12883451
         }
       }
     }
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/cache-vast.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/cache-vast.json
index 7878767a..fe944535 100644
--- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/cache-vast.json
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/cache-vast.json
@@ -13,7 +13,7 @@
       },
       "ext": {
         "appnexus": {
-          "placementId": 10433394
+          "placementId": 12883451
         }
       }
     }
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/ccpa-invalid.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/ccpa-invalid.json
new file mode 100644
index 00000000..f3b67763
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/ccpa-invalid.json
@@ -0,0 +1,41 @@
+{
+    "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5",
+    "site": {
+      "page": "prebid.org",
+      "publisher": {
+        "id": "a3de7af2-a86a-4043-a77b-c7e86744155e"
+      }
+    },
+    "source": {
+      "tid": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5"
+    },
+    "tmax": 1000,
+    "imp": [
+      {
+        "id": "/19968336/header-bid-tag-0",
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          }
+        },
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 300
+            }
+          ]
+        }
+      }
+    ],
+    "regs": {
+      "ext": {
+        "us_privacy": "invalid by length. allowed since it only produces a warning."
+      }
+    }
+  }
+  
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/digitrust.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/digitrust.json
index 0017f5e7..ca8e0907 100644
--- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/digitrust.json
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/digitrust.json
@@ -23,7 +23,7 @@
       },
       "ext": {
         "appnexus": {
-          "placementId": 10433394
+          "placementId": 12883451
         }
       }
     }
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/gdpr-no-consentstring.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/gdpr-no-consentstring.json
index 77f1f86b..5a63c6d1 100644
--- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/gdpr-no-consentstring.json
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/gdpr-no-consentstring.json
@@ -15,7 +15,7 @@
       "id": "/19968336/header-bid-tag-0",
       "ext": {
         "appnexus": {
-          "placementId": 10433394
+          "placementId": 12883451
         }
       },
       "banner": {
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/gdpr.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/gdpr.json
index 1234f58d..ef9f10d0 100644
--- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/gdpr.json
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/gdpr.json
@@ -15,7 +15,7 @@
       "id": "/19968336/header-bid-tag-0",
       "ext": {
         "appnexus": {
-          "placementId": 10433394
+          "placementId": 12883451
         }
       },
       "banner": {
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/interstitial-device-only.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/interstitial-device-only.json
index 2c85ef14..64146eae 100644
--- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/interstitial-device-only.json
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/interstitial-device-only.json
@@ -10,7 +10,7 @@
         "instl": 1,
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       }
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/interstitial-no-extension.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/interstitial-no-extension.json
index 1e6604e7..15cd8320 100644
--- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/interstitial-no-extension.json
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/interstitial-no-extension.json
@@ -17,7 +17,7 @@
         "instl": 1,
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       }
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/interstitial.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/interstitial.json
index c5d37379..64fc2fe2 100644
--- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/interstitial.json
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/interstitial.json
@@ -17,7 +17,7 @@
         "instl": 1,
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       }
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-amp.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-amp.json
index c0dbb355..30c0afc8 100644
--- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-amp.json
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-amp.json
@@ -26,7 +26,7 @@
       },
       "ext": {
         "appnexus": {
-          "placementId": 10433394
+          "placementId": 12883451
         }
       }
     }
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site.json
index f0a02103..7a25249c 100644
--- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site.json
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site.json
@@ -23,7 +23,7 @@
       },
       "ext": {
         "appnexus": {
-          "placementId": 10433394
+          "placementId": 12883451
         }
       }
     }
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/timeout.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/timeout.json
index 611d96ca..b3dbe1f5 100644
--- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/timeout.json
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/timeout.json
@@ -22,7 +22,7 @@
       },
       "ext": {
         "appnexus": {
-          "placementId": 10433394
+          "placementId": 12883451
         }
       }
     }
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/user.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/user.json
index 951a7774..243b0739 100644
--- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/user.json
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/user.json
@@ -23,7 +23,7 @@
       },
       "ext": {
         "appnexus": {
-          "placementId": 10433394
+          "placementId": 12883451
         }
       }
     }
diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go
index 1f234b85..b8b21b76 100644
--- a/endpoints/openrtb2/video_auction.go
+++ b/endpoints/openrtb2/video_auction.go
@@ -65,7 +65,7 @@ func NewVideoEndpoint(ex exchange.Exchange, validator openrtb_ext.BidderParamVal
 */
 func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
 
-	ao := analytics.AuctionObject{
+	vo := analytics.VideoObject{
 		Status: http.StatusOK,
 		Errors: make([]error, 0),
 	}
@@ -82,7 +82,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 	defer func() {
 		deps.metricsEngine.RecordRequest(labels)
 		deps.metricsEngine.RecordRequestTime(labels, time.Since(start))
-		deps.analytics.LogAuctionObject(&ao)
+		deps.analytics.LogVideoObject(&vo)
 	}()
 
 	lr := &io.LimitedReader{
@@ -91,7 +91,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 	}
 	requestJson, err := ioutil.ReadAll(lr)
 	if err != nil {
-		handleError(&labels, w, []error{err}, &ao)
+		handleError(&labels, w, []error{err}, &vo)
 		return
 	}
 
@@ -102,35 +102,37 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 
 	if err != nil {
 		if deps.cfg.VideoStoredRequestRequired {
-			handleError(&labels, w, []error{err}, &ao)
+			handleError(&labels, w, []error{err}, &vo)
 			return
 		}
 	} else {
 		storedRequest, errs := deps.loadStoredVideoRequest(context.Background(), storedRequestId)
 		if len(errs) > 0 {
-			handleError(&labels, w, errs, &ao)
+			handleError(&labels, w, errs, &vo)
 			return
 		}
 
 		//merge incoming req with stored video req
 		resolvedRequest, err = jsonpatch.MergePatch(storedRequest, requestJson)
 		if err != nil {
-			handleError(&labels, w, []error{err}, &ao)
+			handleError(&labels, w, []error{err}, &vo)
 			return
 		}
 	}
 	//unmarshal and validate combined result
 	videoBidReq, errL, podErrors := deps.parseVideoRequest(resolvedRequest)
 	if len(errL) > 0 {
-		handleError(&labels, w, errL, &ao)
+		handleError(&labels, w, errL, &vo)
 		return
 	}
 
+	vo.VideoRequest = videoBidReq
+
 	var bidReq = &openrtb.BidRequest{}
 	if deps.defaultRequest {
 		if err := json.Unmarshal(deps.defReqJSON, bidReq); err != nil {
 			err = fmt.Errorf("Invalid JSON in Default Request Settings: %s", err)
-			handleError(&labels, w, []error{err}, &ao)
+			handleError(&labels, w, []error{err}, &vo)
 			return
 		}
 	}
@@ -154,7 +156,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 		}
 		err := errors.New(fmt.Sprintf("all pods are incorrect: %s", strings.Join(resPodErr, "; ")))
 		errL = append(errL, err)
-		handleError(&labels, w, errL, &ao)
+		handleError(&labels, w, errL, &vo)
 		return
 	}
 
@@ -166,7 +168,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 
 	errL = deps.validateRequest(bidReq)
 	if len(errL) > 0 {
-		handleError(&labels, w, errL, &ao)
+		handleError(&labels, w, errL, &vo)
 		return
 	}
 
@@ -194,16 +196,16 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 
 	if acctIdErr := validateAccount(deps.cfg, labels.PubID); acctIdErr != nil {
 		errL = append(errL, acctIdErr)
-		handleError(&labels, w, errL, &ao)
+		handleError(&labels, w, errL, &vo)
 		return
 	}
 	//execute auction logic
 	response, err := deps.ex.HoldAuction(ctx, bidReq, usersyncs, labels, &deps.categories)
-	ao.Request = bidReq
-	ao.Response = response
+	vo.Request = bidReq
+	vo.Response = response
 	if err != nil {
 		errL := []error{err}
-		handleError(&labels, w, errL, &ao)
+		handleError(&labels, w, errL, &vo)
 		return
 	}
 
@@ -211,18 +213,20 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 	bidResp, err := buildVideoResponse(response, podErrors)
 	if err != nil {
 		errL := []error{err}
-		handleError(&labels, w, errL, &ao)
+		handleError(&labels, w, errL, &vo)
 		return
 	}
 	if bidReq.Test == 1 {
 		bidResp.Ext = response.Ext
 	}
 
+	vo.VideoResponse = bidResp
+
 	resp, err := json.Marshal(bidResp)
 	//resp, err := json.Marshal(response)
 	if err != nil {
 		errL := []error{err}
-		handleError(&labels, w, errL, &ao)
+		handleError(&labels, w, errL, &vo)
 		return
 	}
 
@@ -238,7 +242,7 @@ func cleanupVideoBidRequest(videoReq *openrtb_ext.BidRequestVideo, podErrors []P
 	return videoReq
 }
 
-func handleError(labels *pbsmetrics.Labels, w http.ResponseWriter, errL []error, ao *analytics.AuctionObject) {
+func handleError(labels *pbsmetrics.Labels, w http.ResponseWriter, errL []error, vo *analytics.VideoObject) {
 	labels.RequestStatus = pbsmetrics.RequestStatusErr
 	var errors string
 	var status int = http.StatusInternalServerError
@@ -256,10 +260,10 @@ func handleError(labels *pbsmetrics.Labels, w http.ResponseWriter, errL []error,
 		errors = fmt.Sprintf("%s %s", errors, er.Error())
 	}
 	w.WriteHeader(status)
-	ao.Status = status
+	vo.Status = status
 	fmt.Fprintf(w, "Critical error while running the video endpoint: %v", errors)
 	glog.Errorf("/openrtb2/video Critical error: %v", errors)
-	ao.Errors = append(ao.Errors, errL...)
+	vo.Errors = append(vo.Errors, errL...)
 }
 
 func (deps *endpointDeps) createImpressions(videoReq *openrtb_ext.BidRequestVideo, podErrors []PodError) ([]openrtb.Imp, []PodError) {
@@ -500,6 +504,10 @@ func mergeData(videoRequest *openrtb_ext.BidRequestVideo, bidRequest *openrtb.Bi
 		bidRequest.TMax = videoRequest.TMax
 	}
 
+	if videoRequest.Regs != nil {
+		bidRequest.Regs = videoRequest.Regs
+	}
+
 	return nil
 }
 
diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go
index 429e885c..30a31727 100644
--- a/exchange/adapter_map.go
+++ b/exchange/adapter_map.go
@@ -15,6 +15,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/adpone"
 	"github.com/prebid/prebid-server/adapters/adtelligent"
 	"github.com/prebid/prebid-server/adapters/advangelists"
+	"github.com/prebid/prebid-server/adapters/applogy"
 	"github.com/prebid/prebid-server/adapters/appnexus"
 	"github.com/prebid/prebid-server/adapters/audienceNetwork"
 	"github.com/prebid/prebid-server/adapters/beachfront"
@@ -33,6 +34,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/ix"
 	"github.com/prebid/prebid-server/adapters/lifestreet"
 	"github.com/prebid/prebid-server/adapters/lockerdome"
+	"github.com/prebid/prebid-server/adapters/marsmedia"
 	"github.com/prebid/prebid-server/adapters/mgid"
 	"github.com/prebid/prebid-server/adapters/openx"
 	"github.com/prebid/prebid-server/adapters/pubmatic"
@@ -70,6 +72,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter
 		openrtb_ext.BidderAdpone:       adpone.NewAdponeBidder(cfg.Adapters[string(openrtb_ext.BidderAdpone)].Endpoint),
 		openrtb_ext.BidderAdtelligent:  adtelligent.NewAdtelligentBidder(cfg.Adapters[string(openrtb_ext.BidderAdtelligent)].Endpoint),
 		openrtb_ext.BidderAdvangelists: advangelists.NewAdvangelistsBidder(cfg.Adapters[string(openrtb_ext.BidderAdvangelists)].Endpoint),
+		openrtb_ext.BidderApplogy:      applogy.NewApplogyBidder(cfg.Adapters[string(openrtb_ext.BidderApplogy)].Endpoint),
 		openrtb_ext.BidderAppnexus:     appnexus.NewAppNexusBidder(client, cfg.Adapters[string(openrtb_ext.BidderAppnexus)].Endpoint, cfg.Adapters[string(openrtb_ext.BidderAppnexus)].PlatformID),
 		// TODO #615: Update the config setup so that the Beachfront URLs can be configured, and use those in TestRaceIntegration in exchange_test.go
 		openrtb_ext.BidderBeachfront: beachfront.NewBeachfrontBidder(),
@@ -90,6 +93,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter
 		openrtb_ext.BidderImprovedigital: improvedigital.NewImprovedigitalBidder(cfg.Adapters[string(openrtb_ext.BidderImprovedigital)].Endpoint),
 		openrtb_ext.BidderKubient:        kubient.NewKubientBidder(cfg.Adapters[string(openrtb_ext.BidderKubient)].Endpoint),
 		openrtb_ext.BidderLockerDome:     lockerdome.NewLockerDomeBidder(cfg.Adapters[string(openrtb_ext.BidderLockerDome)].Endpoint),
+		openrtb_ext.BidderMarsmedia:      marsmedia.NewMarsmediaBidder(cfg.Adapters[string(openrtb_ext.BidderMarsmedia)].Endpoint),
 		openrtb_ext.BidderMgid:           mgid.NewMgidBidder(cfg.Adapters[string(openrtb_ext.BidderMgid)].Endpoint),
 		openrtb_ext.BidderOpenx:          openx.NewOpenxBidder(cfg.Adapters[string(openrtb_ext.BidderOpenx)].Endpoint),
 		openrtb_ext.BidderPubmatic:       pubmatic.NewPubmaticBidder(client, cfg.Adapters[string(openrtb_ext.BidderPubmatic)].Endpoint),
diff --git a/exchange/exchange.go b/exchange/exchange.go
index e92e813f..3d9055ca 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -46,6 +46,7 @@ type exchange struct {
 	currencyConverter   *currencies.RateConverter
 	UsersyncIfAmbiguous bool
 	defaultTTLs         config.DefaultTTLs
+	enforceCCPA         bool
 }
 
 // Container to pass out response ext data from the GetAllBids goroutines back into the main thread
@@ -71,6 +72,7 @@ func NewExchange(client *http.Client, cache prebid_cache_client.Client, cfg *con
 	e.currencyConverter = currencyConverter
 	e.UsersyncIfAmbiguous = cfg.GDPR.UsersyncIfAmbiguous
 	e.defaultTTLs = cfg.CacheURL.DefaultTTLs
+	e.enforceCCPA = cfg.CCPA.Enforce
 	return e
 }
 
@@ -93,7 +95,7 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque
 
 	// Slice of BidRequests, each a copy of the original cleaned to only contain bidder data for the named bidder
 	blabels := make(map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels)
-	cleanRequests, aliases, errs := cleanOpenRTBRequests(ctx, bidRequest, usersyncs, blabels, labels, e.gDPR, e.UsersyncIfAmbiguous)
+	cleanRequests, aliases, errs := cleanOpenRTBRequests(ctx, bidRequest, usersyncs, blabels, labels, e.gDPR, e.UsersyncIfAmbiguous, e.enforceCCPA)
 
 	// List of bidders we have requests for.
 	liveAdapters := listBiddersWithRequests(cleanRequests)
@@ -322,6 +324,7 @@ func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_
 		if adapterBids[a] != nil && len(adapterBids[a].bids) > 0 {
 			sb := e.makeSeatBid(adapterBids[a], a, adapterExtra, auc)
 			seatBids = append(seatBids, *sb)
+			bidResponse.Cur = adapterBids[a].currency
 		}
 	}
 
diff --git a/exchange/utils.go b/exchange/utils.go
index c77d907c..d1c95b88 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -5,13 +5,14 @@ import (
 	"encoding/json"
 	"fmt"
 	"math/rand"
-	"strings"
 
 	"github.com/buger/jsonparser"
 	"github.com/mxmCherry/openrtb"
 	"github.com/prebid/prebid-server/gdpr"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/pbsmetrics"
+	"github.com/prebid/prebid-server/privacy"
+	"github.com/prebid/prebid-server/privacy/ccpa"
 )
 
 // cleanOpenRTBRequests splits the input request into requests which are sanitized for each bidder. Intended behavior is:
@@ -25,7 +26,8 @@ func cleanOpenRTBRequests(ctx context.Context,
 	blables map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels,
 	labels pbsmetrics.Labels,
 	gDPR gdpr.Permissions,
-	usersyncIfAmbiguous bool) (requestsByBidder map[openrtb_ext.BidderName]*openrtb.BidRequest, aliases map[string]string, errs []error) {
+	usersyncIfAmbiguous,
+	enforceCCPA bool) (requestsByBidder map[openrtb_ext.BidderName]*openrtb.BidRequest, aliases map[string]string, errs []error) {
 
 	impsByBidder, errs := splitImps(orig.Imp)
 	if len(errs) > 0 {
@@ -39,34 +41,32 @@ func cleanOpenRTBRequests(ctx context.Context,
 
 	requestsByBidder, errs = splitBidRequest(orig, impsByBidder, aliases, usersyncs, blables, labels)
 
-	// Clean PI from bidrequests if not allowed per GDPR
 	gdpr := extractGDPR(orig, usersyncIfAmbiguous)
 	consent := extractConsent(orig)
-
-	// Check if it's an AMP request
 	isAMP := labels.RType == pbsmetrics.ReqTypeAMP
 
-	// Check if COPPA applies for this request
-	var applyCOPPA bool
-	if orig.Regs != nil && orig.Regs.COPPA == 1 {
-		applyCOPPA = true
+	privacyEnforcement := privacy.Enforcement{
+		COPPA: orig.Regs != nil && orig.Regs.COPPA == 1,
+	}
+
+	if enforceCCPA {
+		ccpaPolicy, _ := ccpa.ReadPolicy(orig)
+		privacyEnforcement.CCPA = ccpaPolicy.ShouldEnforce()
 	}
 
 	for bidder, bidReq := range requestsByBidder {
-		var applyGDPR bool
-		// Fixes #820
+
 		if gdpr == 1 {
 			coreBidder := resolveBidder(bidder.String(), aliases)
 
 			var publisherID = labels.PubID
-			if ok, err := gDPR.PersonalInfoAllowed(ctx, coreBidder, publisherID, consent); !ok && err == nil {
-				applyGDPR = true
-			}
+			ok, err := gDPR.PersonalInfoAllowed(ctx, coreBidder, publisherID, consent)
+			privacyEnforcement.GDPR = !ok && err == nil
+		} else {
+			privacyEnforcement.GDPR = false
 		}
 
-		if applyGDPR || applyCOPPA {
-			applyRegs(bidReq, isAMP, applyGDPR, applyCOPPA)
-		}
+		privacyEnforcement.Apply(bidReq, isAMP)
 	}
 
 	return
@@ -313,105 +313,3 @@ func randomizeList(list []openrtb_ext.BidderName) {
 		list[i], list[j] = list[j], list[i]
 	}
 }
-
-func applyRegs(bidRequest *openrtb.BidRequest, isAMP, applyGDPR, applyCOPPA bool) {
-	if bidRequest.User != nil {
-		// Need to duplicate pointer objects
-		user := *bidRequest.User
-		bidRequest.User = &user
-
-		// There's no way for AMP to send a GDPR consent string yet so it's hard
-		// to know if the vendor is consented or not and therefore for AMP requests
-		// we keep the BuyerUID as is
-		if !isAMP {
-			bidRequest.User.BuyerUID = ""
-		}
-		if applyCOPPA {
-			bidRequest.User.ID = ""
-			bidRequest.User.Yob = 0
-			bidRequest.User.Gender = ""
-			bidRequest.User.BuyerUID = ""
-		}
-		bidRequest.User.Geo = cleanGeo(bidRequest.User.Geo, applyGDPR, applyCOPPA)
-	}
-	if bidRequest.Device != nil {
-		// Need to duplicate pointer objects
-		device := *bidRequest.Device
-		bidRequest.Device = &device
-
-		bidRequest.Device.DIDMD5 = ""
-		bidRequest.Device.DIDSHA1 = ""
-		bidRequest.Device.DPIDMD5 = ""
-		bidRequest.Device.DPIDSHA1 = ""
-		if applyCOPPA {
-			bidRequest.Device.MACSHA1 = ""
-			bidRequest.Device.MACMD5 = ""
-			bidRequest.Device.IFA = ""
-		}
-		bidRequest.Device.IP = cleanIP(bidRequest.Device.IP)
-		bidRequest.Device.IPv6 = cleanIPV6(bidRequest.Device.IPv6, applyGDPR, applyCOPPA)
-		bidRequest.Device.Geo = cleanGeo(bidRequest.Device.Geo, applyGDPR, applyCOPPA)
-	}
-}
-
-// Zero the last byte of an IP address
-func cleanIP(fullIP string) string {
-	i := strings.LastIndex(fullIP, ".")
-	if i == -1 {
-		return ""
-	}
-	return fullIP[0:i] + ".0"
-}
-
-func cleanIPV6(fullIP string, applyGDPR, applyCOPPA bool) string {
-	// If neither GDPR nor COPPA applies then do nothing
-	if !applyGDPR && !applyCOPPA {
-		return fullIP
-	}
-
-	i := strings.LastIndex(fullIP, ":")
-	if i == -1 {
-		return ""
-	}
-	fullIP = fullIP[0:i]
-
-	// If COPPA then remove the lowest 32 bits of the IP
-	if applyCOPPA {
-		fullIP = fullIP[:strings.LastIndex(fullIP, ":")+1] + "0:0"
-		return fullIP
-	}
-	// If GDPR then remove the lowest 32 bits of the IP
-	if applyGDPR {
-		fullIP = fullIP + ":0"
-	}
-	return fullIP
-}
-
-// Return a cleaned Geo object pointer (round off the latitude/longitude)
-func cleanGeo(geo *openrtb.Geo, applyGDPR, applyCOPPA bool) *openrtb.Geo {
-	// If neither GDPR nor COPPA applies then do nothing
-	if !applyCOPPA && !applyGDPR {
-		return geo
-	}
-
-	if geo == nil {
-		return nil
-	}
-	newGeo := *geo
-
-	// If GDPR applies then round off the Lat and LON values
-	if applyGDPR {
-		newGeo.Lat = float64(int(geo.Lat*100.0+0.5)) / 100.0
-		newGeo.Lon = float64(int(geo.Lon*100.0+0.5)) / 100.0
-	}
-
-	// If COPPA applies then remove Lat, Lon, Metro, City and Zip values
-	if applyCOPPA {
-		newGeo.Lat = 0
-		newGeo.Lon = 0
-		newGeo.Metro = ""
-		newGeo.City = ""
-		newGeo.ZIP = ""
-	}
-	return &newGeo
-}
diff --git a/openrtb_ext/bid_request_video.go b/openrtb_ext/bid_request_video.go
index 43b780fc..45447685 100644
--- a/openrtb_ext/bid_request_video.go
+++ b/openrtb_ext/bid_request_video.go
@@ -128,6 +128,14 @@ type BidRequestVideo struct {
 	// Description:
 	//   Block list of advertisers by their domains (e.g., “ford.com”).
 	BAdv []string `json:"badv,omitempty"`
+
+	// Attribute:
+	//   regs
+	// Type:
+	//   object; optional
+	// Description:
+	//   Contains the OpenRTB Regs object to be passed to OpenRTB request
+	Regs *openrtb.Regs `json:"regs,omitempty"`
 }
 
 type PodConfig struct {
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index eb8fef81..9621b23d 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -27,6 +27,7 @@ const (
 	BidderAdpone           BidderName = "adpone"
 	BidderAdtelligent      BidderName = "adtelligent"
 	BidderAdvangelists     BidderName = "advangelists"
+	BidderApplogy          BidderName = "applogy"
 	BidderAppnexus         BidderName = "appnexus"
 	BidderBeachfront       BidderName = "beachfront"
 	BidderBrightroll       BidderName = "brightroll"
@@ -46,6 +47,7 @@ const (
 	BidderKubient          BidderName = "kubient"
 	BidderLifestreet       BidderName = "lifestreet"
 	BidderLockerDome       BidderName = "lockerdome"
+	BidderMarsmedia        BidderName = "marsmedia"
 	BidderMgid             BidderName = "mgid"
 	BidderOpenx            BidderName = "openx"
 	BidderPubmatic         BidderName = "pubmatic"
@@ -78,6 +80,7 @@ var BidderMap = map[string]BidderName{
 	"adpone":            BidderAdpone,
 	"adtelligent":       BidderAdtelligent,
 	"advangelists":      BidderAdvangelists,
+	"applogy":           BidderApplogy,
 	"appnexus":          BidderAppnexus,
 	"beachfront":        BidderBeachfront,
 	"brightroll":        BidderBrightroll,
@@ -97,6 +100,7 @@ var BidderMap = map[string]BidderName{
 	"kubient":           BidderKubient,
 	"lifestreet":        BidderLifestreet,
 	"lockerdome":        BidderLockerDome,
+	"marsmedia":         BidderMarsmedia,
 	"mgid":              BidderMgid,
 	"openx":             BidderOpenx,
 	"pubmatic":          BidderPubmatic,
diff --git a/openrtb_ext/imp_applogy.go b/openrtb_ext/imp_applogy.go
new file mode 100644
index 00000000..45774a05
--- /dev/null
+++ b/openrtb_ext/imp_applogy.go
@@ -0,0 +1,5 @@
+package openrtb_ext
+
+type ExtImpApplogy struct {
+	Token string `json:"token"`
+}
diff --git a/openrtb_ext/imp_marsmedia.go b/openrtb_ext/imp_marsmedia.go
new file mode 100644
index 00000000..39bc433d
--- /dev/null
+++ b/openrtb_ext/imp_marsmedia.go
@@ -0,0 +1,6 @@
+package openrtb_ext
+
+// ExtImpMarsmedia defines the contract for bidrequest.imp[i].ext.marsmedia
+type ExtImpMarsmedia struct {
+	ZoneID string `json:"zone"`
+}
diff --git a/privacy/ccpa/ccpa.go b/privacy/ccpa/ccpa.go
deleted file mode 100644
index ba6ac611..00000000
--- a/privacy/ccpa/ccpa.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package ccpa
-
-import (
-	"encoding/json"
-
-	"github.com/buger/jsonparser"
-	"github.com/mxmCherry/openrtb"
-)
-
-// Policy represents the CCPA regulation for an OpenRTB bid request.
-type Policy struct {
-	Signal string
-}
-
-// Write mutates an OpenRTB bid request with the context of the CCPA policy.
-func (p Policy) Write(req *openrtb.BidRequest) error {
-	if p.Signal == "" {
-		return nil
-	}
-
-	if req.Regs == nil {
-		req.Regs = &openrtb.Regs{}
-	}
-
-	if req.Regs.Ext == nil {
-		req.Regs.Ext = json.RawMessage(`{"us_privacy":"` + p.Signal + `"}`)
-		return nil
-	}
-
-	var err error
-	req.Regs.Ext, err = jsonparser.Set(req.Regs.Ext, []byte(`"`+p.Signal+`"`), "us_privacy")
-	return err
-}
diff --git a/privacy/ccpa/policy.go b/privacy/ccpa/policy.go
new file mode 100644
index 00000000..e34a3571
--- /dev/null
+++ b/privacy/ccpa/policy.go
@@ -0,0 +1,93 @@
+package ccpa
+
+import (
+	"encoding/json"
+	"errors"
+
+	"github.com/buger/jsonparser"
+	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+// Policy represents the CCPA regulation for an OpenRTB bid request.
+type Policy struct {
+	Value string
+}
+
+// ReadPolicy extracts the CCPA regulation policy from an OpenRTB regs ext.
+func ReadPolicy(req *openrtb.BidRequest) (Policy, error) {
+	policy := Policy{}
+
+	if req != nil && 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, err
+		}
+		policy.Value = ext.USPrivacy
+	}
+
+	return policy, nil
+}
+
+// Write mutates an OpenRTB bid request with the context of the CCPA policy.
+func (p Policy) Write(req *openrtb.BidRequest) error {
+	if p.Value == "" {
+		return nil
+	}
+
+	if req.Regs == nil {
+		req.Regs = &openrtb.Regs{}
+	}
+
+	if req.Regs.Ext == nil {
+		req.Regs.Ext = json.RawMessage(`{"us_privacy":"` + p.Value + `"}`)
+		return nil
+	}
+
+	var err error
+	req.Regs.Ext, err = jsonparser.Set(req.Regs.Ext, []byte(`"`+p.Value+`"`), "us_privacy")
+	return err
+}
+
+// Validate returns an error if the CCPA regulation value does not adhere to the IAB spec.
+func (p Policy) Validate() error {
+	if p.Value == "" {
+		return nil
+	}
+
+	if len(p.Value) != 4 {
+		return errors.New("request.regs.ext.us_privacy must contain 4 characters")
+	}
+
+	if p.Value[0] != '1' {
+		return errors.New("request.regs.ext.us_privacy must specify version 1")
+	}
+
+	var c byte
+
+	c = p.Value[1]
+	if c != 'N' && c != 'Y' && c != '-' {
+		return errors.New("request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the explicit notice")
+	}
+
+	c = p.Value[2]
+	if c != 'N' && c != 'Y' && c != '-' {
+		return errors.New("request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the opt-out sale")
+	}
+
+	c = p.Value[3]
+	if c != 'N' && c != 'Y' && c != '-' {
+		return errors.New("request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the limited service provider agreement")
+	}
+
+	return nil
+}
+
+// ShouldEnforce returns true when the opt-out signal is explicitly detected.
+func (p Policy) ShouldEnforce() bool {
+	if err := p.Validate(); err != nil {
+		return false
+	}
+
+	return p.Value != "" && p.Value[2] == 'Y'
+}
diff --git a/privacy/enforcement.go b/privacy/enforcement.go
new file mode 100644
index 00000000..caea396c
--- /dev/null
+++ b/privacy/enforcement.go
@@ -0,0 +1,76 @@
+package privacy
+
+import (
+	"github.com/mxmCherry/openrtb"
+)
+
+// Enforcement represents the privacy policies to enforce for an OpenRTB bid request.
+type Enforcement struct {
+	CCPA  bool
+	COPPA bool
+	GDPR  bool
+}
+
+// Any returns true if at least one privacy policy requires enforcement.
+func (e Enforcement) Any() bool {
+	return e.CCPA || e.COPPA || e.GDPR
+}
+
+// Apply cleans personally identifiable information from an OpenRTB bid request.
+func (e Enforcement) Apply(bidRequest *openrtb.BidRequest, isAMP bool) {
+	e.apply(bidRequest, isAMP, NewScrubber())
+}
+
+func (e Enforcement) apply(bidRequest *openrtb.BidRequest, isAMP bool, scrubber Scrubber) {
+	if bidRequest != nil && e.Any() {
+		bidRequest.Device = scrubber.ScrubDevice(bidRequest.Device, e.getDeviceMacAndIFA(), e.getIPv6ScrubStrategy(), e.getGeoScrubStrategy())
+		bidRequest.User = scrubber.ScrubUser(bidRequest.User, e.getUserScrubStrategy(isAMP), e.getGeoScrubStrategy())
+	}
+}
+
+func (e Enforcement) getDeviceMacAndIFA() bool {
+	return e.COPPA
+}
+
+func (e Enforcement) getIPv6ScrubStrategy() ScrubStrategyIPV6 {
+	if e.COPPA {
+		return ScrubStrategyIPV6Lowest32
+	}
+
+	if e.GDPR || e.CCPA {
+		return ScrubStrategyIPV6Lowest16
+	}
+
+	return ScrubStrategyIPV6None
+}
+
+func (e Enforcement) getGeoScrubStrategy() ScrubStrategyGeo {
+	if e.COPPA {
+		return ScrubStrategyGeoFull
+	}
+
+	if e.GDPR || e.CCPA {
+		return ScrubStrategyGeoReducedPrecision
+	}
+
+	return ScrubStrategyGeoNone
+}
+
+func (e Enforcement) getUserScrubStrategy(isAMP bool) ScrubStrategyUser {
+	if e.COPPA {
+		return ScrubStrategyUserFull
+	}
+
+	// There's no way for AMP to send a GDPR consent string yet so it's hard
+	// to know if the vendor is consented or not and therefore for AMP requests
+	// we keep the BuyerUID as is for GDPR.
+	if e.GDPR && isAMP {
+		return ScrubStrategyUserNone
+	}
+
+	if e.GDPR || e.CCPA {
+		return ScrubStrategyUserBuyerIDOnly
+	}
+
+	return ScrubStrategyUserNone
+}
diff --git a/privacy/gdpr/gdpr.go b/privacy/gdpr/policy.go
similarity index 100%
rename from privacy/gdpr/gdpr.go
rename to privacy/gdpr/policy.go
diff --git a/privacy/gdpr/gdpr_test.go b/privacy/gdpr/policy_test.go
similarity index 100%
rename from privacy/gdpr/gdpr_test.go
rename to privacy/gdpr/policy_test.go
diff --git a/privacy/scrubber.go b/privacy/scrubber.go
new file mode 100644
index 00000000..916b660d
--- /dev/null
+++ b/privacy/scrubber.go
@@ -0,0 +1,190 @@
+package privacy
+
+import (
+	"strings"
+
+	"github.com/mxmCherry/openrtb"
+)
+
+// ScrubStrategyIPV6 defines the approach to scrub PII from an IPV6 address.
+type ScrubStrategyIPV6 int
+
+const (
+	// ScrubStrategyIPV6None does not remove any part of an IPV6 address.
+	ScrubStrategyIPV6None ScrubStrategyIPV6 = iota
+
+	// ScrubStrategyIPV6Lowest16 zeroes out the last 16 bits of an IPV6 address.
+	ScrubStrategyIPV6Lowest16
+
+	// ScrubStrategyIPV6Lowest32 zeroes out the last 32 bits of an IPV6 address.
+	ScrubStrategyIPV6Lowest32
+)
+
+// ScrubStrategyGeo defines the approach to scrub PII from geographical data.
+type ScrubStrategyGeo int
+
+const (
+	// ScrubStrategyGeoNone does not remove any geographical data.
+	ScrubStrategyGeoNone ScrubStrategyGeo = iota
+
+	// ScrubStrategyGeoFull removes all geographical data.
+	ScrubStrategyGeoFull
+
+	// ScrubStrategyGeoReducedPrecision anonymizes geographical data with rounding.
+	ScrubStrategyGeoReducedPrecision
+)
+
+// ScrubStrategyUser defines the approach to scrub PII from user data.
+type ScrubStrategyUser int
+
+const (
+	// ScrubStrategyUserNone does not remove user data.
+	ScrubStrategyUserNone ScrubStrategyUser = iota
+
+	// ScrubStrategyUserFull removes the user's buyer id, exchange id year of birth, and gender.
+	ScrubStrategyUserFull
+
+	// ScrubStrategyUserBuyerIDOnly removes the user's buyer id.
+	ScrubStrategyUserBuyerIDOnly
+)
+
+// Scrubber removes PII from parts of an OpenRTB request.
+type Scrubber interface {
+	ScrubDevice(device *openrtb.Device, macAndIFA bool, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb.Device
+	ScrubUser(user *openrtb.User, strategy ScrubStrategyUser, geo ScrubStrategyGeo) *openrtb.User
+}
+
+type scrubber struct{}
+
+// NewScrubber returns an OpenRTB scrubber.
+func NewScrubber() Scrubber {
+	return scrubber{}
+}
+
+func (scrubber) ScrubDevice(device *openrtb.Device, macAndIFA bool, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb.Device {
+	if device == nil {
+		return nil
+	}
+
+	deviceCopy := *device
+
+	deviceCopy.DIDMD5 = ""
+	deviceCopy.DIDSHA1 = ""
+	deviceCopy.DPIDMD5 = ""
+	deviceCopy.DPIDSHA1 = ""
+	deviceCopy.IP = scrubIPV4(device.IP)
+
+	if macAndIFA {
+		deviceCopy.MACSHA1 = ""
+		deviceCopy.MACMD5 = ""
+		deviceCopy.IFA = ""
+	}
+
+	switch ipv6 {
+	case ScrubStrategyIPV6Lowest16:
+		deviceCopy.IPv6 = scrubIPV6Lowest16Bits(device.IPv6)
+	case ScrubStrategyIPV6Lowest32:
+		deviceCopy.IPv6 = scrubIPV6Lowest32Bits(device.IPv6)
+	}
+
+	switch geo {
+	case ScrubStrategyGeoFull:
+		deviceCopy.Geo = scrubGeoFull(device.Geo)
+	case ScrubStrategyGeoReducedPrecision:
+		deviceCopy.Geo = scrubGeoPrecision(device.Geo)
+	}
+
+	return &deviceCopy
+}
+
+func (scrubber) ScrubUser(user *openrtb.User, strategy ScrubStrategyUser, geo ScrubStrategyGeo) *openrtb.User {
+	if user == nil {
+		return nil
+	}
+
+	userCopy := *user
+
+	switch strategy {
+	case ScrubStrategyUserFull:
+		userCopy.BuyerUID = ""
+		userCopy.ID = ""
+		userCopy.Yob = 0
+		userCopy.Gender = ""
+	case ScrubStrategyUserBuyerIDOnly:
+		userCopy.BuyerUID = ""
+	}
+
+	switch geo {
+	case ScrubStrategyGeoFull:
+		userCopy.Geo = scrubGeoFull(user.Geo)
+	case ScrubStrategyGeoReducedPrecision:
+		userCopy.Geo = scrubGeoPrecision(user.Geo)
+	}
+
+	return &userCopy
+}
+
+func scrubIPV4(ip string) string {
+	i := strings.LastIndex(ip, ".")
+	if i == -1 {
+		return ""
+	}
+
+	return ip[0:i] + ".0"
+}
+
+func scrubIPV6Lowest16Bits(ip string) string {
+	ip = removeLowestIPV6Segment(ip)
+
+	if ip != "" {
+		ip += ":0"
+	}
+
+	return ip
+}
+
+func scrubIPV6Lowest32Bits(ip string) string {
+	ip = removeLowestIPV6Segment(ip)
+	ip = removeLowestIPV6Segment(ip)
+
+	if ip != "" {
+		ip += ":0:0"
+	}
+
+	return ip
+}
+
+func removeLowestIPV6Segment(ip string) string {
+	i := strings.LastIndex(ip, ":")
+
+	if i == -1 {
+		return ""
+	}
+
+	return ip[0:i]
+}
+
+func scrubGeoFull(geo *openrtb.Geo) *openrtb.Geo {
+	if geo == nil {
+		return nil
+	}
+
+	geoCopy := *geo
+	geoCopy.Lat = 0
+	geoCopy.Lon = 0
+	geoCopy.Metro = ""
+	geoCopy.City = ""
+	geoCopy.ZIP = ""
+	return &geoCopy
+}
+
+func scrubGeoPrecision(geo *openrtb.Geo) *openrtb.Geo {
+	if geo == nil {
+		return nil
+	}
+
+	geoCopy := *geo
+	geoCopy.Lat = float64(int(geo.Lat*100.0+0.5)) / 100.0 // Round Latitude
+	geoCopy.Lon = float64(int(geo.Lon*100.0+0.5)) / 100.0 // Round Longitude
+	return &geoCopy
+}
diff --git a/static/bidder-info/applogy.yaml b/static/bidder-info/applogy.yaml
new file mode 100644
index 00000000..bb908c94
--- /dev/null
+++ b/static/bidder-info/applogy.yaml
@@ -0,0 +1,13 @@
+maintainer:
+  email: work@applogy.com
+capabilities:
+  app:
+    mediaTypes:
+      - banner
+      - video
+      - native
+  site:
+    mediaTypes:
+      - banner
+      - video
+      - native
diff --git a/static/bidder-info/marsmedia.yaml b/static/bidder-info/marsmedia.yaml
new file mode 100644
index 00000000..e0267205
--- /dev/null
+++ b/static/bidder-info/marsmedia.yaml
@@ -0,0 +1,11 @@
+maintainer:
+  email: "prebid@mars.media"
+capabilities:
+  app:
+    mediaTypes:
+      - banner
+      - video
+  site:
+    mediaTypes:
+      - banner
+      - video
diff --git a/static/bidder-params/applogy.json b/static/bidder-params/applogy.json
new file mode 100644
index 00000000..2650640c
--- /dev/null
+++ b/static/bidder-params/applogy.json
@@ -0,0 +1,13 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "Applogy Adapter Params",
+  "description": "A schema which validates params accepted by the Applogy adapter",
+  "type": "object",
+  "properties": {
+    "token": {
+      "type": "string",
+      "description": "Applogy token"
+    }
+  },
+  "required": ["token"]
+}
diff --git a/static/bidder-params/marsmedia.json b/static/bidder-params/marsmedia.json
new file mode 100644
index 00000000..208a42e8
--- /dev/null
+++ b/static/bidder-params/marsmedia.json
@@ -0,0 +1,13 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "Marsmedia Adapter Params",
+  "description": "A schema which validates params accepted by the Marsmedia adapter",
+  "type": "object",
+  "properties": {
+    "zone": {
+      "type": "string",
+      "description": "Zone ID to use."
+    }
+  },
+  "required": ["zone"]
+}
diff --git a/static/bidder-params/triplelift.json b/static/bidder-params/triplelift.json
index 9f130459..1983ae4f 100644
--- a/static/bidder-params/triplelift.json
+++ b/static/bidder-params/triplelift.json
@@ -11,11 +11,5 @@
     },
     "floor" :  {"description" : "the bid floor", "type": "number" }
   },
-  "oneOf": [{
-    "oneOf": [{
-      "required": ["inventoryCode"]
-    }] }],
-  "not": {
-    "required": ["floor"]
-  }
+  "required": ["inventoryCode"]
 }
diff --git a/static/bidder-params/triplelift_native.json b/static/bidder-params/triplelift_native.json
index c81230aa..4fd71c72 100644
--- a/static/bidder-params/triplelift_native.json
+++ b/static/bidder-params/triplelift_native.json
@@ -11,11 +11,5 @@
     },
     "floor" :  {"description" : "the bid floor, in usd", "type": "number" }
   },
-  "oneOf": [{
-    "oneOf": [{
-      "required": ["inventoryCode"]
-    }] }],
-  "not": {
-    "required": ["floor"]
-  }
+  "required": ["inventoryCode"]
 }
diff --git a/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go
index 2db827cb..62779932 100644
--- a/usersync/usersyncers/syncer.go
+++ b/usersync/usersyncers/syncer.go
@@ -1,10 +1,11 @@
 package usersyncers
 
 import (
-	"github.com/prebid/prebid-server/adapters/adpone"
 	"strings"
 	"text/template"
 
+	"github.com/prebid/prebid-server/adapters/adpone"
+
 	"github.com/golang/glog"
 	ttx "github.com/prebid/prebid-server/adapters/33across"
 	"github.com/prebid/prebid-server/adapters/adform"
@@ -30,6 +31,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/ix"
 	"github.com/prebid/prebid-server/adapters/lifestreet"
 	"github.com/prebid/prebid-server/adapters/lockerdome"
+	"github.com/prebid/prebid-server/adapters/marsmedia"
 	"github.com/prebid/prebid-server/adapters/mgid"
 	"github.com/prebid/prebid-server/adapters/openx"
 	"github.com/prebid/prebid-server/adapters/pubmatic"
@@ -85,6 +87,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderIx, ix.NewIxSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderLifestreet, lifestreet.NewLifestreetSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderLockerDome, lockerdome.NewLockerDomeSyncer)
+	insertIntoMap(cfg, syncers, openrtb_ext.BidderMarsmedia, marsmedia.NewMarsmediaSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderMgid, mgid.NewMgidSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderOpenx, openx.NewOpenxSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderPubmatic, pubmatic.NewPubmaticSyncer)


Test patch:
diff --git a/adapters/adform/adform_test.go b/adapters/adform/adform_test.go
index e59bd951..63646f5f 100644
--- a/adapters/adform/adform_test.go
+++ b/adapters/adform/adform_test.go
@@ -21,6 +21,8 @@ import (
 	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/openrtb_ext"
+
+	"github.com/stretchr/testify/assert"
 )
 
 func TestJsonSamples(t *testing.T) {
@@ -597,3 +599,77 @@ func TestPriceTypeUrlParameterCreation(t *testing.T) {
 		}
 	}
 }
+
+// Asserts that toOpenRtbBidResponse() creates a *adapters.BidderResponse with
+// the currency of the last valid []*adformBid element and the expected number of bids
+func TestToOpenRtbBidResponse(t *testing.T) {
+	expectedBids := 3
+	lastCurrency, anotherCurrency, emptyCurrency := "EUR", "USD", ""
+
+	request := &openrtb.BidRequest{
+		ID: "test-request-id",
+		Imp: []openrtb.Imp{
+			{
+				ID:     "banner-imp-no1",
+				Ext:    json.RawMessage(`{"bidder1": { "mid": "32341" }}`),
+				Banner: &openrtb.Banner{},
+			},
+			{
+				ID:     "banner-imp-no2",
+				Ext:    json.RawMessage(`{"bidder1": { "mid": "32342" }}`),
+				Banner: &openrtb.Banner{},
+			},
+			{
+				ID:     "banner-imp-no3",
+				Ext:    json.RawMessage(`{"bidder1": { "mid": "32343" }}`),
+				Banner: &openrtb.Banner{},
+			},
+			{
+				ID:     "banner-imp-no4",
+				Ext:    json.RawMessage(`{"bidder1": { "mid": "32344" }}`),
+				Banner: &openrtb.Banner{},
+			},
+		},
+		Device: &openrtb.Device{UA: "ua", IP: "ip"},
+		User:   &openrtb.User{BuyerUID: "buyerUID"},
+	}
+
+	testAdformBids := []*adformBid{
+		{
+			ResponseType: "banner",
+			Banner:       "banner-content1",
+			Price:        1.23,
+			Currency:     anotherCurrency,
+			Width:        300,
+			Height:       200,
+			DealId:       "dealId1",
+			CreativeId:   "creativeId1",
+		},
+		{},
+		{
+			ResponseType: "banner",
+			Banner:       "banner-content3",
+			Price:        1.24,
+			Currency:     emptyCurrency,
+			Width:        300,
+			Height:       200,
+			DealId:       "dealId3",
+			CreativeId:   "creativeId3",
+		},
+		{
+			ResponseType: "banner",
+			Banner:       "banner-content4",
+			Price:        1.25,
+			Currency:     lastCurrency,
+			Width:        300,
+			Height:       200,
+			DealId:       "dealId4",
+			CreativeId:   "creativeId4",
+		},
+	}
+
+	actualBidResponse := toOpenRtbBidResponse(testAdformBids, request)
+
+	assert.Equalf(t, expectedBids, len(actualBidResponse.Bids), "bid count")
+	assert.Equalf(t, lastCurrency, actualBidResponse.Currency, "currency")
+}
diff --git a/adapters/adtelligent/adtelligenttest/supplemental/audio.json b/adapters/adtelligent/adtelligenttest/supplemental/audio.json
index aa9a40bc..d8904251 100644
--- a/adapters/adtelligent/adtelligenttest/supplemental/audio.json
+++ b/adapters/adtelligent/adtelligenttest/supplemental/audio.json
@@ -9,7 +9,7 @@
         },
         "ext": {
           "bidder": {
-            "placementId": 10433394
+            "placementId": 1
           }
         }
       }
diff --git a/adapters/adtelligent/adtelligenttest/supplemental/native.json b/adapters/adtelligent/adtelligenttest/supplemental/native.json
index 90ddff2f..00fb3984 100644
--- a/adapters/adtelligent/adtelligenttest/supplemental/native.json
+++ b/adapters/adtelligent/adtelligenttest/supplemental/native.json
@@ -9,7 +9,7 @@
         },
         "ext": {
           "bidder": {
-            "placementId": 10433394
+            "placementId": 1
           }
         }
       }
diff --git a/adapters/applogy/applogy_test.go b/adapters/applogy/applogy_test.go
new file mode 100644
index 00000000..8883db57
--- /dev/null
+++ b/adapters/applogy/applogy_test.go
@@ -0,0 +1,11 @@
+package applogy
+
+import (
+	"testing"
+
+	"github.com/prebid/prebid-server/adapters/adapterstest"
+)
+
+func TestJsonSamples(t *testing.T) {
+	adapterstest.RunJSONBidderTest(t, "applogytest", NewApplogyBidder("http://example.com/prebid"))
+}
diff --git a/adapters/applogy/applogytest/exemplary/simple-banner.json b/adapters/applogy/applogytest/exemplary/simple-banner.json
new file mode 100644
index 00000000..a3926dea
--- /dev/null
+++ b/adapters/applogy/applogytest/exemplary/simple-banner.json
@@ -0,0 +1,170 @@
+{
+	"mockBidRequest": {
+		"id": "test-request-id",
+		"imp": [{
+			"id": "test-impression-id-1",
+			"banner": {
+				"format": [{
+					"h": 250,
+					"w": 300
+				}]
+			},
+			"ext": {
+				"bidder": {
+					"token": "test-token-1"
+				}
+			}
+		}, {
+			"id": "test-impression-id-2",
+			"banner": {
+				"w": 300,
+				"h": 250
+			},
+			"ext": {
+				"bidder": {
+					"token": "test-token-1"
+				}
+			}
+		}, {
+			"id": "test-impression-id-3",
+			"banner": {
+				"w": 300,
+				"h": 250
+			},
+			"ext": {
+				"bidder": {
+					"token": "test-token-2"
+				}
+			}
+		}]
+	},
+	"httpCalls": [{
+		"expectedRequest": {
+			"uri": "http://example.com/prebid/test-token-1",
+			"body": {
+				"id": "test-request-id",
+				"imp": [{
+					"id": "test-impression-id-1",
+					"banner": {
+						"format": [{
+							"h": 250,
+							"w": 300
+						}],
+						"h": 250,
+						"w": 300
+					},
+					"ext": {
+						"bidder": {
+							"token": "test-token-1"
+						}
+					}
+				}]
+			}
+		},
+		"mockResponse": {
+			"status": 200,
+			"body": {
+				"id": "test-response-id",
+				"seatbid": [{
+					"bid": [{
+						"id": "test-bid-id-1",
+						"impid": "test-impression-id-1",
+						"price": 1
+					}]
+				}]
+			}
+		}
+	}, {
+		"expectedRequest": {
+			"uri": "http://example.com/prebid/test-token-1",
+			"body": {
+				"id": "test-request-id",
+				"imp": [{
+					"id": "test-impression-id-2",
+					"banner": {
+						"h": 250,
+						"w": 300
+					},
+					"ext": {
+						"bidder": {
+							"token": "test-token-1"
+						}
+					}
+				}]
+			}
+		},
+		"mockResponse": {
+			"status": 200,
+			"body": {
+				"id": "test-response-id-2",
+				"seatbid": [{
+					"bid": [{
+						"id": "test-bid-id-2",
+						"impid": "test-impression-id-2",
+						"price": 2
+					}]
+				}]
+			}
+		}
+	}, {
+		"expectedRequest": {
+			"uri": "http://example.com/prebid/test-token-2",
+			"body": {
+				"id": "test-request-id",
+				"imp": [{
+					"id": "test-impression-id-3",
+					"banner": {
+						"h": 250,
+						"w": 300
+					},
+					"ext": {
+						"bidder": {
+							"token": "test-token-2"
+						}
+					}
+				}]
+			}
+		},
+		"mockResponse": {
+			"status": 200,
+			"body": {
+				"id": "test-response-id-3",
+				"seatbid": [{
+					"bid": [{
+						"id": "test-bid-id-3",
+						"impid": "test-impression-id-3",
+						"price": 3
+					}]
+				}]
+			}
+		}
+	}],
+	"expectedBidResponses": [{
+		"bids": [{
+			"bid": {
+				"id": "test-bid-id-1",
+				"impid": "test-impression-id-1",
+				"price": 1
+			},
+			"type": "banner"
+		}]
+	}, {
+		"bids": [{
+			"bid": {
+				"id": "test-bid-id-2",
+				"impid": "test-impression-id-2",
+				"price": 2
+			},
+			"type": "banner"
+		}]
+	}, {
+		"bids": [{
+			"bid": {
+				"id": "test-bid-id-3",
+				"impid": "test-impression-id-3",
+				"price": 3
+			},
+			"type": "banner"
+		}]
+	}]
+}
diff --git a/adapters/applogy/applogytest/exemplary/simple-native.json b/adapters/applogy/applogytest/exemplary/simple-native.json
new file mode 100644
index 00000000..84565ec5
--- /dev/null
+++ b/adapters/applogy/applogytest/exemplary/simple-native.json
@@ -0,0 +1,164 @@
+{
+	"mockBidRequest": {
+		"id": "test-request-id",
+		"imp": [{
+			"id": "test-impression-id-1",
+			"native": {
+				"request": "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":500}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":1,\"hmin\":1}},{\"id\":3,\"required\":0,\"data\":{\"type\":1,\"len\":200}},{\"id\":4,\"required\":0,\"data\":{\"type\":2,\"len\":15000}},{\"id\":5,\"required\":0,\"data\":{\"type\":6,\"len\":40}},{\"id\":6,\"required\":0,\"data\":{\"type\":500}}]}",
+				"ver": "1.1"
+			},
+			"ext": {
+				"bidder": {
+					"token": "test-token-1"
+				}
+			}
+		}, {
+			"id": "test-impression-id-2",
+			"native": {
+				"request": "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":500}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":1,\"hmin\":1}},{\"id\":3,\"required\":0,\"data\":{\"type\":1,\"len\":200}},{\"id\":4,\"required\":0,\"data\":{\"type\":2,\"len\":15000}},{\"id\":5,\"required\":0,\"data\":{\"type\":6,\"len\":40}},{\"id\":6,\"required\":0,\"data\":{\"type\":500}}]}",
+				"ver": "1.1"
+			},
+			"ext": {
+				"bidder": {
+					"token": "test-token-1"
+				}
+			}
+		}, {
+			"id": "test-impression-id-3",
+			"native": {
+				"request": "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":500}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":1,\"hmin\":1}},{\"id\":3,\"required\":0,\"data\":{\"type\":1,\"len\":200}},{\"id\":4,\"required\":0,\"data\":{\"type\":2,\"len\":15000}},{\"id\":5,\"required\":0,\"data\":{\"type\":6,\"len\":40}},{\"id\":6,\"required\":0,\"data\":{\"type\":500}}]}",
+				"ver": "1.1"
+			},
+			"ext": {
+				"bidder": {
+					"token": "test-token-2"
+				}
+			}
+		}]
+	},
+	"httpCalls": [{
+		"expectedRequest": {
+			"uri": "http://example.com/prebid/test-token-1",
+			"body": {
+				"id": "test-request-id",
+				"imp": [{
+					"id": "test-impression-id-1",
+					"native": {
+						"request": "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":500}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":1,\"hmin\":1}},{\"id\":3,\"required\":0,\"data\":{\"type\":1,\"len\":200}},{\"id\":4,\"required\":0,\"data\":{\"type\":2,\"len\":15000}},{\"id\":5,\"required\":0,\"data\":{\"type\":6,\"len\":40}},{\"id\":6,\"required\":0,\"data\":{\"type\":500}}]}",
+						"ver": "1.1"
+					},
+					"ext": {
+						"bidder": {
+							"token": "test-token-1"
+						}
+					}
+				}]
+			}
+		},
+		"mockResponse": {
+			"status": 200,
+			"body": {
+				"id": "test-response-id",
+				"seatbid": [{
+					"bid": [{
+						"id": "test-bid-id-1",
+						"impid": "test-impression-id-1",
+						"price": 1
+					}]
+				}]
+			}
+		}
+	}, {
+		"expectedRequest": {
+			"uri": "http://example.com/prebid/test-token-1",
+			"body": {
+				"id": "test-request-id",
+				"imp": [{
+					"id": "test-impression-id-2",
+					"native": {
+						"request": "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":500}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":1,\"hmin\":1}},{\"id\":3,\"required\":0,\"data\":{\"type\":1,\"len\":200}},{\"id\":4,\"required\":0,\"data\":{\"type\":2,\"len\":15000}},{\"id\":5,\"required\":0,\"data\":{\"type\":6,\"len\":40}},{\"id\":6,\"required\":0,\"data\":{\"type\":500}}]}",
+						"ver": "1.1"
+					},
+					"ext": {
+						"bidder": {
+							"token": "test-token-1"
+						}
+					}
+				}]
+			}
+		},
+		"mockResponse": {
+			"status": 200,
+			"body": {
+				"id": "test-response-id-2",
+				"seatbid": [{
+					"bid": [{
+						"id": "test-bid-id-2",
+						"impid": "test-impression-id-2",
+						"price": 2
+					}]
+				}]
+			}
+		}
+	}, {
+		"expectedRequest": {
+			"uri": "http://example.com/prebid/test-token-2",
+			"body": {
+				"id": "test-request-id",
+				"imp": [{
+					"id": "test-impression-id-3",
+					"native": {
+						"request": "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":500}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":1,\"hmin\":1}},{\"id\":3,\"required\":0,\"data\":{\"type\":1,\"len\":200}},{\"id\":4,\"required\":0,\"data\":{\"type\":2,\"len\":15000}},{\"id\":5,\"required\":0,\"data\":{\"type\":6,\"len\":40}},{\"id\":6,\"required\":0,\"data\":{\"type\":500}}]}",
+						"ver": "1.1"
+					},
+					"ext": {
+						"bidder": {
+							"token": "test-token-2"
+						}
+					}
+				}]
+			}
+		},
+		"mockResponse": {
+			"status": 200,
+			"body": {
+				"id": "test-response-id-3",
+				"seatbid": [{
+					"bid": [{
+						"id": "test-bid-id-3",
+						"impid": "test-impression-id-3",
+						"price": 3
+					}]
+				}]
+			}
+		}
+	}],
+	"expectedBidResponses": [{
+		"bids": [{
+			"bid": {
+				"id": "test-bid-id-1",
+				"impid": "test-impression-id-1",
+				"price": 1
+			},
+			"type": "native"
+		}]
+	}, {
+		"bids": [{
+			"bid": {
+				"id": "test-bid-id-2",
+				"impid": "test-impression-id-2",
+				"price": 2
+			},
+			"type": "native"
+		}]
+	}, {
+		"bids": [{
+			"bid": {
+				"id": "test-bid-id-3",
+				"impid": "test-impression-id-3",
+				"price": 3
+			},
+			"type": "native"
+		}]
+	}]
+}
diff --git a/adapters/applogy/applogytest/exemplary/simple-video.json b/adapters/applogy/applogytest/exemplary/simple-video.json
new file mode 100644
index 00000000..30237ccc
--- /dev/null
+++ b/adapters/applogy/applogytest/exemplary/simple-video.json
@@ -0,0 +1,182 @@
+{
+	"mockBidRequest": {
+		"id": "test-request-id",
+		"imp": [{
+			"id": "test-impression-id-1",
+			"video": {
+				"w": 900,
+				"h": 250,
+				"mimes": [
+					"video/mp4"
+				]
+			},
+			"ext": {
+				"bidder": {
+					"token": "test-token-1"
+				}
+			}
+		}, {
+			"id": "test-impression-id-2",
+			"video": {
+				"w": 900,
+				"h": 250,
+				"mimes": [
+					"video/mp4"
+				]
+			},
+			"ext": {
+				"bidder": {
+					"token": "test-token-1"
+				}
+			}
+		}, {
+			"id": "test-impression-id-3",
+			"video": {
+				"w": 900,
+				"h": 250,
+				"mimes": [
+					"video/mp4"
+				]
+			},
+			"ext": {
+				"bidder": {
+					"token": "test-token-2"
+				}
+			}
+		}]
+	},
+	"httpCalls": [{
+		"expectedRequest": {
+			"uri": "http://example.com/prebid/test-token-1",
+			"body": {
+				"id": "test-request-id",
+				"imp": [{
+					"id": "test-impression-id-1",
+					"video": {
+						"w": 900,
+						"h": 250,
+						"mimes": [
+							"video/mp4"
+						]
+					},
+					"ext": {
+						"bidder": {
+							"token": "test-token-1"
+						}
+					}
+				}]
+			}
+		},
+		"mockResponse": {
+			"status": 200,
+			"body": {
+				"id": "test-response-id",
+				"seatbid": [{
+					"bid": [{
+						"id": "test-bid-id-1",
+						"impid": "test-impression-id-1",
+						"price": 1
+					}]
+				}]
+			}
+		}
+	}, {
+		"expectedRequest": {
+			"uri": "http://example.com/prebid/test-token-1",
+			"body": {
+				"id": "test-request-id",
+				"imp": [{
+					"id": "test-impression-id-2",
+					"video": {
+						"w": 900,
+						"h": 250,
+						"mimes": [
+							"video/mp4"
+						]
+					},
+					"ext": {
+						"bidder": {
+							"token": "test-token-1"
+						}
+					}
+				}]
+			}
+		},
+		"mockResponse": {
+			"status": 200,
+			"body": {
+				"id": "test-response-id-2",
+				"seatbid": [{
+					"bid": [{
+						"id": "test-bid-id-2",
+						"impid": "test-impression-id-2",
+						"price": 2
+					}]
+				}]
+			}
+		}
+	}, {
+		"expectedRequest": {
+			"uri": "http://example.com/prebid/test-token-2",
+			"body": {
+				"id": "test-request-id",
+				"imp": [{
+					"id": "test-impression-id-3",
+					"video": {
+						"w": 900,
+						"h": 250,
+						"mimes": [
+							"video/mp4"
+						]
+					},
+					"ext": {
+						"bidder": {
+							"token": "test-token-2"
+						}
+					}
+				}]
+			}
+		},
+		"mockResponse": {
+			"status": 200,
+			"body": {
+				"id": "test-response-id-3",
+				"seatbid": [{
+					"bid": [{
+						"id": "test-bid-id-3",
+						"impid": "test-impression-id-3",
+						"price": 3
+					}]
+				}]
+			}
+		}
+	}],
+	"expectedBidResponses": [{
+		"bids": [{
+			"bid": {
+				"id": "test-bid-id-1",
+				"impid": "test-impression-id-1",
+				"price": 1
+			},
+			"type": "video"
+		}]
+	}, {
+		"bids": [{
+			"bid": {
+				"id": "test-bid-id-2",
+				"impid": "test-impression-id-2",
+				"price": 2
+			},
+			"type": "video"
+		}]
+	}, {
+		"bids": [{
+			"bid": {
+				"id": "test-bid-id-3",
+				"impid": "test-impression-id-3",
+				"price": 3
+			},
+			"type": "video"
+		}]
+	}]
+}
diff --git a/adapters/applogy/applogytest/supplemental/all-failed.json b/adapters/applogy/applogytest/supplemental/all-failed.json
new file mode 100644
index 00000000..7f0244af
--- /dev/null
+++ b/adapters/applogy/applogytest/supplemental/all-failed.json
@@ -0,0 +1,16 @@
+{
+	"mockBidRequest": {
+		"id": "test-request-id",
+		"imp": [{
+			"id": "test-impression-id-1",
+			"banner": {
+				"w": 300,
+				"h": 250
+			}
+		}]
+	},
+	"expectedMakeRequestsErrors": [{
+		"value": "impression extensions required",
+		"comparison": "literal"
+	}]
+}
diff --git a/adapters/applogy/applogytest/supplemental/invalid-params.json b/adapters/applogy/applogytest/supplemental/invalid-params.json
new file mode 100644
index 00000000..6b5d5e32
--- /dev/null
+++ b/adapters/applogy/applogytest/supplemental/invalid-params.json
@@ -0,0 +1,133 @@
+{
+	"mockBidRequest": {
+		"id": "test-request-id",
+		"imp": [{
+			"id": "test-impression-id-1",
+			"banner": {}
+		}, {
+			"id": "test-impression-id-2",
+			"banner": {
+				"w": 300,
+				"h": 250
+			}
+		}, {
+			"id": "test-impression-id-3",
+			"banner": {
+				"w": 300,
+				"h": 250
+			},
+			"ext": {}
+		}, {
+			"id": "test-impression-id-4"
+		}, {
+			"id": "test-impression-id-5",
+			"banner": {
+				"w": 300,
+				"h": 250
+			},
+			"ext": {
+				"bidder": {
+					"token": "test-token-5"
+				}
+			}
+		}, {
+			"id": "test-impression-id-0",
+			"banner": {
+				"w": 300,
+				"h": 250
+			},
+			"ext": {
+				"bidder": "invalid bidder"
+			}
+		}, {
+			"id": "test-impression-id-0",
+			"banner": {
+				"w": 300,
+				"h": 250
+			},
+			"ext": {
+				"bidder": {}
+			}
+		}, {
+			"id": "test-impression-id-0",
+			"banner": {
+				"w": 300,
+				"h": 250
+			},
+			"ext": "invalid ext"
+		}]
+	},
+	"httpCalls": [{
+		"expectedRequest": {
+			"uri": "http://example.com/prebid/test-token-5",
+			"body": {
+				"id": "test-request-id",
+				"imp": [{
+					"id": "test-impression-id-5",
+					"banner": {
+						"h": 250,
+						"w": 300
+					},
+					"ext": {
+						"bidder": {
+							"token": "test-token-5"
+						}
+					}
+				}]
+			}
+		},
+		"mockResponse": {
+			"status": 200,
+			"body": {
+				"id": "test-response-id",
+				"seatbid": [{
+					"bid": [{
+						"id": "test-bid-id-5",
+						"impid": "test-impression-id-5",
+						"price": 5
+					}, {
+						"id": "test-bid-id-6",
+						"impid": "test-impression-id-6",
+						"price": 6
+					}]
+				}]
+			}
+		}
+	}],
+	"expectedBidResponses": [{
+		"bids": [{
+			"bid": {
+				"id": "test-bid-id-5",
+				"impid": "test-impression-id-5",
+				"price": 5
+			},
+			"type": "banner"
+		}]
+	}],
+	"expectedMakeRequestsErrors": [{
+		"value": "banner size information missing",
+		"comparison": "literal"
+	}, {
+		"value": "impression extensions required",
+		"comparison": "literal"
+	}, {
+		"value": "bidder required",
+		"comparison": "literal"
+	}, {
+		"value": "Applogy only supports banner, video or native ads",
+		"comparison": "literal"
+	}, {
+		"value": "json: cannot unmarshal string into Go value of type openrtb_ext.ExtImpApplogy",
+		"comparison": "literal"
+	}, {
+		"value": "Applogy token required",
+		"comparison": "literal"
+	}, {
+		"value": "json: cannot unmarshal string into Go value of type adapters.ExtImpBidder",
+		"comparison": "literal"
+	}],
+	"expectedMakeBidsErrors": [{
+		"value": "ignoring bid id=test-bid-id-6, request doesn't contain any valid impression with id=test-impression-id-6",
+		"comparison": "literal"
+	}]
+}
diff --git a/adapters/applogy/applogytest/supplemental/status-204.json b/adapters/applogy/applogytest/supplemental/status-204.json
new file mode 100644
index 00000000..c3516b18
--- /dev/null
+++ b/adapters/applogy/applogytest/supplemental/status-204.json
@@ -0,0 +1,41 @@
+{
+	"mockBidRequest": {
+		"id": "test-request-id",
+		"imp": [{
+			"id": "test-impression-id-1",
+			"banner": {
+				"w": 300,
+				"h": 250
+			},
+			"ext": {
+				"bidder": {
+					"token": "test-token-1"
+				}
+			}
+		}]
+	},
+	"httpCalls": [{
+		"expectedRequest": {
+			"uri": "http://example.com/prebid/test-token-1",
+			"body": {
+				"id": "test-request-id",
+				"imp": [{
+					"id": "test-impression-id-1",
+					"banner": {
+						"h": 250,
+						"w": 300
+					},
+					"ext": {
+						"bidder": {
+							"token": "test-token-1"
+						}
+					}
+				}]
+			}
+		},
+		"mockResponse": {
+			"status": 204,
+			"body": {}
+		}
+	}]
+}
diff --git a/adapters/applogy/applogytest/supplemental/status-400.json b/adapters/applogy/applogytest/supplemental/status-400.json
new file mode 100644
index 00000000..95e271ba
--- /dev/null
+++ b/adapters/applogy/applogytest/supplemental/status-400.json
@@ -0,0 +1,45 @@
+{
+	"mockBidRequest": {
+		"id": "test-request-id",
+		"imp": [{
+			"id": "test-impression-id-1",
+			"banner": {
+				"w": 300,
+				"h": 250
+			},
+			"ext": {
+				"bidder": {
+					"token": "test-token-1"
+				}
+			}
+		}]
+	},
+	"httpCalls": [{
+		"expectedRequest": {
+			"uri": "http://example.com/prebid/test-token-1",
+			"body": {
+				"id": "test-request-id",
+				"imp": [{
+					"id": "test-impression-id-1",
+					"banner": {
+						"h": 250,
+						"w": 300
+					},
+					"ext": {
+						"bidder": {
+							"token": "test-token-1"
+						}
+					}
+				}]
+			}
+		},
+		"mockResponse": {
+			"status": 400,
+			"body": {}
+		}
+	}],
+	"expectedMakeBidsErrors": [{
+		"value": "unexpected status code: 400",
+		"comparison": "literal"
+	}]
+}
diff --git a/adapters/applogy/applogytest/supplemental/status-502.json b/adapters/applogy/applogytest/supplemental/status-502.json
new file mode 100644
index 00000000..c0b16416
--- /dev/null
+++ b/adapters/applogy/applogytest/supplemental/status-502.json
@@ -0,0 +1,45 @@
+{
+	"mockBidRequest": {
+		"id": "test-request-id",
+		"imp": [{
+			"id": "test-impression-id-1",
+			"banner": {
+				"w": 300,
+				"h": 250
+			},
+			"ext": {
+				"bidder": {
+					"token": "test-token-1"
+				}
+			}
+		}]
+	},
+	"httpCalls": [{
+		"expectedRequest": {
+			"uri": "http://example.com/prebid/test-token-1",
+			"body": {
+				"id": "test-request-id",
+				"imp": [{
+					"id": "test-impression-id-1",
+					"banner": {
+						"h": 250,
+						"w": 300
+					},
+					"ext": {
+						"bidder": {
+							"token": "test-token-1"
+						}
+					}
+				}]
+			}
+		},
+		"mockResponse": {
+			"status": 502,
+			"body": {}
+		}
+	}],
+	"expectedMakeBidsErrors": [{
+		"value": "unexpected status code: 502",
+		"comparison": "literal"
+	}]
+}
diff --git a/adapters/appnexus/appnexusplatformtest/exemplary/simple-auction.json b/adapters/appnexus/appnexusplatformtest/exemplary/simple-auction.json
index 8e4f5295..03c3f4c5 100644
--- a/adapters/appnexus/appnexusplatformtest/exemplary/simple-auction.json
+++ b/adapters/appnexus/appnexusplatformtest/exemplary/simple-auction.json
@@ -14,7 +14,7 @@
 		  },
 		  "ext": {
 			"bidder": {
-			  "placement_id": 10433394
+			  "placement_id": 1
 			}
 		  }
 		}
@@ -46,7 +46,7 @@
 				  },
 				"ext": {
 				  "appnexus": {
-					"placement_id": 10433394
+					"placement_id": 1
 				  }
 				}
 			  }
diff --git a/adapters/appnexus/appnexusplatformtest/video/simple-video.json b/adapters/appnexus/appnexusplatformtest/video/simple-video.json
index 3c28b48a..85960427 100644
--- a/adapters/appnexus/appnexusplatformtest/video/simple-video.json
+++ b/adapters/appnexus/appnexusplatformtest/video/simple-video.json
@@ -14,7 +14,7 @@
 		  },
 		  "ext": {
 			"bidder": {
-			  "placement_id": 10433394
+			  "placement_id": 1
 			}
 		  }
 		}
@@ -46,7 +46,7 @@
 				  },
 				"ext": {
 				  "appnexus": {
-					"placement_id": 10433394
+					"placement_id": 1
 				  }
 				}
 			  }
diff --git a/adapters/appnexus/appnexustest/amp/simple-banner.json b/adapters/appnexus/appnexustest/amp/simple-banner.json
index b5ea656c..646359b4 100644
--- a/adapters/appnexus/appnexustest/amp/simple-banner.json
+++ b/adapters/appnexus/appnexustest/amp/simple-banner.json
@@ -18,7 +18,7 @@
         },
         "ext": {
           "bidder": {
-            "placement_id": 10433394
+            "placement_id": 1
           }
         }
       }
@@ -58,7 +58,7 @@
               },
               "ext": {
                 "appnexus": {
-                  "placement_id": 10433394
+                  "placement_id": 1
                 }
               }
             }
diff --git a/adapters/appnexus/appnexustest/amp/simple-video.json b/adapters/appnexus/appnexustest/amp/simple-video.json
index f44ed066..a6f96be3 100644
--- a/adapters/appnexus/appnexustest/amp/simple-video.json
+++ b/adapters/appnexus/appnexustest/amp/simple-video.json
@@ -14,7 +14,7 @@
 		  },
 		  "ext": {
 			"bidder": {
-			  "placement_id": 10433394
+			  "placement_id": 1
 			}
 		  }
 		}
@@ -48,7 +48,7 @@
 				  },
 				"ext": {
 				  "appnexus": {
-					"placement_id": 10433394
+					"placement_id": 1
 				  }
 				}
 			  }
diff --git a/adapters/appnexus/appnexustest/exemplary/simple-banner.json b/adapters/appnexus/appnexustest/exemplary/simple-banner.json
index 976f35b2..e5bd3116 100644
--- a/adapters/appnexus/appnexustest/exemplary/simple-banner.json
+++ b/adapters/appnexus/appnexustest/exemplary/simple-banner.json
@@ -18,7 +18,7 @@
         },
         "ext": {
           "bidder": {
-            "placement_id": 10433394
+            "placement_id": 1
           }
         }
       }
@@ -56,7 +56,7 @@
               },
               "ext": {
                 "appnexus": {
-                  "placement_id": 10433394
+                  "placement_id": 1
                 }
               }
             }
diff --git a/adapters/appnexus/appnexustest/exemplary/simple-video.json b/adapters/appnexus/appnexustest/exemplary/simple-video.json
index 5afef01f..15755c7d 100644
--- a/adapters/appnexus/appnexustest/exemplary/simple-video.json
+++ b/adapters/appnexus/appnexustest/exemplary/simple-video.json
@@ -14,7 +14,7 @@
 		  },
 		  "ext": {
 			"bidder": {
-			  "placement_id": 10433394
+			  "placement_id": 1
 			}
 		  }
 		}
@@ -46,7 +46,7 @@
 				  },
 				"ext": {
 				  "appnexus": {
-					"placement_id": 10433394
+					"placement_id": 1
 				  }
 				}
 			  }
diff --git a/adapters/appnexus/appnexustest/exemplary/video-invalid-category.json b/adapters/appnexus/appnexustest/exemplary/video-invalid-category.json
index 283f13cf..d3686af0 100644
--- a/adapters/appnexus/appnexustest/exemplary/video-invalid-category.json
+++ b/adapters/appnexus/appnexustest/exemplary/video-invalid-category.json
@@ -14,7 +14,7 @@
 		  },
 		  "ext": {
 			"bidder": {
-			  "placement_id": 10433394
+			  "placement_id": 1
 			}
 		  }
 		}
@@ -46,7 +46,7 @@
 				  },
 				"ext": {
 				  "appnexus": {
-					"placement_id": 10433394
+					"placement_id": 1
 				  }
 				}
 			  }
diff --git a/adapters/appnexus/appnexustest/params/race/banner.json b/adapters/appnexus/appnexustest/params/race/banner.json
index 2e2b39e9..a37e0036 100644
--- a/adapters/appnexus/appnexustest/params/race/banner.json
+++ b/adapters/appnexus/appnexustest/params/race/banner.json
@@ -1,5 +1,5 @@
 {
-  "placement_id": 10433394,
+  "placement_id": 1,
   "reserve": 20,
   "position": "below",
   "traffic_source_code": "trafficSource",
diff --git a/adapters/appnexus/appnexustest/params/race/video.json b/adapters/appnexus/appnexustest/params/race/video.json
index 2e2b39e9..a37e0036 100644
--- a/adapters/appnexus/appnexustest/params/race/video.json
+++ b/adapters/appnexus/appnexustest/params/race/video.json
@@ -1,5 +1,5 @@
 {
-  "placement_id": 10433394,
+  "placement_id": 1,
   "reserve": 20,
   "position": "below",
   "traffic_source_code": "trafficSource",
diff --git a/adapters/appnexus/appnexustest/supplemental/displaymanager-test.json b/adapters/appnexus/appnexustest/supplemental/displaymanager-test.json
index a1602ff9..d5c981c6 100644
--- a/adapters/appnexus/appnexustest/supplemental/displaymanager-test.json
+++ b/adapters/appnexus/appnexustest/supplemental/displaymanager-test.json
@@ -26,7 +26,7 @@
           },
           "ext": {
             "bidder": {
-              "placement_id": 10433394
+              "placement_id": 1
             }
           }
         }
@@ -73,7 +73,7 @@
                 "displaymanagerver": "prebid-mobile-1.0.0",
                 "ext": {
                   "appnexus": {
-                    "placement_id": 10433394
+                    "placement_id": 1
                   }
                 }
               }
diff --git a/adapters/appnexus/appnexustest/supplemental/explicit-dimensions.json b/adapters/appnexus/appnexustest/supplemental/explicit-dimensions.json
index 11b5d887..06e7724a 100644
--- a/adapters/appnexus/appnexustest/supplemental/explicit-dimensions.json
+++ b/adapters/appnexus/appnexustest/supplemental/explicit-dimensions.json
@@ -16,7 +16,7 @@
         },
         "ext": {
           "bidder": {
-            "placementId": 10433394
+            "placementId": 1
           }
         }
       }
@@ -49,7 +49,7 @@
               },
               "ext": {
                 "appnexus": {
-                  "placement_id": 10433394
+                  "placement_id": 1
                 }
               }
             }
diff --git a/adapters/appnexus/appnexustest/supplemental/multi-bid.json b/adapters/appnexus/appnexustest/supplemental/multi-bid.json
index 1e3089b1..7234551e 100644
--- a/adapters/appnexus/appnexustest/supplemental/multi-bid.json
+++ b/adapters/appnexus/appnexustest/supplemental/multi-bid.json
@@ -18,7 +18,7 @@
         },
         "ext": {
           "bidder": {
-            "placement_id": 10433394
+            "placement_id": 1
           }
         }
       }
@@ -56,7 +56,7 @@
               },
               "ext": {
                 "appnexus": {
-                  "placement_id": 10433394
+                  "placement_id": 1
                 }
               }
             }
diff --git a/adapters/gamoshi/gamoshitest/exemplary/banner-and-audio.json b/adapters/gamoshi/gamoshitest/exemplary/banner-and-audio.json
index 51c30394..b8024edd 100644
--- a/adapters/gamoshi/gamoshitest/exemplary/banner-and-audio.json
+++ b/adapters/gamoshi/gamoshitest/exemplary/banner-and-audio.json
@@ -40,7 +40,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://rtb.gamoshi.io/r/1707/bidr?reqformat=RTB_JSON",
+        "uri": "https://rtb.gamoshi.io/r/1707/bidr?bidder=prebid-server",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/adapters/gamoshi/gamoshitest/exemplary/banner-and-video.json b/adapters/gamoshi/gamoshitest/exemplary/banner-and-video.json
index 05608f1c..2342b49e 100644
--- a/adapters/gamoshi/gamoshitest/exemplary/banner-and-video.json
+++ b/adapters/gamoshi/gamoshitest/exemplary/banner-and-video.json
@@ -41,7 +41,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://rtb.gamoshi.io/r/1707/bidr?reqformat=RTB_JSON",
+        "uri": "https://rtb.gamoshi.io/r/1707/bidr?bidder=prebid-server",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/adapters/gamoshi/gamoshitest/exemplary/banner-native-audio.json b/adapters/gamoshi/gamoshitest/exemplary/banner-native-audio.json
index 71a7530b..22ce1e83 100644
--- a/adapters/gamoshi/gamoshitest/exemplary/banner-native-audio.json
+++ b/adapters/gamoshi/gamoshitest/exemplary/banner-native-audio.json
@@ -52,7 +52,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://rtb.gamoshi.io/r/1707/bidr?reqformat=RTB_JSON",
+        "uri": "https://rtb.gamoshi.io/r/1707/bidr?bidder=prebid-server",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/adapters/gamoshi/gamoshitest/exemplary/banner-video-native.json b/adapters/gamoshi/gamoshitest/exemplary/banner-video-native.json
index 80f4f24d..5999e379 100644
--- a/adapters/gamoshi/gamoshitest/exemplary/banner-video-native.json
+++ b/adapters/gamoshi/gamoshitest/exemplary/banner-video-native.json
@@ -53,7 +53,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://rtb.gamoshi.io/r/1707/bidr?reqformat=RTB_JSON",
+        "uri": "https://rtb.gamoshi.io/r/1707/bidr?bidder=prebid-server",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/adapters/gamoshi/gamoshitest/exemplary/simple-banner.json b/adapters/gamoshi/gamoshitest/exemplary/simple-banner.json
index 397da9de..2262efe9 100644
--- a/adapters/gamoshi/gamoshitest/exemplary/simple-banner.json
+++ b/adapters/gamoshi/gamoshitest/exemplary/simple-banner.json
@@ -28,7 +28,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://rtb.gamoshi.io/r/1707/bidr?reqformat=RTB_JSON",
+        "uri": "https://rtb.gamoshi.io/r/1707/bidr?bidder=prebid-server",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/adapters/gamoshi/gamoshitest/exemplary/simple-video.json b/adapters/gamoshi/gamoshitest/exemplary/simple-video.json
index 37b66ca8..2c10ad62 100644
--- a/adapters/gamoshi/gamoshitest/exemplary/simple-video.json
+++ b/adapters/gamoshi/gamoshitest/exemplary/simple-video.json
@@ -22,7 +22,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://rtb.gamoshi.io/r/1707/bidr?reqformat=RTB_JSON",
+        "uri": "https://rtb.gamoshi.io/r/1707/bidr?bidder=prebid-server",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/adapters/gamoshi/gamoshitest/exemplary/valid-extension.json b/adapters/gamoshi/gamoshitest/exemplary/valid-extension.json
index c12995c3..6806dba2 100644
--- a/adapters/gamoshi/gamoshitest/exemplary/valid-extension.json
+++ b/adapters/gamoshi/gamoshitest/exemplary/valid-extension.json
@@ -22,7 +22,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://rtb.gamoshi.io/r/1707/bidr?reqformat=RTB_JSON",
+        "uri": "https://rtb.gamoshi.io/r/1707/bidr?bidder=prebid-server",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/adapters/gamoshi/gamoshitest/exemplary/valid-with-device.json b/adapters/gamoshi/gamoshitest/exemplary/valid-with-device.json
index c4756470..32fd21e2 100644
--- a/adapters/gamoshi/gamoshitest/exemplary/valid-with-device.json
+++ b/adapters/gamoshi/gamoshitest/exemplary/valid-with-device.json
@@ -38,7 +38,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://rtb.gamoshi.io/r/1707/bidr?reqformat=RTB_JSON",
+        "uri": "https://rtb.gamoshi.io/r/1707/bidr?bidder=prebid-server",
         "body": {
           "id": "test-request-id",
           "device": {
diff --git a/adapters/gamoshi/gamoshitest/exemplary/video-and-audio.json b/adapters/gamoshi/gamoshitest/exemplary/video-and-audio.json
index 02b7bfb5..ec01c7b9 100644
--- a/adapters/gamoshi/gamoshitest/exemplary/video-and-audio.json
+++ b/adapters/gamoshi/gamoshitest/exemplary/video-and-audio.json
@@ -34,7 +34,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://rtb.gamoshi.io/r/1707/bidr?reqformat=RTB_JSON",
+        "uri": "https://rtb.gamoshi.io/r/1707/bidr?bidder=prebid-server",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/adapters/gamoshi/gamoshitest/supplemental/bad-response-no-body.json b/adapters/gamoshi/gamoshitest/supplemental/bad-response-no-body.json
index 88d66fc4..29390963 100644
--- a/adapters/gamoshi/gamoshitest/supplemental/bad-response-no-body.json
+++ b/adapters/gamoshi/gamoshitest/supplemental/bad-response-no-body.json
@@ -22,7 +22,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://rtb.gamoshi.io/r/1707/bidr?reqformat=RTB_JSON",
+        "uri": "https://rtb.gamoshi.io/r/1707/bidr?bidder=prebid-server",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/adapters/gamoshi/gamoshitest/supplemental/status-bad-request.json b/adapters/gamoshi/gamoshitest/supplemental/status-bad-request.json
index a0293bf6..64c75fc8 100644
--- a/adapters/gamoshi/gamoshitest/supplemental/status-bad-request.json
+++ b/adapters/gamoshi/gamoshitest/supplemental/status-bad-request.json
@@ -22,7 +22,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://rtb.gamoshi.io/r/1707/bidr?reqformat=RTB_JSON",
+        "uri": "https://rtb.gamoshi.io/r/1707/bidr?bidder=prebid-server",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/adapters/gamoshi/gamoshitest/supplemental/status-no-content.json b/adapters/gamoshi/gamoshitest/supplemental/status-no-content.json
index 57227e72..3bcb4a9b 100644
--- a/adapters/gamoshi/gamoshitest/supplemental/status-no-content.json
+++ b/adapters/gamoshi/gamoshitest/supplemental/status-no-content.json
@@ -22,7 +22,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://rtb.gamoshi.io/r/1707/bidr?reqformat=RTB_JSON",
+        "uri": "https://rtb.gamoshi.io/r/1707/bidr?bidder=prebid-server",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/adapters/gamoshi/gamoshitest/supplemental/unexpected-status-code.json b/adapters/gamoshi/gamoshitest/supplemental/unexpected-status-code.json
index acb9652f..1789707a 100644
--- a/adapters/gamoshi/gamoshitest/supplemental/unexpected-status-code.json
+++ b/adapters/gamoshi/gamoshitest/supplemental/unexpected-status-code.json
@@ -22,7 +22,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://rtb.gamoshi.io/r/1707/bidr?reqformat=RTB_JSON",
+        "uri": "https://rtb.gamoshi.io/r/1707/bidr?bidder=prebid-server",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/adapters/gamoshi/usersync_test.go b/adapters/gamoshi/usersync_test.go
index 10e23c02..b8e3e327 100644
--- a/adapters/gamoshi/usersync_test.go
+++ b/adapters/gamoshi/usersync_test.go
@@ -5,20 +5,25 @@ import (
 	"text/template"
 
 	"github.com/prebid/prebid-server/privacy"
+	"github.com/prebid/prebid-server/privacy/ccpa"
 	"github.com/stretchr/testify/assert"
 )
 
 func TestGamoshiSyncer(t *testing.T) {
-	syncURL := "https://rtb.gamoshi.io/pix/1707/scm?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&rurl=localhost/setuid%3Fbidder%3Dgamoshi%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5Bgusr%5D"
+	syncURL := "https://rtb.gamoshi.io/user_sync_prebid?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&rurl=https%3A%2F%2Flocalhost%2Fsetuid%3Fbidder%3Dgamoshi%26gdpr%3D%7B%7B.GDPR%7D%7D%26gdpr_consent%3D%7B%7B.GDPRConsent%7D%7D%26uid%3D%5Bgusr%5D"
 	syncURLTemplate := template.Must(
 		template.New("sync-template").Parse(syncURL),
 	)
 
 	syncer := NewGamoshiSyncer(syncURLTemplate)
-	syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{})
+	syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{
+		CCPA: ccpa.Policy{
+			Value: "anyValue",
+		},
+	})
 
 	assert.NoError(t, err)
-	assert.Equal(t, "https://rtb.gamoshi.io/pix/1707/scm?gdpr=&consent=&rurl=localhost/setuid%3Fbidder%3Dgamoshi%26gdpr%3D%26gdpr_consent%3D%26uid%3D%5Bgusr%5D", syncInfo.URL)
+	assert.Equal(t, "https://rtb.gamoshi.io/user_sync_prebid?gdpr=&consent=&us_privacy=anyValue&rurl=https%3A%2F%2Flocalhost%2Fsetuid%3Fbidder%3Dgamoshi%26gdpr%3D%7B%7B.GDPR%7D%7D%26gdpr_consent%3D%7B%7B.GDPRConsent%7D%7D%26uid%3D%5Bgusr%5D", syncInfo.URL)
 	assert.Equal(t, "redirect", syncInfo.Type)
 	assert.EqualValues(t, 644, syncer.GDPRVendorID())
 	assert.Equal(t, false, syncInfo.SupportCORS)
diff --git a/adapters/marsmedia/marsmedia_test.go b/adapters/marsmedia/marsmedia_test.go
new file mode 100644
index 00000000..ce96d882
--- /dev/null
+++ b/adapters/marsmedia/marsmedia_test.go
@@ -0,0 +1,11 @@
+package marsmedia
+
+import (
+	"testing"
+
+	"github.com/prebid/prebid-server/adapters/adapterstest"
+)
+
+func TestJsonSamples(t *testing.T) {
+	adapterstest.RunJSONBidderTest(t, "marsmediatest", NewMarsmediaBidder("http://bid306.rtbsrv.com/bidder/?bid=f3xtet"))
+}
diff --git a/adapters/marsmedia/marsmediatest/exemplary/simple-banner.json b/adapters/marsmedia/marsmediatest/exemplary/simple-banner.json
new file mode 100644
index 00000000..17f2c038
--- /dev/null
+++ b/adapters/marsmedia/marsmediatest/exemplary/simple-banner.json
@@ -0,0 +1,105 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "at" : 1,
+        "banner": {
+          "format": [
+            {
+                "w": 300,
+                "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "zone": "9999"
+          }
+        }
+      }
+    ]
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://bid306.rtbsrv.com/bidder/?bid=f3xtet&zone=9999",
+        "body": {
+          "id": "test-request-id",
+          "at" : 1,
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "banner": {
+                "format": [
+                  {
+                      "w": 300,
+                      "h": 250
+                  }
+                ],
+                "w": 300,
+                "h": 250
+              },
+              "ext": {
+                "bidder": {
+                  "zone": "9999"
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "marsmedia",
+              "bid": [{
+                "id": "1",
+                "impid": "test-imp-id",
+                "price": 1.1,
+                "adid": "17586",
+                "adm": "some-test-ad",
+                "nurl" : "some-test-nurl",
+                "iurl" : "some-test-iurl",
+                "adomain": ["wooga.com"],
+                "cid": "17586",
+                "crid": "17586",
+                "h": 250,
+                "w": 300
+              }]
+            }
+          ],
+          "bidid": "5778926625248726496",
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+
+  "expectedBids": [
+    {
+      "bid": {
+        "id": "7706636740145184841",
+        "impid": "test-imp-id",
+        "price": 0.5,
+        "adm": "some-test-ad",
+        "adid": "29681110",
+        "adomain": ["yahoo.com"],
+        "cid": "958",
+        "crid": "29681110",
+        "h": 250,
+        "w": 300
+      },
+      "ext": {
+          "prebid": {
+              "type": "banner"
+          }
+      }
+    }
+  ]
+}
diff --git a/adapters/marsmedia/marsmediatest/exemplary/simple-video.json b/adapters/marsmedia/marsmediatest/exemplary/simple-video.json
new file mode 100644
index 00000000..3a636374
--- /dev/null
+++ b/adapters/marsmedia/marsmediatest/exemplary/simple-video.json
@@ -0,0 +1,85 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "video": {
+          "mimes": ["video/mp4"],
+          "protocols": [2, 5],
+          "w": 1024,
+          "h": 576
+        },
+        "ext":{
+          "bidder":{
+            "zone": "9999"
+          }
+        }
+      }
+    ]
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://bid306.rtbsrv.com/bidder/?bid=f3xtet&zone=9999",
+        "body": {
+          "id": "test-request-id",
+          "at":1,
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "video": {
+                "mimes": ["video/mp4"],
+                "protocols": [2, 5],
+                "w": 1024,
+                "h": 576
+              },
+              "ext": {
+                "bidder": {
+                  "zone": "9999"
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "cur": "USD",
+          "seatbid": [
+            {
+              "seat": "Marsmedia",
+              "bid": [{
+                "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
+                "impid": "test-imp-id",
+                "price": 0.500000,
+                "adm": "some-test-ad",
+                "crid": "crid_10",
+                "w": 1024,
+                "h": 576
+              }]
+            }
+          ]
+        }
+      }
+    }
+  ],
+
+  "expectedBids": [
+    {
+      "bid": {
+        "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
+        "impid": "test-imp-id",
+        "price": 0.5,
+        "adm": "some-test-ad",
+        "crid": "crid_10",
+        "w": 1024,
+        "h": 576
+      },
+      "type": "video"
+    }
+  ]
+}
diff --git a/adapters/marsmedia/marsmediatest/exemplary/valid-extension.json b/adapters/marsmedia/marsmediatest/exemplary/valid-extension.json
new file mode 100644
index 00000000..38bacb19
--- /dev/null
+++ b/adapters/marsmedia/marsmediatest/exemplary/valid-extension.json
@@ -0,0 +1,85 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "video": {
+          "mimes": ["video/mp4"],
+          "protocols": [2, 5],
+          "w": 1024,
+          "h": 576
+        },
+        "ext":{
+          "bidder":{
+            "zone": "9999"
+           }
+        }
+      }
+    ]
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://bid306.rtbsrv.com/bidder/?bid=f3xtet&zone=9999",
+        "body": {
+          "id": "test-request-id",
+          "at":1,
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "video": {
+                "mimes": ["video/mp4"],
+                "protocols": [2, 5],
+                "w": 1024,
+                "h": 576
+              },
+              "ext": {
+                "bidder": {
+                  "zone": "9999"
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "cur": "USD",
+          "seatbid": [
+            {
+              "seat": "Marsmedia",
+              "bid": [{
+                "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
+                "impid": "test-imp-id",
+                "price": 0.500000,
+                "adm": "some-test-ad",
+                "crid": "crid_10",
+                "w": 1024,
+                "h": 576
+              }]
+            }
+          ]
+        }
+      }
+    }
+  ],
+
+  "expectedBids": [
+    {
+      "bid": {
+        "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
+        "impid": "test-imp-id",
+        "price": 0.5,
+        "adm": "some-test-ad",
+        "crid": "crid_10",
+        "w": 1024,
+        "h": 576
+      },
+      "type": "video"
+    }
+  ]
+}
diff --git a/adapters/marsmedia/marsmediatest/params/race/banner.json b/adapters/marsmedia/marsmediatest/params/race/banner.json
new file mode 100644
index 00000000..3a727957
--- /dev/null
+++ b/adapters/marsmedia/marsmediatest/params/race/banner.json
@@ -0,0 +1,3 @@
+{
+  "zone": "9999"
+}
diff --git a/adapters/marsmedia/marsmediatest/params/race/video.json b/adapters/marsmedia/marsmediatest/params/race/video.json
new file mode 100644
index 00000000..3a727957
--- /dev/null
+++ b/adapters/marsmedia/marsmediatest/params/race/video.json
@@ -0,0 +1,3 @@
+{
+  "zone": "9999"
+}
diff --git a/adapters/marsmedia/marsmediatest/supplemental/invalid-extension.json b/adapters/marsmedia/marsmediatest/supplemental/invalid-extension.json
new file mode 100644
index 00000000..4ee31fe4
--- /dev/null
+++ b/adapters/marsmedia/marsmediatest/supplemental/invalid-extension.json
@@ -0,0 +1,31 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-invalid-ext-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+        }
+
+      }
+    ]
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "ext.bidder.zone not provided",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/marsmedia/marsmediatest/supplemental/invalid-imp.json b/adapters/marsmedia/marsmediatest/supplemental/invalid-imp.json
new file mode 100644
index 00000000..641066d7
--- /dev/null
+++ b/adapters/marsmedia/marsmediatest/supplemental/invalid-imp.json
@@ -0,0 +1,16 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "ext": {
+      "bidder": {
+        "zone": "9999"
+      }
+    }
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "No impression in the bid request",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/marsmedia/marsmediatest/supplemental/missing-extension.json b/adapters/marsmedia/marsmediatest/supplemental/missing-extension.json
new file mode 100644
index 00000000..82ec775d
--- /dev/null
+++ b/adapters/marsmedia/marsmediatest/supplemental/missing-extension.json
@@ -0,0 +1,22 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-missing-ext-id",
+        "video": {
+          "mimes": ["video/mp4"],
+          "protocols": [2, 5],
+          "w": 1024,
+          "h": 576
+        }
+      }
+    ]
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "ext.bidder not provided",
+      "comparison": "literal"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/marsmedia/marsmediatest/supplemental/missing-param.json b/adapters/marsmedia/marsmediatest/supplemental/missing-param.json
new file mode 100644
index 00000000..70117f3b
--- /dev/null
+++ b/adapters/marsmedia/marsmediatest/supplemental/missing-param.json
@@ -0,0 +1,34 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-missing-req-param-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "zone":""
+          }
+        }
+
+      }
+    ]
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "Zone is empty",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/marsmedia/params_test.go b/adapters/marsmedia/params_test.go
new file mode 100644
index 00000000..43cd49c2
--- /dev/null
+++ b/adapters/marsmedia/params_test.go
@@ -0,0 +1,59 @@
+package marsmedia
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+// This file actually intends to test static/bidder-params/marsmedia.json
+//
+// These also validate the format of the external API: request.imp[i].ext.marsmedia
+
+// TestValidParams makes sure that the Marsmedia 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.BidderMarsmedia, json.RawMessage(validParam)); err != nil {
+			t.Errorf("Schema rejected Marsmedia params: %s", validParam)
+		}
+	}
+}
+
+// TestInvalidParams makes sure that the Marsmedia 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.BidderMarsmedia, json.RawMessage(invalidParam)); err == nil {
+			t.Errorf("Schema allowed unexpected params: %s", invalidParam)
+		}
+	}
+}
+
+var validParams = []string{
+	`{"zone": "9999"}`,
+}
+
+var invalidParams = []string{
+	`{"zone": 100}`,
+	`{"headerbidding": false}`,
+	`{"zone": true}`,
+	`{"zoneId": 123, "headerbidding": true}`,
+	`{"zoneID": "1"}`,
+	``,
+	`null`,
+	`true`,
+	`9`,
+	`1.2`,
+	`[]`,
+	`{}`,
+}
diff --git a/adapters/marsmedia/usersync_test.go b/adapters/marsmedia/usersync_test.go
new file mode 100644
index 00000000..0e50e2bc
--- /dev/null
+++ b/adapters/marsmedia/usersync_test.go
@@ -0,0 +1,20 @@
+package marsmedia
+
+import (
+	"testing"
+	"text/template"
+
+	"github.com/prebid/prebid-server/privacy"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestMarsmediaSyncer(t *testing.T) {
+	temp := template.Must(template.New("sync-template").Parse("http://dmp.rtbsrv.com/dmp/profiles/cm?p_id=179&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&redirect=localhost:8000%2Fsetuid%3Fbidder%3Dmarsmedia%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUUID%7D"))
+	syncer := NewMarsmediaSyncer(temp)
+	syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{})
+	assert.NoError(t, err)
+	assert.Equal(t, "http://dmp.rtbsrv.com/dmp/profiles/cm?p_id=179&gdpr=&gdpr_consent=&redirect=localhost:8000%2Fsetuid%3Fbidder%3Dmarsmedia%26gdpr%3D%26gdpr_consent%3D%26uid%3D%24%7BUUID%7D", syncInfo.URL)
+	assert.Equal(t, "redirect", syncInfo.Type)
+	assert.EqualValues(t, 0, syncer.GDPRVendorID())
+	assert.Equal(t, false, syncInfo.SupportCORS)
+}
diff --git a/adapters/pubmatic/pubmatic_test.go b/adapters/pubmatic/pubmatic_test.go
index 4eb66d25..be086f5a 100644
--- a/adapters/pubmatic/pubmatic_test.go
+++ b/adapters/pubmatic/pubmatic_test.go
@@ -23,7 +23,7 @@ import (
 )
 
 func TestJsonSamples(t *testing.T) {
-	adapterstest.RunJSONBidderTest(t, "pubmatictest", NewPubmaticBidder(nil, "http://hbopenbid.pubmatic.com/translator?source=prebid-server"))
+	adapterstest.RunJSONBidderTest(t, "pubmatictest", NewPubmaticBidder(nil, "https://hbopenbid.pubmatic.com/translator?source=prebid-server"))
 }
 
 // ----------------------------------------------------------------------------
diff --git a/adapters/pubmatic/pubmatictest/exemplary/simple-banner.json b/adapters/pubmatic/pubmatictest/exemplary/simple-banner.json
index e669bce8..1eb8a212 100644
--- a/adapters/pubmatic/pubmatictest/exemplary/simple-banner.json
+++ b/adapters/pubmatic/pubmatictest/exemplary/simple-banner.json
@@ -43,7 +43,7 @@
     "httpCalls": [
       {
         "expectedRequest": {
-          "uri": "http://hbopenbid.pubmatic.com/translator?source=prebid-server",
+          "uri": "https://hbopenbid.pubmatic.com/translator?source=prebid-server",
           "body": {
             "id": "test-request-id",
             "imp": [
diff --git a/adapters/pubmatic/pubmatictest/exemplary/video.json b/adapters/pubmatic/pubmatictest/exemplary/video.json
index f9cfc297..25ed366e 100644
--- a/adapters/pubmatic/pubmatictest/exemplary/video.json
+++ b/adapters/pubmatic/pubmatictest/exemplary/video.json
@@ -49,7 +49,7 @@
     "httpCalls": [
       {
         "expectedRequest": {
-          "uri": "http://hbopenbid.pubmatic.com/translator?source=prebid-server",
+          "uri": "https://hbopenbid.pubmatic.com/translator?source=prebid-server",
           "body": {
             "id": "test-video-request",
             "imp": [
diff --git a/adapters/pubmatic/pubmatictest/supplemental/app.json b/adapters/pubmatic/pubmatictest/supplemental/app.json
index a2b737f0..636433ca 100644
--- a/adapters/pubmatic/pubmatictest/supplemental/app.json
+++ b/adapters/pubmatic/pubmatictest/supplemental/app.json
@@ -43,7 +43,7 @@
     "httpCalls": [
       {
         "expectedRequest": {
-          "uri": "http://hbopenbid.pubmatic.com/translator?source=prebid-server",
+          "uri": "https://hbopenbid.pubmatic.com/translator?source=prebid-server",
           "body": {
             "id": "app-request",
             "imp": [
diff --git a/adapters/pubmatic/pubmatictest/supplemental/multiplemedia.json b/adapters/pubmatic/pubmatictest/supplemental/multiplemedia.json
index faf71fec..a576abe6 100644
--- a/adapters/pubmatic/pubmatictest/supplemental/multiplemedia.json
+++ b/adapters/pubmatic/pubmatictest/supplemental/multiplemedia.json
@@ -29,7 +29,7 @@
     "httpCalls": [
         {
           "expectedRequest": {
-            "uri": "http://hbopenbid.pubmatic.com/translator?source=prebid-server",
+            "uri": "https://hbopenbid.pubmatic.com/translator?source=prebid-server",
             "body": {
               "id": "multiple-media-request",
               "imp": [
diff --git a/adapters/rubicon/rubicon_test.go b/adapters/rubicon/rubicon_test.go
index bc3d2e8a..dd9cea62 100644
--- a/adapters/rubicon/rubicon_test.go
+++ b/adapters/rubicon/rubicon_test.go
@@ -1244,8 +1244,9 @@ func TestOpenRTBRequestWithVideoImpEvenIfImpHasBannerButAllRequiredVideoFields(t
 				"zoneId": 8394,
 				"siteId": 283282,
 				"accountId": 7891,
-				"inventory": {"key1" : "val1"},
-				"visitor": {"key2" : "val2"}
+				"inventory": {"key1": "val1"},
+				"visitor": {"key2": "val2"},
+				"video": {"size_id": 1}
 			}}`),
 		}},
 	}
diff --git a/adapters/rubicon/rubicontest/supplemental/required-video-size-id.json b/adapters/rubicon/rubicontest/supplemental/required-video-size-id.json
new file mode 100644
index 00000000..cf0a594e
--- /dev/null
+++ b/adapters/rubicon/rubicontest/supplemental/required-video-size-id.json
@@ -0,0 +1,31 @@
+{
+    "mockBidRequest": {
+        "id": "test-req-id",
+        "imp": [
+            {
+                "id": "test-imp-1",
+                "video": {
+                    "w": 640,
+                    "h": 480,
+                    "linearity": 1
+                },
+                "ext": {
+                    "bidder": {
+                        "accountId": 1001,
+                        "siteId":113932,
+                        "zoneId":535510
+                    }
+                }
+            }
+        ],
+        "site": {
+            "page": "prebid.org"
+        }
+    },
+    "expectedMakeRequestsErrors": [
+        {
+            "value": "imp[0].ext.bidder.rubicon.video.size_id must be defined for video impression",
+            "comparison": "literal"
+        }
+    ]
+}
diff --git a/adapters/syncer_test.go b/adapters/syncer_test.go
index 8defd7b6..9be52309 100644
--- a/adapters/syncer_test.go
+++ b/adapters/syncer_test.go
@@ -17,7 +17,7 @@ func TestGetUsersyncInfo(t *testing.T) {
 			Consent: "B",
 		},
 		CCPA: ccpa.Policy{
-			Signal: "C",
+			Value: "C",
 		},
 	}
 
diff --git a/adapters/triplelift_native/triplelift_native_test.go b/adapters/triplelift_native/triplelift_native_test.go
index 0beb18ed..69d05625 100644
--- a/adapters/triplelift_native/triplelift_native_test.go
+++ b/adapters/triplelift_native/triplelift_native_test.go
@@ -19,5 +19,5 @@ func TestBadConfig(t *testing.T) {
 }
 
 func TestJsonSamples(t *testing.T) {
-	adapterstest.RunJSONBidderTest(t, "triplelift_nativetest", NewTripleliftNativeBidder(nil, "http://tlx.3lift.net/s2s/auction?supplier_id=19", "{\"publisher_whitelist\":[\"foo\",\"bar\",\"baz\"], \"endpoint\":\"http://tlx.3lift.net/s2sn/auction?supplier_id=20\"}"))
+	adapterstest.RunJSONBidderTest(t, "triplelift_nativetest", NewTripleliftNativeBidder(nil, "http://tlx.3lift.net/s2sn/auction?supplier_id=20", "{\"publisher_whitelist\":[\"foo\",\"bar\",\"baz\"]}"))
 }
diff --git a/analytics/config/config_test.go b/analytics/config/config_test.go
index cb2bb049..0fd3ec20 100644
--- a/analytics/config/config_test.go
+++ b/analytics/config/config_test.go
@@ -45,6 +45,11 @@ func TestSampleModule(t *testing.T) {
 	if count != 4 {
 		t.Errorf("PBSAnalyticsModule failed at LogAmpObject")
 	}
+
+	am.LogVideoObject(&analytics.VideoObject{})
+	if count != 5 {
+		t.Errorf("PBSAnalyticsModule failed at LogVideoObject")
+	}
 }
 
 type sampleModule struct {
@@ -53,6 +58,8 @@ type sampleModule struct {
 
 func (m *sampleModule) LogAuctionObject(ao *analytics.AuctionObject) { *m.count++ }
 
+func (m *sampleModule) LogVideoObject(vo *analytics.VideoObject) { *m.count++ }
+
 func (m *sampleModule) LogCookieSyncObject(cso *analytics.CookieSyncObject) { *m.count++ }
 
 func (m *sampleModule) LogSetUIDObject(so *analytics.SetUIDObject) { *m.count++ }
diff --git a/analytics/filesystem/file_module_test.go b/analytics/filesystem/file_module_test.go
index 4ec2bb6a..6e2d9f62 100644
--- a/analytics/filesystem/file_module_test.go
+++ b/analytics/filesystem/file_module_test.go
@@ -34,6 +34,15 @@ func TestAuctionObject_ToJson(t *testing.T) {
 	}
 }
 
+func TestVideoObject_ToJson(t *testing.T) {
+	vo := &analytics.VideoObject{
+		Status: http.StatusOK,
+	}
+	if voJson := jsonifyVideoObject(vo); strings.Contains(voJson, "Transactional Logs Error") {
+		t.Fatalf("AuctionObject failed to convert to json")
+	}
+}
+
 func TestSetUIDObject_ToJson(t *testing.T) {
 	so := &analytics.SetUIDObject{
 		Status: http.StatusOK,
@@ -64,6 +73,7 @@ func TestFileLogger_LogObjects(t *testing.T) {
 	defer os.RemoveAll(TEST_DIR)
 	if fl, err := NewFileLogger(TEST_DIR + "//test"); err == nil {
 		fl.LogAuctionObject(&analytics.AuctionObject{})
+		fl.LogVideoObject(&analytics.VideoObject{})
 		fl.LogAmpObject(&analytics.AmpObject{})
 		fl.LogSetUIDObject(&analytics.SetUIDObject{})
 		fl.LogCookieSyncObject(&analytics.CookieSyncObject{})
diff --git a/config/config_test.go b/config/config_test.go
index 4d2b0a1c..24008980 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -27,7 +27,7 @@ func TestDefaults(t *testing.T) {
 	cmpInts(t, "host_cookie.ttl_days", int(cfg.HostCookie.TTL), 90)
 	cmpInts(t, "host_cookie.max_cookie_size_bytes", cfg.HostCookie.MaxCookieSizeBytes, 0)
 	cmpStrings(t, "datacache.type", cfg.DataCache.Type, "dummy")
-	cmpStrings(t, "adapters.pubmatic.endpoint", cfg.Adapters[string(openrtb_ext.BidderPubmatic)].Endpoint, "http://hbopenbid.pubmatic.com/translator?source=prebid-server")
+	cmpStrings(t, "adapters.pubmatic.endpoint", cfg.Adapters[string(openrtb_ext.BidderPubmatic)].Endpoint, "https://hbopenbid.pubmatic.com/translator?source=prebid-server")
 	cmpInts(t, "currency_converter.fetch_interval_seconds", cfg.CurrencyConverter.FetchIntervalSeconds, 1800)
 	cmpStrings(t, "currency_converter.fetch_url", cfg.CurrencyConverter.FetchURL, "https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json")
 	cmpBools(t, "account_required", cfg.AccountRequired, false)
@@ -41,6 +41,8 @@ gdpr:
   host_vendor_id: 15
   usersync_if_ambiguous: true
   non_standard_publishers: ["siteID","fake-site-id","appID","agltb3B1Yi1pbmNyDAsSA0FwcBiJkfIUDA"]
+ccpa:
+  enforce: true
 host_cookie:
   cookie_name: userid
   family: prebid
@@ -102,7 +104,7 @@ adapters:
       username: rubiuser
       password: rubipw23
   brightroll:
-    usersync_url: http://test-bh.ybp.yahoo.com/sync/appnexuspbs?gdpr={{.GDPR}}&euconsent={{.GDPRConsent}}&url=%s
+    usersync_url: http://test-bh.ybp.yahoo.com/sync/appnexuspbs?gdpr={{.GDPR}}&euconsent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&url=%s
     endpoint: http://test-bid.ybp.yahoo.com/bid/appnexuspbs
   adkerneladn:
      usersync_url: https://tag.adkernel.com/syncr?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&r=
@@ -230,6 +232,8 @@ func TestFullConfig(t *testing.T) {
 	_, found = cfg.GDPR.NonStandardPublisherMap["appnexus"]
 	cmpBools(t, "cfg.GDPR.NonStandardPublisherMap", found, false)
 
+	cmpBools(t, "ccpa.enforce", cfg.CCPA.Enforce, true)
+
 	//Assert the NonStandardPublishers was correctly unmarshalled
 	cmpStrings(t, "blacklisted_apps", cfg.BlacklistedApps[0], "spamAppID")
 	cmpStrings(t, "blacklisted_apps", cfg.BlacklistedApps[1], "sketchy-app-id")
@@ -265,7 +269,7 @@ func TestFullConfig(t *testing.T) {
 	cmpStrings(t, "adapters.rubicon.xapi.username", cfg.Adapters[string(openrtb_ext.BidderRubicon)].XAPI.Username, "rubiuser")
 	cmpStrings(t, "adapters.rubicon.xapi.password", cfg.Adapters[string(openrtb_ext.BidderRubicon)].XAPI.Password, "rubipw23")
 	cmpStrings(t, "adapters.brightroll.endpoint", cfg.Adapters[string(openrtb_ext.BidderBrightroll)].Endpoint, "http://test-bid.ybp.yahoo.com/bid/appnexuspbs")
-	cmpStrings(t, "adapters.brightroll.usersync_url", cfg.Adapters[string(openrtb_ext.BidderBrightroll)].UserSyncURL, "http://test-bh.ybp.yahoo.com/sync/appnexuspbs?gdpr={{.GDPR}}&euconsent={{.GDPRConsent}}&url=%s")
+	cmpStrings(t, "adapters.brightroll.usersync_url", cfg.Adapters[string(openrtb_ext.BidderBrightroll)].UserSyncURL, "http://test-bh.ybp.yahoo.com/sync/appnexuspbs?gdpr={{.GDPR}}&euconsent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&url=%s")
 	cmpStrings(t, "adapters.adkerneladn.usersync_url", cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderAdkernelAdn))].UserSyncURL, "https://tag.adkernel.com/syncr?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&r=")
 	cmpStrings(t, "adapters.rhythmone.endpoint", cfg.Adapters[string(openrtb_ext.BidderRhythmone)].Endpoint, "http://tag.1rx.io/rmp")
 	cmpStrings(t, "adapters.rhythmone.usersync_url", cfg.Adapters[string(openrtb_ext.BidderRhythmone)].UserSyncURL, "https://sync.1rx.io/usersync2/rmphb?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&redir=http%3A%2F%2Fprebid-server.prebid.org%2F%2Fsetuid%3Fbidder%3Drhythmone%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5BRX_UUID%5D")
diff --git a/currencies/rate_converter_test.go b/currencies/rate_converter_test.go
index a1807a63..f9a14895 100644
--- a/currencies/rate_converter_test.go
+++ b/currencies/rate_converter_test.go
@@ -379,6 +379,7 @@ func TestRates(t *testing.T) {
 		{from: "", to: "EUR", expectedRate: 0, hasError: true},
 		{from: "CNY", to: "", expectedRate: 0, hasError: true},
 		{from: "", to: "", expectedRate: 0, hasError: true},
+		{from: "USD", to: "USD", expectedRate: 1, hasError: false},
 	}
 
 	mockedHttpServer := httptest.NewServer(http.HandlerFunc(
diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go
index 8f126bb9..011cecf3 100644
--- a/endpoints/cookie_sync_test.go
+++ b/endpoints/cookie_sync_test.go
@@ -65,6 +65,61 @@ func TestGDPRConsentRequired(t *testing.T) {
 	assert.Equal(t, "gdpr_consent is required if gdpr=1\n", rr.Body.String())
 }
 
+func TestCCPA(t *testing.T) {
+	testCases := []struct {
+		description   string
+		requestBody   string
+		enforceCCPA   bool
+		expectedSyncs []string
+	}{
+		{
+			description:   "Feature Flag On & Opt-Out Yes",
+			requestBody:   `{"bidders":["appnexus"], "us_privacy":"1-Y-"}`,
+			enforceCCPA:   true,
+			expectedSyncs: []string{},
+		},
+		{
+			description:   "Feature Flag Off & Opt-Out Yes",
+			requestBody:   `{"bidders":["appnexus"], "us_privacy":"1-Y-"}`,
+			enforceCCPA:   false,
+			expectedSyncs: []string{"appnexus"},
+		},
+		{
+			description:   "Feature Flag On & Opt-Out No",
+			requestBody:   `{"bidders":["appnexus"], "us_privacy":"1-N-"}`,
+			enforceCCPA:   false,
+			expectedSyncs: []string{"appnexus"},
+		},
+		{
+			description:   "Feature Flag On & Opt-Out Unknown",
+			requestBody:   `{"bidders":["appnexus"], "us_privacy":"1---"}`,
+			enforceCCPA:   false,
+			expectedSyncs: []string{"appnexus"},
+		},
+		{
+			description:   "Feature Flag On & Opt-Out Invalid",
+			requestBody:   `{"bidders":["appnexus"], "us_privacy":"invalid"}`,
+			enforceCCPA:   false,
+			expectedSyncs: []string{"appnexus"},
+		},
+		{
+			description:   "Feature Flag On & Opt-Out Not Provided",
+			requestBody:   `{"bidders":["appnexus"]}`,
+			enforceCCPA:   false,
+			expectedSyncs: []string{"appnexus"},
+		},
+	}
+
+	for _, test := range testCases {
+		gdpr := config.GDPR{UsersyncIfAmbiguous: true}
+		ccpa := config.CCPA{Enforce: test.enforceCCPA}
+		rr := doConfigurablePost(test.requestBody, nil, true, syncersForTest(), gdpr, ccpa)
+		assert.Equal(t, http.StatusOK, rr.Code, test.description+":httpResponseCode")
+		assert.ElementsMatch(t, test.expectedSyncs, parseSyncs(t, rr.Body.Bytes()), test.description+":syncs")
+		assert.Equal(t, "no_cookie", parseStatus(t, rr.Body.Bytes()), test.description+":status")
+	}
+}
+
 func TestCookieSyncHasCookies(t *testing.T) {
 	rr := doPost(`{"bidders":["appnexus", "audienceNetwork", "random"]}`, map[string]string{
 		"adnxs":           "1234",
@@ -95,7 +150,7 @@ func TestCookieSyncNoBidders(t *testing.T) {
 }
 
 func TestCookieSyncNoCookiesBrokenGDPR(t *testing.T) {
-	rr := doConfigurablePost(`{"bidders":["appnexus", "audienceNetwork", "random"],"gdpr_consent":"GLKHGKGKKGK"}`, nil, true, map[openrtb_ext.BidderName]usersync.Usersyncer{}, config.GDPR{UsersyncIfAmbiguous: true})
+	rr := doConfigurablePost(`{"bidders":["appnexus", "audienceNetwork", "random"],"gdpr_consent":"GLKHGKGKKGK"}`, nil, true, map[openrtb_ext.BidderName]usersync.Usersyncer{}, config.GDPR{UsersyncIfAmbiguous: true}, config.CCPA{})
 	assert.Equal(t, rr.Header().Get("Content-Type"), "application/json; charset=utf-8")
 	assert.Equal(t, http.StatusOK, rr.Code)
 	assert.ElementsMatch(t, []string{"appnexus", "audienceNetwork"}, parseSyncs(t, rr.Body.Bytes()))
@@ -118,15 +173,16 @@ func TestCookieSyncWithLargeLimit(t *testing.T) {
 }
 
 func doPost(body string, existingSyncs map[string]string, gdprHostConsent bool, gdprBidders map[openrtb_ext.BidderName]usersync.Usersyncer) *httptest.ResponseRecorder {
-	return doConfigurablePost(body, existingSyncs, gdprHostConsent, gdprBidders, config.GDPR{})
+	return doConfigurablePost(body, existingSyncs, gdprHostConsent, gdprBidders, config.GDPR{}, config.CCPA{})
 }
 
-func doConfigurablePost(body string, existingSyncs map[string]string, gdprHostConsent bool, gdprBidders map[openrtb_ext.BidderName]usersync.Usersyncer, cfgGDPR config.GDPR) *httptest.ResponseRecorder {
-	endpoint := testableEndpoint(mockPermissions(gdprHostConsent, gdprBidders), cfgGDPR)
+func doConfigurablePost(body string, existingSyncs map[string]string, gdprHostConsent bool, gdprBidders map[openrtb_ext.BidderName]usersync.Usersyncer, cfgGDPR config.GDPR, cfgCCPA config.CCPA) *httptest.ResponseRecorder {
+	endpoint := testableEndpoint(mockPermissions(gdprHostConsent, gdprBidders), cfgGDPR, cfgCCPA)
 	router := httprouter.New()
 	router.POST("/cookie_sync", endpoint)
 	req, _ := http.NewRequest("POST", "/cookie_sync", strings.NewReader(body))
 	if len(existingSyncs) > 0 {
+
 		pcs := usersync.NewPBSCookie()
 		for bidder, uid := range existingSyncs {
 			pcs.TrySync(bidder, uid)
@@ -139,8 +195,8 @@ func doConfigurablePost(body string, existingSyncs map[string]string, gdprHostCo
 	return rr
 }
 
-func testableEndpoint(perms gdpr.Permissions, cfgGDPR config.GDPR) httprouter.Handle {
-	return NewCookieSyncEndpoint(syncersForTest(), &config.Configuration{GDPR: cfgGDPR}, perms, &metricsConf.DummyMetricsEngine{}, analyticsConf.NewPBSAnalytics(&config.Analytics{}))
+func testableEndpoint(perms gdpr.Permissions, cfgGDPR config.GDPR, cfgCCPA config.CCPA) httprouter.Handle {
+	return NewCookieSyncEndpoint(syncersForTest(), &config.Configuration{GDPR: cfgGDPR, CCPA: cfgCCPA}, perms, &metricsConf.DummyMetricsEngine{}, analyticsConf.NewPBSAnalytics(&config.Analytics{}))
 }
 
 func syncersForTest() map[openrtb_ext.BidderName]usersync.Usersyncer {
diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go
index 0eafaa05..c62a6a71 100644
--- a/endpoints/openrtb2/amp_auction_test.go
+++ b/endpoints/openrtb2/amp_auction_test.go
@@ -966,7 +966,7 @@ func getTestBidRequest(nilUser bool, nilExt bool, consentString string, digitrus
 		Imp: []openrtb.Imp{
 			{
 				ID:  "/19968336/header-bid-tag-0",
-				Ext: json.RawMessage(`{"appnexus": { "placementId":10433394 }}`),
+				Ext: json.RawMessage(`{"appnexus": { "placementId":12883451 }}`),
 				Banner: &openrtb.Banner{
 					Format: []openrtb.Format{
 						{
diff --git a/endpoints/openrtb2/auction_benchmark_test.go b/endpoints/openrtb2/auction_benchmark_test.go
index 430b0361..93d7575e 100644
--- a/endpoints/openrtb2/auction_benchmark_test.go
+++ b/endpoints/openrtb2/auction_benchmark_test.go
@@ -21,7 +21,7 @@ import (
 
 // dummyServer returns the header bidding test ad. This response was scraped from a real appnexus server response.
 func dummyServer(w http.ResponseWriter, r *http.Request) {
-	w.Write([]byte(`{"id":"some-request-id","seatbid":[{"bid":[{"id":"4625436751433509010","impid":"my-imp-id","price":0.5,"adm":"\u003cscript type=\"application/javascript\" src=\"http://nym1-ib.adnxs.com/ab?e=wqT_3QKABqAAAwAAAwDWAAUBCM-OiNAFELuV09Pqi86EVRj6t-7QyLin_REqLQkAAAECCOA_EQEHNAAA4D8ZAAAAgOtR4D8hERIAKREJoDDy5vwEOL4HQL4HSAJQ1suTDljhgEhgAGiRQHixhQSAAQGKAQNVU0SSBQbwUpgBrAKgAfoBqAEBsAEAuAECwAEDyAEC0AEA2AEA4AEB8AEAigI6dWYoJ2EnLCA0OTQ0NzIsIDE1MTAwODIzODMpO3VmKCdyJywgMjk2ODExMTAsMh4A8JySAvkBIVR6WGNkQWk2MEljRUVOYkxrdzRZQUNEaGdFZ3dBRGdBUUFSSXZnZFE4dWI4QkZnQVlQX19fXzhQYUFCd0FYZ0JnQUVCaUFFQmtBRUJtQUVCb0FFQnFBRURzQUVBdVFFcGk0aURBQURnUDhFQktZdUlnd0FBNERfSkFTZlJKRUdtbi00XzJRRUFBQUFBQUFEd1AtQUJBUFVCBQ8oSmdDQUtBQ0FMVUMFEARMMAkI8ExNQUNBY2dDQWRBQ0FkZ0NBZUFDQU9nQ0FQZ0NBSUFEQVpBREFKZ0RBYWdEdXRDSEJMb0RDVTVaVFRJNk16STNOdy4umgItITh3aENuZzb8ALg0WUJJSUFRb0FEb0pUbGxOTWpvek1qYzPYAugH4ALH0wHyAhAKBkFEVl9JRBIGNCV1HPICEQoGQ1BHARMcBzE5Nzc5MzMBJwgFQ1AFE_B-ODUxMzU5NIADAYgDAZADAJgDFKADAaoDAMADrALIAwDYAwDgAwDoAwD4AwCABACSBAkvb3BlbnJ0YjKYBACoBACyBAwIABAAGAAgADAAOAC4BADABADIBADSBAlOWU0yOjMyNzfaBAIIAeAEAPAE1suTDogFAZgFAKAF_____wUDXAGqBQ9zb21lLXJlcXVlc3QtaWTABQDJBUmbTPA_0gUJCQAAAAAAAAAA2AUB4AUB\u0026s=61dc0e8770543def5a3a77b4589830d1274b26f1\u0026test=1\u0026pp=${AUCTION_PRICE}\u0026\"\u003e\u003c/script\u003e","adid":"29681110","adomain":["appnexus.com"],"iurl":"http://nym1-ib.adnxs.com/cr?id=29681110","cid":"958","crid":"29681110","w":300,"h":250,"ext":{"bidder":{"appnexus":{"brand_id":1,"auction_id":6127490747252132539,"bidder_id":2}}}}],"seat":"appnexus"}],"ext":{"debug":{"httpcalls":{"appnexus":[{"uri":"http://ib.adnxs.com/openrtb2","requestbody":"{\"id\":\"some-request-id\",\"imp\":[{\"id\":\"my-imp-id\",\"banner\":{\"format\":[{\"w\":300,\"h\":250},{\"w\":300,\"h\":600}]},\"ext\":{\"appnexus\":{\"placement_id\":10433394}}}],\"test\":1,\"tmax\":500}","responsebody":"{\"id\":\"some-request-id\",\"seatbid\":[{\"bid\":[{\"id\":\"4625436751433509010\",\"impid\":\"my-imp-id\",\"price\": 0.500000,\"adid\":\"29681110\",\"adm\":\"\u003cscript type=\\\"application/javascript\\\" src=\\\"http://nym1-ib.adnxs.com/ab?e=wqT_3QKABqAAAwAAAwDWAAUBCM-OiNAFELuV09Pqi86EVRj6t-7QyLin_REqLQkAAAECCOA_EQEHNAAA4D8ZAAAAgOtR4D8hERIAKREJoDDy5vwEOL4HQL4HSAJQ1suTDljhgEhgAGiRQHixhQSAAQGKAQNVU0SSBQbwUpgBrAKgAfoBqAEBsAEAuAECwAEDyAEC0AEA2AEA4AEB8AEAigI6dWYoJ2EnLCA0OTQ0NzIsIDE1MTAwODIzODMpO3VmKCdyJywgMjk2ODExMTAsMh4A8JySAvkBIVR6WGNkQWk2MEljRUVOYkxrdzRZQUNEaGdFZ3dBRGdBUUFSSXZnZFE4dWI4QkZnQVlQX19fXzhQYUFCd0FYZ0JnQUVCaUFFQmtBRUJtQUVCb0FFQnFBRURzQUVBdVFFcGk0aURBQURnUDhFQktZdUlnd0FBNERfSkFTZlJKRUdtbi00XzJRRUFBQUFBQUFEd1AtQUJBUFVCBQ8oSmdDQUtBQ0FMVUMFEARMMAkI8ExNQUNBY2dDQWRBQ0FkZ0NBZUFDQU9nQ0FQZ0NBSUFEQVpBREFKZ0RBYWdEdXRDSEJMb0RDVTVaVFRJNk16STNOdy4umgItITh3aENuZzb8ALg0WUJJSUFRb0FEb0pUbGxOTWpvek1qYzPYAugH4ALH0wHyAhAKBkFEVl9JRBIGNCV1HPICEQoGQ1BHARMcBzE5Nzc5MzMBJwgFQ1AFE_B-ODUxMzU5NIADAYgDAZADAJgDFKADAaoDAMADrALIAwDYAwDgAwDoAwD4AwCABACSBAkvb3BlbnJ0YjKYBACoBACyBAwIABAAGAAgADAAOAC4BADABADIBADSBAlOWU0yOjMyNzfaBAIIAeAEAPAE1suTDogFAZgFAKAF_____wUDXAGqBQ9zb21lLXJlcXVlc3QtaWTABQDJBUmbTPA_0gUJCQAAAAAAAAAA2AUB4AUB\u0026s=61dc0e8770543def5a3a77b4589830d1274b26f1\u0026test=1\u0026pp=${AUCTION_PRICE}\u0026\\\"\u003e\u003c/script\u003e\",\"adomain\":[\"appnexus.com\"],\"iurl\":\"http://nym1-ib.adnxs.com/cr?id=29681110\",\"cid\":\"958\",\"crid\":\"29681110\",\"h\": 250,\"w\": 300,\"ext\":{\"appnexus\":{\"brand_id\": 1,\"auction_id\": 6127490747252132539,\"bidder_id\": 2}}}],\"seat\":\"958\"}],\"bidid\":\"8271358638249766712\",\"cur\":\"USD\"}","status":200}]}},"responsetimemillis":{"appnexus":42}}}`))
+	w.Write([]byte(`{"id":"some-request-id","seatbid":[{"bid":[{"id":"4625436751433509010","impid":"my-imp-id","price":0.5,"adm":"\u003cscript type=\"application/javascript\" src=\"http://nym1-ib.adnxs.com/ab?e=wqT_3QKABqAAAwAAAwDWAAUBCM-OiNAFELuV09Pqi86EVRj6t-7QyLin_REqLQkAAAECCOA_EQEHNAAA4D8ZAAAAgOtR4D8hERIAKREJoDDy5vwEOL4HQL4HSAJQ1suTDljhgEhgAGiRQHixhQSAAQGKAQNVU0SSBQbwUpgBrAKgAfoBqAEBsAEAuAECwAEDyAEC0AEA2AEA4AEB8AEAigI6dWYoJ2EnLCA0OTQ0NzIsIDE1MTAwODIzODMpO3VmKCdyJywgMjk2ODExMTAsMh4A8JySAvkBIVR6WGNkQWk2MEljRUVOYkxrdzRZQUNEaGdFZ3dBRGdBUUFSSXZnZFE4dWI4QkZnQVlQX19fXzhQYUFCd0FYZ0JnQUVCaUFFQmtBRUJtQUVCb0FFQnFBRURzQUVBdVFFcGk0aURBQURnUDhFQktZdUlnd0FBNERfSkFTZlJKRUdtbi00XzJRRUFBQUFBQUFEd1AtQUJBUFVCBQ8oSmdDQUtBQ0FMVUMFEARMMAkI8ExNQUNBY2dDQWRBQ0FkZ0NBZUFDQU9nQ0FQZ0NBSUFEQVpBREFKZ0RBYWdEdXRDSEJMb0RDVTVaVFRJNk16STNOdy4umgItITh3aENuZzb8ALg0WUJJSUFRb0FEb0pUbGxOTWpvek1qYzPYAugH4ALH0wHyAhAKBkFEVl9JRBIGNCV1HPICEQoGQ1BHARMcBzE5Nzc5MzMBJwgFQ1AFE_B-ODUxMzU5NIADAYgDAZADAJgDFKADAaoDAMADrALIAwDYAwDgAwDoAwD4AwCABACSBAkvb3BlbnJ0YjKYBACoBACyBAwIABAAGAAgADAAOAC4BADABADIBADSBAlOWU0yOjMyNzfaBAIIAeAEAPAE1suTDogFAZgFAKAF_____wUDXAGqBQ9zb21lLXJlcXVlc3QtaWTABQDJBUmbTPA_0gUJCQAAAAAAAAAA2AUB4AUB\u0026s=61dc0e8770543def5a3a77b4589830d1274b26f1\u0026test=1\u0026pp=${AUCTION_PRICE}\u0026\"\u003e\u003c/script\u003e","adid":"29681110","adomain":["appnexus.com"],"iurl":"http://nym1-ib.adnxs.com/cr?id=29681110","cid":"958","crid":"29681110","w":300,"h":250,"ext":{"bidder":{"appnexus":{"brand_id":1,"auction_id":6127490747252132539,"bidder_id":2}}}}],"seat":"appnexus"}],"ext":{"debug":{"httpcalls":{"appnexus":[{"uri":"http://ib.adnxs.com/openrtb2","requestbody":"{\"id\":\"some-request-id\",\"imp\":[{\"id\":\"my-imp-id\",\"banner\":{\"format\":[{\"w\":300,\"h\":250},{\"w\":300,\"h\":600}]},\"ext\":{\"appnexus\":{\"placement_id\":12883451}}}],\"test\":1,\"tmax\":500}","responsebody":"{\"id\":\"some-request-id\",\"seatbid\":[{\"bid\":[{\"id\":\"4625436751433509010\",\"impid\":\"my-imp-id\",\"price\": 0.500000,\"adid\":\"29681110\",\"adm\":\"\u003cscript type=\\\"application/javascript\\\" src=\\\"http://nym1-ib.adnxs.com/ab?e=wqT_3QKABqAAAwAAAwDWAAUBCM-OiNAFELuV09Pqi86EVRj6t-7QyLin_REqLQkAAAECCOA_EQEHNAAA4D8ZAAAAgOtR4D8hERIAKREJoDDy5vwEOL4HQL4HSAJQ1suTDljhgEhgAGiRQHixhQSAAQGKAQNVU0SSBQbwUpgBrAKgAfoBqAEBsAEAuAECwAEDyAEC0AEA2AEA4AEB8AEAigI6dWYoJ2EnLCA0OTQ0NzIsIDE1MTAwODIzODMpO3VmKCdyJywgMjk2ODExMTAsMh4A8JySAvkBIVR6WGNkQWk2MEljRUVOYkxrdzRZQUNEaGdFZ3dBRGdBUUFSSXZnZFE4dWI4QkZnQVlQX19fXzhQYUFCd0FYZ0JnQUVCaUFFQmtBRUJtQUVCb0FFQnFBRURzQUVBdVFFcGk0aURBQURnUDhFQktZdUlnd0FBNERfSkFTZlJKRUdtbi00XzJRRUFBQUFBQUFEd1AtQUJBUFVCBQ8oSmdDQUtBQ0FMVUMFEARMMAkI8ExNQUNBY2dDQWRBQ0FkZ0NBZUFDQU9nQ0FQZ0NBSUFEQVpBREFKZ0RBYWdEdXRDSEJMb0RDVTVaVFRJNk16STNOdy4umgItITh3aENuZzb8ALg0WUJJSUFRb0FEb0pUbGxOTWpvek1qYzPYAugH4ALH0wHyAhAKBkFEVl9JRBIGNCV1HPICEQoGQ1BHARMcBzE5Nzc5MzMBJwgFQ1AFE_B-ODUxMzU5NIADAYgDAZADAJgDFKADAaoDAMADrALIAwDYAwDgAwDoAwD4AwCABACSBAkvb3BlbnJ0YjKYBACoBACyBAwIABAAGAAgADAAOAC4BADABADIBADSBAlOWU0yOjMyNzfaBAIIAeAEAPAE1suTDogFAZgFAKAF_____wUDXAGqBQ9zb21lLXJlcXVlc3QtaWTABQDJBUmbTPA_0gUJCQAAAAAAAAAA2AUB4AUB\u0026s=61dc0e8770543def5a3a77b4589830d1274b26f1\u0026test=1\u0026pp=${AUCTION_PRICE}\u0026\\\"\u003e\u003c/script\u003e\",\"adomain\":[\"appnexus.com\"],\"iurl\":\"http://nym1-ib.adnxs.com/cr?id=29681110\",\"cid\":\"958\",\"crid\":\"29681110\",\"h\": 250,\"w\": 300,\"ext\":{\"appnexus\":{\"brand_id\": 1,\"auction_id\": 6127490747252132539,\"bidder_id\": 2}}}],\"seat\":\"958\"}],\"bidid\":\"8271358638249766712\",\"cur\":\"USD\"}","status":200}]}},"responsetimemillis":{"appnexus":42}}}`))
 }
 
 // newDummyRequest returns a request which fetches the header bidding test ad.
@@ -45,7 +45,7 @@ func newDummyRequest() *http.Request {
       },
       "ext": {
         "appnexus": {
-          "placementId": 10433394
+          "placementId": 12883451
         }
       }
     }
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index 7f3d3d4c..89f0fa25 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -88,7 +88,7 @@ func TestExplicitUserId(t *testing.T) {
 				},
 				"ext": {
 					"appnexus": {
-						"placementId": 10433394
+						"placementId": 12883451
 					}
 				}
 			}
@@ -425,7 +425,7 @@ func buildNativeRequest(t *testing.T, nativeData []byte) []byte {
 	buf := bytes.NewBuffer(nil)
 	buf.WriteString(`{"id":"req-id","site":{"page":"some.page.com"},"tmax":500,"imp":[{"id":"some-imp","native":{"request":`)
 	buf.Write(serialized)
-	buf.WriteString(`},"ext":{"appnexus":{"placementId":10433394}}}]}`)
+	buf.WriteString(`},"ext":{"appnexus":{"placementId":12883451}}}]}`)
 	return buf.Bytes()
 }
 
@@ -900,16 +900,52 @@ func TestCurrencyTrunc(t *testing.T) {
 	}
 
 	errL := deps.validateRequest(&req)
-	for _, err := range errL {
-		if err.Error() != "A prebid request can only process one currency. Taking the first currency in the list, USD, as the active currency" {
-			t.Errorf(err.Error())
-		} else {
-			if errortypes.DecodeError(err) != errortypes.WarningCode {
-				t.Errorf("The expected warning has the wrong error type/code")
-			}
-		}
+
+	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})
+}
+
+func TestCCPAInvalidValueWarning(t *testing.T) {
+	deps := &endpointDeps{
+		&nobidExchange{},
+		newParamsValidator(t),
+		&mockStoredReqFetcher{},
+		empty_fetcher.EmptyFetcher{},
+		empty_fetcher.EmptyFetcher{},
+		&config.Configuration{},
+		pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}),
+		analyticsConf.NewPBSAnalytics(&config.Analytics{}),
+		map[string]string{},
+		false,
+		[]byte{},
+		openrtb_ext.BidderMap,
+	}
+
+	ui := uint64(1)
+	req := openrtb.BidRequest{
+		ID: "someID",
+		Imp: []openrtb.Imp{
+			{
+				ID: "imp-ID",
+				Banner: &openrtb.Banner{
+					W: &ui,
+					H: &ui,
+				},
+				Ext: json.RawMessage("{\"appnexus\": {\"placementId\": 5667}}"),
+			},
+		},
+		Site: &openrtb.Site{
+			ID: "myID",
+		},
+		Regs: &openrtb.Regs{
+			Ext: json.RawMessage("{\"us_privacy\":\"invalid by length\"}"),
+		},
 	}
-	assert.Len(t, req.Cur, 1)
+
+	errL := deps.validateRequest(&req)
+
+	expectedError := errortypes.Warning{Message: "CCPA value is invalid and will be ignored. (request.regs.ext.us_privacy must contain 4 characters)"}
+	assert.ElementsMatch(t, errL, []error{&expectedError})
 }
 
 // nobidExchange is a well-behaved exchange which always bids "no bid".
diff --git a/endpoints/openrtb2/interstitial_test.go b/endpoints/openrtb2/interstitial_test.go
index e680478e..1c6eb255 100644
--- a/endpoints/openrtb2/interstitial_test.go
+++ b/endpoints/openrtb2/interstitial_test.go
@@ -22,7 +22,7 @@ var request = &openrtb.BidRequest{
 				},
 			},
 			Instl: 1,
-			Ext:   json.RawMessage(`{"appnexus": {"placementId": 10433394}}`),
+			Ext:   json.RawMessage(`{"appnexus": {"placementId": 12883451}}`),
 		},
 	},
 	Device: &openrtb.Device{
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/prebid-test-ad.json b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/prebid-test-ad.json
index b4fd8693..fc479432 100644
--- a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/prebid-test-ad.json
+++ b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/prebid-test-ad.json
@@ -23,7 +23,7 @@
         },
         "ext": {
           "appnexus": {
-            "placementId": 10433394
+            "placementId": 12883451
           }
         }
       }
diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go
index 6a8e913a..cd870410 100644
--- a/endpoints/openrtb2/video_auction_test.go
+++ b/endpoints/openrtb2/video_auction_test.go
@@ -629,6 +629,10 @@ func TestMergeOpenRTBToVideoRequest(t *testing.T) {
 	videoReq.BCat = []string{"test1", "test2"}
 	videoReq.BAdv = []string{"test3", "test4"}
 
+	videoReq.Regs = &openrtb.Regs{
+		Ext: json.RawMessage(`{"gdpr":1,"us_privacy":"1NYY","existing":"any","consent":"anyConsent"}`),
+	}
+
 	mergeData(videoReq, bidReq)
 
 	assert.Equal(t, videoReq.BCat, bidReq.BCat, "BCat is incorrect")
@@ -641,10 +645,12 @@ func TestMergeOpenRTBToVideoRequest(t *testing.T) {
 	assert.Equal(t, videoReq.Device.DNT, bidReq.Device.DNT, "Device.DNT is incorrect")
 
 	assert.Equal(t, videoReq.Site.Page, bidReq.Site.Page, "Device.Site.Page is incorrect")
+
+	assert.Equal(t, videoReq.Regs, bidReq.Regs, "Regs is incorrect")
 }
 
 func TestHandleError(t *testing.T) {
-	ao := analytics.AuctionObject{
+	vo := analytics.VideoObject{
 		Status: 200,
 		Errors: make([]error, 0),
 	}
@@ -661,14 +667,14 @@ func TestHandleError(t *testing.T) {
 	recorder := httptest.NewRecorder()
 	err1 := errors.New("Error for testing handleError 1")
 	err2 := errors.New("Error for testing handleError 2")
-	handleError(&labels, recorder, []error{err1, err2}, &ao)
+	handleError(&labels, recorder, []error{err1, err2}, &vo)
 
 	assert.Equal(t, pbsmetrics.RequestStatusErr, labels.RequestStatus, "labels.RequestStatus should indicate an error")
 	assert.Equal(t, 500, recorder.Code, "Error status should be written to writer")
-	assert.Equal(t, 500, ao.Status, "AnalyticsObject should have error status")
-	assert.Equal(t, 2, len(ao.Errors), "New errors should be appended to AnalyticsObject Errors")
-	assert.Equal(t, "Error for testing handleError 1", ao.Errors[0].Error(), "Error in AnalyticsObject should have test error message for first error")
-	assert.Equal(t, "Error for testing handleError 2", ao.Errors[1].Error(), "Error in AnalyticsObject should have test error message for second error")
+	assert.Equal(t, 500, vo.Status, "Analytics object should have error status")
+	assert.Equal(t, 2, len(vo.Errors), "New errors should be appended to Analytics object Errors")
+	assert.Equal(t, "Error for testing handleError 1", vo.Errors[0].Error(), "Error in Analytics object should have test error message for first error")
+	assert.Equal(t, "Error for testing handleError 2", vo.Errors[1].Error(), "Error in Analytics object should have test error message for second error")
 }
 
 func TestHandleErrorMetrics(t *testing.T) {
@@ -686,11 +692,11 @@ func TestHandleErrorMetrics(t *testing.T) {
 
 	assert.Equal(t, int64(0), met.RequestStatuses[pbsmetrics.ReqTypeVideo][pbsmetrics.RequestStatusOK].Count(), "OK requests count should be 0")
 	assert.Equal(t, int64(1), met.RequestStatuses[pbsmetrics.ReqTypeVideo][pbsmetrics.RequestStatusErr].Count(), "Error requests count should be 1")
-	assert.Equal(t, 1, len(mod.auctionObjects), "Mock AnalyticsModule should have 1 AuctionObject")
-	assert.Equal(t, 500, mod.auctionObjects[0].Status, "AnalyticsObject should have 500 status")
-	assert.Equal(t, 2, len(mod.auctionObjects[0].Errors), "AnalyticsObject should have Errors length of 2")
-	assert.Equal(t, "request missing required field: PodConfig.DurationRangeSec", mod.auctionObjects[0].Errors[0].Error(), "First error in AnalyticsObject should have message regarding DurationRangeSec")
-	assert.Equal(t, "request missing required field: PodConfig.Pods", mod.auctionObjects[0].Errors[1].Error(), "Second error in AnalyticsObject should have message regarding Pods")
+	assert.Equal(t, 1, len(mod.videoObjects), "Mock AnalyticsModule should have 1 AuctionObject")
+	assert.Equal(t, 500, mod.videoObjects[0].Status, "AnalyticsObject should have 500 status")
+	assert.Equal(t, 2, len(mod.videoObjects[0].Errors), "AnalyticsObject should have Errors length of 2")
+	assert.Equal(t, "request missing required field: PodConfig.DurationRangeSec", mod.videoObjects[0].Errors[0].Error(), "First error in AnalyticsObject should have message regarding DurationRangeSec")
+	assert.Equal(t, "request missing required field: PodConfig.Pods", mod.videoObjects[0].Errors[1].Error(), "Second error in AnalyticsObject should have message regarding Pods")
 }
 
 func mockDepsWithMetrics(t *testing.T, ex *mockExchangeVideo) (*endpointDeps, *pbsmetrics.Metrics, *mockAnalyticsModule) {
@@ -716,12 +722,17 @@ func mockDepsWithMetrics(t *testing.T, ex *mockExchangeVideo) (*endpointDeps, *p
 
 type mockAnalyticsModule struct {
 	auctionObjects []*analytics.AuctionObject
+	videoObjects   []*analytics.VideoObject
 }
 
 func (m *mockAnalyticsModule) LogAuctionObject(ao *analytics.AuctionObject) {
 	m.auctionObjects = append(m.auctionObjects, ao)
 }
 
+func (m *mockAnalyticsModule) LogVideoObject(vo *analytics.VideoObject) {
+	m.videoObjects = append(m.videoObjects, vo)
+}
+
 func (m *mockAnalyticsModule) LogCookieSyncObject(cso *analytics.CookieSyncObject) { return }
 
 func (m *mockAnalyticsModule) LogSetUIDObject(so *analytics.SetUIDObject) { return }
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index 2b0e37e6..31dddae4 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -103,17 +103,17 @@ func TestCharacterEscape(t *testing.T) {
 		Imp: []openrtb.Imp{{
 			ID:     "some-impression-id",
 			Banner: &openrtb.Banner{Format: []openrtb.Format{{W: 300, H: 250}, {W: 300, H: 600}}},
-			Ext:    json.RawMessage(`{"appnexus": {"placementId": 10433394}}`),
+			Ext:    json.RawMessage(`{"appnexus": {"placementId": 1}}`),
 		}},
 		Site:   &openrtb.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)},
 		Device: &openrtb.Device{UA: "curl/7.54.0", IP: "::1"},
 		AT:     1,
 		TMax:   500,
-		Ext:    json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 10433394}}}],"tmax": 500}`),
+		Ext:    json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 1}}}],"tmax": 500}`),
 	}
 
 	//resolvedRequest json.RawMessage
-	resolvedRequest := json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 10433394}}}],"tmax": 500}`)
+	resolvedRequest := json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 1}}}],"tmax": 500}`)
 
 	//adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra,
 	adapterExtra := make(map[openrtb_ext.BidderName]*seatResponseExtra, 1)
@@ -231,7 +231,7 @@ func TestGetBidCacheInfo(t *testing.T) {
 	}
 
 	//resolvedRequest json.RawMessage
-	resolvedRequest := json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 10433394}}}],"tmax": 500}`)
+	resolvedRequest := json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 1}}}],"tmax": 500}`)
 
 	//adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra,
 	adapterExtra := map[openrtb_ext.BidderName]*seatResponseExtra{
@@ -259,7 +259,7 @@ func TestGetBidCacheInfo(t *testing.T) {
         "siteId": 113932,
         "zoneId": 535510
     },
-    "appnexus": { "placementId": 10433394 },
+    "appnexus": { "placementId": 1 },
     "pubmatic": { "publisherId": "156209", "adSlot": "pubmatic_test2@300x250" },
     "pulsepoint": { "cf": "300X250", "cp": 512379, "ct": 486653 },
     "conversant": { "site_id": "108060" },
@@ -314,51 +314,145 @@ func TestGetBidCacheInfo(t *testing.T) {
 	assert.Equal(t, expCacheURL, cacheURL, "[TestGetBidCacheInfo] cacheId field in ext should equal \"%s\" \n", expCacheURL)
 }
 
-func buildBidResponseParams(bidderName openrtb_ext.BidderName, bidRequest *openrtb.BidRequest) ([]openrtb_ext.BidderName, map[openrtb_ext.BidderName]*pbsOrtbSeatBid, json.RawMessage, map[openrtb_ext.BidderName]*seatResponseExtra) {
-	//liveAdapters []openrtb_ext.BidderName,
-	liveAdapters := []openrtb_ext.BidderName{bidderName}
+func TestBidResponseCurrency(t *testing.T) {
+	// Init objects
+	cfg := &config.Configuration{Adapters: make(map[string]config.Adapter, 1)}
+	cfg.Adapters["appnexus"] = config.Adapter{Endpoint: "http://ib.adnxs.com"}
 
-	//adapterBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid,
-	adapterBids := map[openrtb_ext.BidderName]*pbsOrtbSeatBid{
-		bidderName: {
-			bids: []*pbsOrtbBid{
+	handlerNoBidServer := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) }
+	server := httptest.NewServer(http.HandlerFunc(handlerNoBidServer))
+	defer server.Close()
+
+	e := NewExchange(server.Client(), nil, cfg, pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}), adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()), gdpr.AlwaysAllow{}, currencies.NewRateConverterDefault()).(*exchange)
+
+	liveAdapters := make([]openrtb_ext.BidderName, 1)
+	liveAdapters[0] = "appnexus"
+
+	bidRequest := &openrtb.BidRequest{
+		ID: "some-request-id",
+		Imp: []openrtb.Imp{{
+			ID:     "some-impression-id",
+			Banner: &openrtb.Banner{Format: []openrtb.Format{{W: 300, H: 250}, {W: 300, H: 600}}},
+			Ext:    json.RawMessage(`{"appnexus": {"placementId": 10433394}}`),
+		}},
+		Site:   &openrtb.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)},
+		Device: &openrtb.Device{UA: "curl/7.54.0", IP: "::1"},
+		AT:     1,
+		TMax:   500,
+		Ext:    json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 10433394}}}],"tmax": 500}`),
+	}
+
+	resolvedRequest := json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 1}}}],"tmax": 500}`)
+
+	adapterExtra := map[openrtb_ext.BidderName]*seatResponseExtra{
+		"appnexus": {ResponseTimeMillis: 5},
+	}
+
+	var errList []error
+
+	sampleBid := &openrtb.Bid{
+		ID:    "some-imp-id",
+		Price: 9.517803,
+		W:     300,
+		H:     250,
+		Ext:   nil,
+	}
+	aPbsOrtbBidArr := []*pbsOrtbBid{{bid: sampleBid, bidType: openrtb_ext.BidTypeBanner}}
+	sampleSeatBid := []openrtb.SeatBid{
+		{
+			Seat: "appnexus",
+			Bid: []openrtb.Bid{
 				{
-					bid: &openrtb.Bid{
-						ID:    "some-imp-id",
-						Price: 9.517803,
-						W:     300,
-						H:     250,
-					},
-					bidType: openrtb_ext.BidTypeBanner,
-					bidTargets: map[string]string{
-						"pricegranularity":  "med",
-						"includewinners":    "true",
-						"includebidderkeys": "false",
-					},
+					ID:    "some-imp-id",
+					Price: 9.517803,
+					W:     300,
+					H:     250,
+					Ext:   json.RawMessage(`{"prebid":{"type":"banner"}}`),
 				},
 			},
-			currency: "USD",
-			//ext:      bidRequest.Ext,
 		},
 	}
+	emptySeatBid := []openrtb.SeatBid{}
 
-	//resolvedRequest json.RawMessage
-	resolvedRequest := json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 10433394}}}],"tmax": 500}`)
-
-	//adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra,
-	adapterExtra := map[openrtb_ext.BidderName]*seatResponseExtra{
-		bidderName: {
-			ResponseTimeMillis: 5,
-			Errors: []openrtb_ext.ExtBidderError{
-				{
-					Code:    999,
-					Message: "Post ib.adnxs.com/openrtb2?query1&query2: unsupported protocol scheme \"\"",
+	// Test cases
+	type aTest struct {
+		description         string
+		adapterBids         map[openrtb_ext.BidderName]*pbsOrtbSeatBid
+		expectedBidResponse *openrtb.BidResponse
+	}
+	testCases := []aTest{
+		{
+			description: "1) Adapter to bids map comes with a non-empty currency field and non-empty bid array",
+			adapterBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{
+				openrtb_ext.BidderName("appnexus"): {
+					bids:     aPbsOrtbBidArr,
+					currency: "USD",
 				},
 			},
+			expectedBidResponse: &openrtb.BidResponse{
+				ID:      "some-request-id",
+				SeatBid: sampleSeatBid,
+				Cur:     "USD",
+				Ext: json.RawMessage(`{"responsetimemillis":{"appnexus":5},"tmaxrequest":500}
+`),
+			},
+		},
+		{
+			description: "2) Adapter to bids map comes with a non-empty currency field but an empty bid array",
+			adapterBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{
+				openrtb_ext.BidderName("appnexus"): {
+					bids:     nil,
+					currency: "USD",
+				},
+			},
+			expectedBidResponse: &openrtb.BidResponse{
+				ID:      "some-request-id",
+				SeatBid: emptySeatBid,
+				Cur:     "",
+				Ext: json.RawMessage(`{"responsetimemillis":{"appnexus":5},"tmaxrequest":500}
+`),
+			},
+		},
+		{
+			description: "3) Adapter to bids map comes with an empty currency string and a non-empty bid array",
+			adapterBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{
+				openrtb_ext.BidderName("appnexus"): {
+					bids:     aPbsOrtbBidArr,
+					currency: "",
+				},
+			},
+			expectedBidResponse: &openrtb.BidResponse{
+				ID:      "some-request-id",
+				SeatBid: sampleSeatBid,
+				Cur:     "",
+				Ext: json.RawMessage(`{"responsetimemillis":{"appnexus":5},"tmaxrequest":500}
+`),
+			},
+		},
+		{
+			description: "4) Adapter to bids map comes with an empty currency string and an empty bid array",
+			adapterBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{
+				openrtb_ext.BidderName("appnexus"): {
+					bids:     nil,
+					currency: "",
+				},
+			},
+			expectedBidResponse: &openrtb.BidResponse{
+				ID:      "some-request-id",
+				SeatBid: emptySeatBid,
+				Cur:     "",
+				Ext: json.RawMessage(`{"responsetimemillis":{"appnexus":5},"tmaxrequest":500}
+`),
+			},
 		},
 	}
 
-	return liveAdapters, adapterBids, resolvedRequest, adapterExtra
+	// Run tests
+	for i := range testCases {
+		actualBidResp, err := e.buildBidResponse(context.Background(), liveAdapters, testCases[i].adapterBids, bidRequest, resolvedRequest, adapterExtra, nil, errList)
+		assert.NoError(t, err, fmt.Sprintf("[TEST_FAILED] e.buildBidResponse resturns error in test: %s Error message: %s \n", testCases[i].description, err))
+		assert.Equalf(t, testCases[i].expectedBidResponse, actualBidResp, fmt.Sprintf("[TEST_FAILED] Objects must be equal for test: %s \n Expected: >>%s<< \n Actual: >>%s<< ", testCases[i].description, testCases[i].expectedBidResponse.Ext, actualBidResp.Ext))
+	}
 }
 
 // TestRaceIntegration runs an integration test using all the sample params from
@@ -631,7 +725,7 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
 	if len(errs) != 0 {
 		t.Fatalf("%s: Failed to parse aliases", filename)
 	}
-	ex := newExchangeForTests(t, filename, spec.OutgoingRequests, aliases)
+	ex := newExchangeForTests(t, filename, spec.OutgoingRequests, aliases, spec.EnforceCCPA)
 	biddersInAuction := findBiddersInAuction(t, filename, &spec.IncomingRequest.OrtbRequest)
 	categoriesFetcher, error := newCategoryFetcher("./test/category-mapping")
 	if error != nil {
@@ -695,7 +789,7 @@ func extractResponseTimes(t *testing.T, context string, bid *openrtb.BidResponse
 	}
 }
 
-func newExchangeForTests(t *testing.T, filename string, expectations map[string]*bidderSpec, aliases map[string]string) Exchange {
+func newExchangeForTests(t *testing.T, filename string, expectations map[string]*bidderSpec, aliases map[string]string, enforceCCPA bool) Exchange {
 	adapters := make(map[openrtb_ext.BidderName]adaptedBidder)
 	for _, bidderName := range openrtb_ext.BidderMap {
 		if spec, ok := expectations[string(bidderName)]; ok {
@@ -733,6 +827,7 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string]
 		gDPR:                gdpr.AlwaysAllow{},
 		currencyConverter:   currencies.NewRateConverterDefault(),
 		UsersyncIfAmbiguous: false,
+		enforceCCPA:         enforceCCPA,
 	}
 }
 
@@ -1105,6 +1200,7 @@ type exchangeSpec struct {
 	IncomingRequest  exchangeRequest        `json:"incomingRequest"`
 	OutgoingRequests map[string]*bidderSpec `json:"outgoingRequests"`
 	Response         exchangeResponse       `json:"response,omitempty"`
+	EnforceCCPA      bool                   `json:"enforceCcpa"`
 }
 
 type exchangeRequest struct {
diff --git a/exchange/exchangetest/ccpa-featureflag-off.json b/exchange/exchangetest/ccpa-featureflag-off.json
new file mode 100644
index 00000000..d5445ccb
--- /dev/null
+++ b/exchange/exchangetest/ccpa-featureflag-off.json
@@ -0,0 +1,65 @@
+{
+    "enforceCcpa": false,
+    "incomingRequest": {
+        "ortbRequest": {
+            "id": "some-request-id",
+            "site": {
+                "page": "test.somepage.com"
+            },
+            "imp": [{
+                "id": "my-imp-id",
+                "video": {
+                    "mimes": ["video/mp4"]
+                },
+                "ext": {
+                    "appnexus": {
+                        "placementId": 1
+                    }
+                }
+            }],
+            "regs": {
+                "ext": {
+                    "us_privacy": "1-Y-"
+                }
+            },
+            "user": {
+                "buyeruid": "some-buyer-id"
+            }
+        }
+    },
+    "outgoingRequests": {
+        "appnexus": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [{
+                        "id": "my-imp-id",
+                        "video": {
+                            "mimes": ["video/mp4"]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "placementId": 1
+                            }
+                        }
+                    }],
+                    "regs": {
+                        "ext": {
+                            "us_privacy": "1-Y-"
+                        }
+                    },
+                    "user": {
+                        "buyeruid": "some-buyer-id"
+                    }
+                },
+                "bidAdjustment": 1.0
+            },
+            "mockResponse": {
+                "errors": ["appnexus-error"]
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/exchange/exchangetest/ccpa-featureflag-on.json b/exchange/exchangetest/ccpa-featureflag-on.json
new file mode 100644
index 00000000..97a7c3a9
--- /dev/null
+++ b/exchange/exchangetest/ccpa-featureflag-on.json
@@ -0,0 +1,64 @@
+{
+    "enforceCcpa": true,
+    "incomingRequest": {
+        "ortbRequest": {
+            "id": "some-request-id",
+            "site": {
+                "page": "test.somepage.com"
+            },
+            "imp": [{
+                "id": "my-imp-id",
+                "video": {
+                    "mimes": ["video/mp4"]
+                },
+                "ext": {
+                    "appnexus": {
+                        "placementId": 1
+                    }
+                }
+            }],
+            "regs": {
+                "ext": {
+                    "us_privacy": "1-Y-"
+                }
+            },
+            "user": {
+                "buyeruid": "some-buyer-id"
+            }
+        }
+    },
+    "outgoingRequests": {
+        "appnexus": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [{
+                        "id": "my-imp-id",
+                        "video": {
+                            "mimes": ["video/mp4"]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "placementId": 1
+                            }
+                        }
+                    }],
+                    "regs": {
+                        "ext": {
+                            "us_privacy": "1-Y-"
+                        }
+                    },
+                    "user": {
+                    }
+                },
+                "bidAdjustment": 1.0
+            },
+            "mockResponse": {
+                "errors": ["appnexus-error"]
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/exchange/utils_test.go b/exchange/utils_test.go
index 612fc100..edbe04a0 100644
--- a/exchange/utils_test.go
+++ b/exchange/utils_test.go
@@ -41,16 +41,15 @@ func assertReq(t *testing.T, reqByBidders map[openrtb_ext.BidderName]*openrtb.Bi
 	// is used as an alias for appnexus in the test request
 	for bidderName, bidder := range reqByBidders {
 		if !applyCOPPA && consentedVendors[bidderName.String()] {
-			assert.NotEqual(t, bidder.User.BuyerUID, "", "cleanOpenRTBRequest shouldn't clean PI data as per COPPA or for a consented vendor as per GDPR")
-			assert.NotEqual(t, bidder.Device.DIDMD5, "", "cleanOpenRTBRequest shouldn't clean PI data as per COPPA or for a consented vendor as per GDPR")
+			assert.NotEqual(t, bidder.User.BuyerUID, "", "cleanOpenRTBRequest shouldn't clean PI data as per COPPA or for a consented vendor as per GDPR or per CCPA")
+			assert.NotEqual(t, bidder.Device.DIDMD5, "", "cleanOpenRTBRequest shouldn't clean PI data as per COPPA or for a consented vendor as per GDPR or per CCPA")
 		} else {
-			assert.Equal(t, bidder.User.BuyerUID, "", "cleanOpenRTBRequest should clean PI data as per COPPA or for a non-consented vendor as per GDPR ", bidderName.String())
-			assert.Equal(t, bidder.Device.DIDMD5, "", "cleanOpenRTBRequest should clean PI data as per COPPA or for a non-consented vendor as per GDPR", bidderName.String())
+			assert.Equal(t, bidder.User.BuyerUID, "", "cleanOpenRTBRequest should clean PI data as per COPPA or for a non-consented vendor as per GDPR or per CCPA", bidderName.String())
+			assert.Equal(t, bidder.Device.DIDMD5, "", "cleanOpenRTBRequest should clean PI data as per COPPA or for a non-consented vendor as per GDPR or per CCPA", bidderName.String())
 		}
 	}
 }
 
-// Prevents #820
 func TestCleanOpenRTBRequests(t *testing.T) {
 	testCases := []struct {
 		req              *openrtb.BidRequest
@@ -67,7 +66,7 @@ func TestCleanOpenRTBRequests(t *testing.T) {
 	}
 
 	for _, test := range testCases {
-		reqByBidders, _, err := cleanOpenRTBRequests(context.Background(), test.req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true)
+		reqByBidders, _, err := cleanOpenRTBRequests(context.Background(), test.req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, true)
 		if test.hasError {
 			assert.NotNil(t, err, "Error shouldn't be nil")
 		} else {
@@ -77,6 +76,42 @@ func TestCleanOpenRTBRequests(t *testing.T) {
 	}
 }
 
+func TestCleanOpenRTBRequestsCCPA(t *testing.T) {
+	testCases := []struct {
+		description     string
+		enforceCCPA     bool
+		expectDataScrub bool
+	}{
+		{
+			description:     "Feature Flag Enabled",
+			enforceCCPA:     true,
+			expectDataScrub: true,
+		},
+		{
+			description:     "Feature Flag Disabled",
+			enforceCCPA:     false,
+			expectDataScrub: false,
+		},
+	}
+
+	for _, test := range testCases {
+		req := newCCPABidRequest(t)
+
+		results, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, test.enforceCCPA)
+		result := results["appnexus"]
+
+		assert.Nil(t, errs)
+
+		if test.expectDataScrub {
+			assert.Equal(t, result.User.BuyerUID, "", test.description+":User.BuyerUID")
+			assert.Equal(t, result.Device.DIDMD5, "", test.description+":Device.DIDMD5")
+		} else {
+			assert.NotEqual(t, result.User.BuyerUID, "", test.description+":User.BuyerUID")
+			assert.NotEqual(t, result.Device.DIDMD5, "", test.description+":Device.DIDMD5")
+		}
+	}
+}
+
 // newAdapterAliasBidRequest builds a BidRequest with aliases
 func newAdapterAliasBidRequest(t *testing.T) *openrtb.BidRequest {
 	dnt := int8(1)
@@ -118,12 +153,57 @@ func newAdapterAliasBidRequest(t *testing.T) *openrtb.BidRequest {
 					H: 600,
 				}},
 			},
-			Ext: json.RawMessage(`{"appnexus": {"placementId": 10433394},"brightroll": {"placementId": 105}}`),
+			Ext: json.RawMessage(`{"appnexus": {"placementId": 1},"brightroll": {"placementId": 105}}`),
 		}},
 		Ext: json.RawMessage(`{"prebid":{"aliases":{"brightroll":"appnexus"}}}`),
 	}
 }
 
+func newCCPABidRequest(t *testing.T) *openrtb.BidRequest {
+	dnt := int8(1)
+	return &openrtb.BidRequest{
+		Site: &openrtb.Site{
+			Page:   "www.some.domain.com",
+			Domain: "domain.com",
+			Publisher: &openrtb.Publisher{
+				ID: "some-publisher-id",
+			},
+		},
+		Device: &openrtb.Device{
+			DIDMD5:   "some device ID hash",
+			UA:       "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36",
+			IFA:      "ifa",
+			IP:       "132.173.230.74",
+			DNT:      &dnt,
+			Language: "EN",
+		},
+		Source: &openrtb.Source{
+			TID: "61018dc9-fa61-4c41-b7dc-f90b9ae80e87",
+		},
+		User: &openrtb.User{
+			ID:       "our-id",
+			BuyerUID: "their-id",
+			Ext:      json.RawMessage(`{"digitrust":{"id":"digi-id","keyv":1,"pref":1}}`),
+		},
+		Regs: &openrtb.Regs{
+			Ext: json.RawMessage(`{"us_privacy":"1-Y-"}`),
+		},
+		Imp: []openrtb.Imp{{
+			ID: "some-imp-id",
+			Banner: &openrtb.Banner{
+				Format: []openrtb.Format{{
+					W: 300,
+					H: 250,
+				}, {
+					W: 300,
+					H: 600,
+				}},
+			},
+			Ext: json.RawMessage(`{"appnexus": {"placementId": 1}}`),
+		}},
+	}
+}
+
 func TestRandomizeList(t *testing.T) {
 	adapters := make([]openrtb_ext.BidderName, 3)
 	adapters[0] = openrtb_ext.BidderName("dummy")
@@ -144,297 +224,3 @@ func TestRandomizeList(t *testing.T) {
 	}
 
 }
-
-func TestCleanIP(t *testing.T) {
-	testCases := []struct {
-		IP          string
-		cleanedIP   string
-		description string
-	}{
-		{
-			IP:          "0.0.0.0",
-			cleanedIP:   "0.0.0.0",
-			description: "Shouldn't do anything for a 0.0.0.0 IP address",
-		},
-		{
-			IP:          "192.127.111.134",
-			cleanedIP:   "192.127.111.0",
-			description: "Should remove the lowest 8 bits for GDPR/COPPA compliance",
-		},
-		{
-			IP:          "192.127.111.0",
-			cleanedIP:   "192.127.111.0",
-			description: "Shouldn't change anything if the lowest 8 bits are already 0",
-		},
-		{
-			IP:          "not an ip",
-			cleanedIP:   "",
-			description: "Should return an empty string for a bad IP",
-		},
-		{
-			IP:          "",
-			cleanedIP:   "",
-			description: "Should return an empty string for a bad IP",
-		},
-		{
-			IP:          "36278042",
-			cleanedIP:   "",
-			description: "Should return an empty string for a bad IP",
-		},
-	}
-
-	for _, test := range testCases {
-		assert.Equal(t, cleanIP(test.IP), test.cleanedIP, "Should properly remove the last 8 bits of the IP")
-	}
-}
-
-func TestCleanIPV6(t *testing.T) {
-	testCases := []struct {
-		IP          string
-		cleanedIP   string
-		applyGDPR   bool
-		applyCOPPA  bool
-		description string
-	}{
-		{
-			IP:          "0:0:0:0",
-			cleanedIP:   "0:0:0:0",
-			description: "Shouldn't do anything for a 0:0:0:0 IP address",
-		},
-		{
-			IP:          "2001:0db8:0000:0000:0000:ff00:0042:8329",
-			cleanedIP:   "2001:0db8:0000:0000:0000:ff00:0:0",
-			applyCOPPA:  true,
-			description: "Should remove lowest 32 bits for COPPA compliance",
-		},
-		{
-			IP:          "2001:0db8:0000:0000:0000:ff00:0042:8329",
-			cleanedIP:   "2001:0db8:0000:0000:0000:ff00:0042:0",
-			applyGDPR:   true,
-			description: "Should remove lowest 16 bits for GDPR compliance",
-		},
-		{
-			IP:          "2001:0db8:0000:0000:0000:ff00:0042:0",
-			cleanedIP:   "2001:0db8:0000:0000:0000:ff00:0042:0",
-			applyGDPR:   true,
-			description: "Shouldn't do anything if the lowest 16 bits are already 0 for GDPR compliance",
-		},
-		{
-			IP:          "2001:0db8:0000:0000:0000:ff00:0:0",
-			cleanedIP:   "2001:0db8:0000:0000:0000:ff00:0:0",
-			applyGDPR:   true,
-			description: "Shouldn't do anything if the lowest 16 bits are already 0 for GDPR compliance",
-		},
-		{
-			IP:          "2001:0db8:0000:0000:0000:ff00:0042:0",
-			cleanedIP:   "2001:0db8:0000:0000:0000:ff00:0:0",
-			applyCOPPA:  true,
-			description: "Shouldn't do anything if the lowest 32 bits are already 0 for COPPA compliance",
-		},
-		{
-			IP:          "2001:0db8:0000:0000:0000:ff00:0:0",
-			cleanedIP:   "2001:0db8:0000:0000:0000:ff00:0:0",
-			applyCOPPA:  true,
-			description: "Shouldn't do anything if the lowest 32 bits are already 0 for COPPA compliance",
-		},
-		{
-			IP:          "not an ip",
-			cleanedIP:   "",
-			applyCOPPA:  true,
-			description: "Should return an empty string for a bad IP",
-		},
-		{
-			IP:          "not an ip",
-			cleanedIP:   "",
-			applyGDPR:   true,
-			description: "Should return an empty string for a bad IP",
-		},
-		{
-			IP:          "",
-			cleanedIP:   "",
-			applyCOPPA:  true,
-			description: "Should return an empty string for a bad IP",
-		},
-	}
-
-	for _, test := range testCases {
-		assert.Equal(t, cleanIPV6(test.IP, test.applyGDPR, test.applyCOPPA), test.cleanedIP, "Should properly remove the last 8 bits of the IP")
-	}
-}
-
-func TestCleanGeo(t *testing.T) {
-	testCases := []struct {
-		geo         *openrtb.Geo
-		cleanedGeo  *openrtb.Geo
-		applyGDPR   bool
-		applyCOPPA  bool
-		description string
-	}{
-		{
-			geo: &openrtb.Geo{
-				Lat:   123.456,
-				Lon:   678.89,
-				Metro: "some metro",
-				City:  "some city",
-				ZIP:   "some zip",
-			},
-			cleanedGeo: &openrtb.Geo{
-				Lat:   123.46,
-				Lon:   678.89,
-				Metro: "some metro",
-				City:  "some city",
-				ZIP:   "some zip",
-			},
-			applyGDPR:   true,
-			description: "Should only round off Lat and Lon values for GDPR compliance",
-		},
-		{
-			geo: &openrtb.Geo{
-				Lat:   123.456,
-				Lon:   678.89,
-				Metro: "some metro",
-				City:  "some city",
-				ZIP:   "some zip",
-			},
-			cleanedGeo: &openrtb.Geo{
-				Lat:   0,
-				Lon:   0,
-				Metro: "",
-				City:  "",
-				ZIP:   "",
-			},
-			applyCOPPA:  true,
-			description: "Should suppress all Geo values for GDPR compliance",
-		},
-		{
-			geo: &openrtb.Geo{
-				Lat:   123.456,
-				Lon:   678.89,
-				Metro: "some metro",
-				City:  "some city",
-				ZIP:   "some zip",
-			},
-			cleanedGeo: &openrtb.Geo{
-				Lat:   123.456,
-				Lon:   678.89,
-				Metro: "some metro",
-				City:  "some city",
-				ZIP:   "some zip",
-			},
-			applyGDPR:   false,
-			applyCOPPA:  false,
-			description: "Should do nothing if neither GDPR nor COPPA applies",
-		},
-	}
-
-	for _, test := range testCases {
-		cleanedGeo := cleanGeo(test.geo, test.applyGDPR, test.applyCOPPA)
-		assert.Equal(t, cleanedGeo, test.cleanedGeo, test.description)
-	}
-}
-
-func TestApplyRegs(t *testing.T) {
-	bidReqOrig := openrtb.BidRequest{
-		User: &openrtb.User{
-			BuyerUID: "abc123",
-			ID:       "123",
-			Yob:      2050,
-			Gender:   "Female",
-		},
-		Device: &openrtb.Device{
-			DIDMD5:  "teapot",
-			MACSHA1: "someshahash",
-			IP:      "12.123.56.128",
-			IPv6:    "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
-			Geo: &openrtb.Geo{
-				Lat: 123.4567,
-				Lon: 7.9836,
-			},
-		},
-	}
-
-	testCases := []struct {
-		bidReq      openrtb.BidRequest
-		cleanedIP   string
-		cleanedIPv6 string
-		applyCOPPA  bool
-		applyGDPR   bool
-		isAMP       bool
-		description string
-	}{
-		{
-			bidReq:      bidReqOrig,
-			cleanedIP:   "12.123.56.0",
-			cleanedIPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:0",
-			applyGDPR:   true,
-			description: "Should clean recommended personal information for GDPR compliance",
-		},
-		{
-			bidReq:      bidReqOrig,
-			cleanedIP:   "12.123.56.0",
-			cleanedIPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:0",
-			applyGDPR:   true,
-			isAMP:       true,
-			description: "Should clean recommended personal information for GDPR compliance",
-		},
-		{
-			bidReq:      bidReqOrig,
-			cleanedIP:   "12.123.56.0",
-			cleanedIPv6: "2001:0db8:85a3:0000:0000:8a2e:0:0",
-			applyGDPR:   true,
-			isAMP:       true,
-			applyCOPPA:  true,
-			description: "Should clean recommended personal information for GDPR compliance",
-		},
-		{
-			bidReq:      bidReqOrig,
-			cleanedIP:   "12.123.56.0",
-			cleanedIPv6: "2001:0db8:85a3:0000:0000:8a2e:0:0",
-			applyCOPPA:  true,
-			description: "Should clean recommended personal information for COPPA compliance",
-		},
-		{
-			bidReq:      openrtb.BidRequest{},
-			cleanedIP:   "",
-			cleanedIPv6: "",
-			description: "Shouldn't do anything for an empty bid request",
-		},
-	}
-
-	for _, test := range testCases {
-		// Make a shallow copy
-		bidReqCopy := test.bidReq
-		applyRegs(&bidReqCopy, test.isAMP, test.applyGDPR, test.applyCOPPA)
-
-		if bidReqCopy.User != nil {
-			if test.isAMP && !test.applyCOPPA {
-				assert.Equal(t, "abc123", bidReqCopy.User.BuyerUID, test.description)
-			} else {
-				assert.Empty(t, bidReqCopy.User.BuyerUID, test.description)
-			}
-
-			if test.applyCOPPA {
-				assert.Empty(t, bidReqCopy.User.ID, test.description)
-				assert.Empty(t, bidReqCopy.User.Yob, test.description)
-				assert.Empty(t, bidReqCopy.User.Gender, test.description)
-			}
-		}
-
-		if bidReqCopy.Device != nil {
-			if test.applyCOPPA {
-				assert.Empty(t, bidReqCopy.Device.MACSHA1, test.description)
-			}
-			assert.Empty(t, bidReqCopy.Device.DIDMD5, test.description)
-			assert.Equal(t, test.cleanedIP, bidReqCopy.Device.IP, test.description)
-			assert.Equal(t, test.cleanedIPv6, bidReqCopy.Device.IPv6, test.description)
-		}
-
-		// verify original untouched, as we want to only modify the cleaned copy for the bidder
-		assert.Equal(t, "abc123", bidReqOrig.User.BuyerUID)
-		assert.Equal(t, "teapot", bidReqOrig.Device.DIDMD5)
-		assert.Equal(t, "12.123.56.128", bidReqOrig.Device.IP)
-		assert.Equal(t, "2001:0db8:85a3:0000:0000:8a2e:0370:7334", bidReqOrig.Device.IPv6)
-		assert.Equal(t, 123.4567, bidReqOrig.Device.Geo.Lat)
-		assert.Equal(t, 7.9836, bidReqOrig.Device.Geo.Lon)
-	}
-}
diff --git a/pbs/pbsrequest_test.go b/pbs/pbsrequest_test.go
index 3e3b37b9..52cd6153 100644
--- a/pbs/pbsrequest_test.go
+++ b/pbs/pbsrequest_test.go
@@ -199,7 +199,7 @@ var dummyConfig = `
 									"bidder": "appnexus",
 									"bid_id": "22222224",
 									"params": {
-											"placementId": "10433394"
+											"placementId": "1"
 									}
 							}
 					]
diff --git a/privacy/ccpa/ccpa_test.go b/privacy/ccpa/ccpa_test.go
deleted file mode 100644
index 5847dcf0..00000000
--- a/privacy/ccpa/ccpa_test.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package ccpa
-
-import (
-	"encoding/json"
-	"testing"
-
-	"github.com/mxmCherry/openrtb"
-	"github.com/stretchr/testify/assert"
-)
-
-func TestWrite(t *testing.T) {
-	testCases := []struct {
-		description   string
-		policy        Policy
-		request       *openrtb.BidRequest
-		expected      *openrtb.BidRequest
-		expectedError bool
-	}{
-		{
-			description: "Disabled",
-			policy:      Policy{Signal: ""},
-			request:     &openrtb.BidRequest{},
-			expected:    &openrtb.BidRequest{},
-		},
-		{
-			description: "Enabled With Nil Request Regs Object",
-			policy:      Policy{Signal: "anySignal"},
-			request:     &openrtb.BidRequest{},
-			expected: &openrtb.BidRequest{Regs: &openrtb.Regs{
-				Ext: json.RawMessage(`{"us_privacy":"anySignal"}`)}},
-		},
-		{
-			description: "Enabled With Nil Request Regs Ext Object",
-			policy:      Policy{Signal: "anySignal"},
-			request:     &openrtb.BidRequest{Regs: &openrtb.Regs{}},
-			expected: &openrtb.BidRequest{Regs: &openrtb.Regs{
-				Ext: json.RawMessage(`{"us_privacy":"anySignal"}`)}},
-		},
-		{
-			description: "Enabled With Existing Request Regs Ext Object - Doesn't Overwrite",
-			policy:      Policy{Signal: "anySignal"},
-			request: &openrtb.BidRequest{Regs: &openrtb.Regs{
-				Ext: json.RawMessage(`{"existing":"any"}`)}},
-			expected: &openrtb.BidRequest{Regs: &openrtb.Regs{
-				Ext: json.RawMessage(`{"existing":"any","us_privacy":"anySignal"}`)}},
-		},
-		{
-			description: "Enabled With Existing Request Regs Ext Object - Overwrites",
-			policy:      Policy{Signal: "anySignal"},
-			request: &openrtb.BidRequest{Regs: &openrtb.Regs{
-				Ext: json.RawMessage(`{"existing":"any","us_privacy":"toBeOverwritten"}`)}},
-			expected: &openrtb.BidRequest{Regs: &openrtb.Regs{
-				Ext: json.RawMessage(`{"existing":"any","us_privacy":"anySignal"}`)}},
-		},
-		{
-			description: "Enabled With Existing Malformed Request Regs Ext Object",
-			policy:      Policy{Signal: "anySignal"},
-			request: &openrtb.BidRequest{Regs: &openrtb.Regs{
-				Ext: json.RawMessage(`malformed`)}},
-			expectedError: true,
-		},
-	}
-
-	for _, test := range testCases {
-		err := test.policy.Write(test.request)
-
-		if test.expectedError {
-			assert.Error(t, err, test.description)
-		} else {
-			assert.NoError(t, err, test.description)
-			assert.Equal(t, test.expected, test.request, test.description)
-		}
-	}
-}
diff --git a/privacy/ccpa/policy_test.go b/privacy/ccpa/policy_test.go
new file mode 100644
index 00000000..a70874eb
--- /dev/null
+++ b/privacy/ccpa/policy_test.go
@@ -0,0 +1,266 @@
+package ccpa
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/mxmCherry/openrtb"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRead(t *testing.T) {
+	testCases := []struct {
+		description    string
+		request        *openrtb.BidRequest
+		expectedPolicy Policy
+		expectedError  bool
+	}{
+		{
+			description: "Success",
+			request: &openrtb.BidRequest{
+				Regs: &openrtb.Regs{
+					Ext: json.RawMessage(`{"us_privacy":"ABC"}`),
+				},
+			},
+			expectedPolicy: Policy{
+				Value: "ABC",
+			},
+		},
+		{
+			description: "Empty - No Request",
+			request:     nil,
+			expectedPolicy: Policy{
+				Value: "",
+			},
+		},
+		{
+			description: "Empty - No Regs",
+			request: &openrtb.BidRequest{
+				Regs: nil,
+			},
+			expectedPolicy: Policy{
+				Value: "",
+			},
+		},
+		{
+			description: "Empty - No Ext",
+			request: &openrtb.BidRequest{
+				Regs: &openrtb.Regs{},
+			},
+			expectedPolicy: Policy{
+				Value: "",
+			},
+		},
+		{
+			description: "Empty - No Value",
+			request: &openrtb.BidRequest{
+				Regs: &openrtb.Regs{
+					Ext: json.RawMessage(`{"anythingElse":"42"}`),
+				},
+			},
+			expectedPolicy: Policy{
+				Value: "",
+			},
+		},
+		{
+			description: "Serialization Issue",
+			request: &openrtb.BidRequest{
+				Regs: &openrtb.Regs{
+					Ext: json.RawMessage(`malformed`),
+				},
+			},
+			expectedError: true,
+		},
+	}
+
+	for _, test := range testCases {
+
+		p, e := ReadPolicy(test.request)
+
+		if test.expectedError {
+			assert.Error(t, e, test.description)
+		} else {
+			assert.NoError(t, e, test.description)
+		}
+
+		assert.Equal(t, test.expectedPolicy, p, test.description)
+	}
+}
+
+func TestWrite(t *testing.T) {
+	testCases := []struct {
+		description   string
+		policy        Policy
+		request       *openrtb.BidRequest
+		expected      *openrtb.BidRequest
+		expectedError bool
+	}{
+		{
+			description: "Disabled",
+			policy:      Policy{Value: ""},
+			request:     &openrtb.BidRequest{},
+			expected:    &openrtb.BidRequest{},
+		},
+		{
+			description: "Enabled With Nil Request Regs Object",
+			policy:      Policy{Value: "anyValue"},
+			request:     &openrtb.BidRequest{},
+			expected: &openrtb.BidRequest{Regs: &openrtb.Regs{
+				Ext: json.RawMessage(`{"us_privacy":"anyValue"}`)}},
+		},
+		{
+			description: "Enabled With Nil Request Regs Ext Object",
+			policy:      Policy{Value: "anyValue"},
+			request:     &openrtb.BidRequest{Regs: &openrtb.Regs{}},
+			expected: &openrtb.BidRequest{Regs: &openrtb.Regs{
+				Ext: json.RawMessage(`{"us_privacy":"anyValue"}`)}},
+		},
+		{
+			description: "Enabled With Existing Request Regs Ext Object - Doesn't Overwrite",
+			policy:      Policy{Value: "anyValue"},
+			request: &openrtb.BidRequest{Regs: &openrtb.Regs{
+				Ext: json.RawMessage(`{"existing":"any"}`)}},
+			expected: &openrtb.BidRequest{Regs: &openrtb.Regs{
+				Ext: json.RawMessage(`{"existing":"any","us_privacy":"anyValue"}`)}},
+		},
+		{
+			description: "Enabled With Existing Request Regs Ext Object - Overwrites",
+			policy:      Policy{Value: "anyValue"},
+			request: &openrtb.BidRequest{Regs: &openrtb.Regs{
+				Ext: json.RawMessage(`{"existing":"any","us_privacy":"toBeOverwritten"}`)}},
+			expected: &openrtb.BidRequest{Regs: &openrtb.Regs{
+				Ext: json.RawMessage(`{"existing":"any","us_privacy":"anyValue"}`)}},
+		},
+		{
+			description: "Enabled With Existing Malformed Request Regs Ext Object",
+			policy:      Policy{Value: "anyValue"},
+			request: &openrtb.BidRequest{Regs: &openrtb.Regs{
+				Ext: json.RawMessage(`malformed`)}},
+			expectedError: true,
+		},
+	}
+
+	for _, test := range testCases {
+		err := test.policy.Write(test.request)
+
+		if test.expectedError {
+			assert.Error(t, err, test.description)
+		} else {
+			assert.NoError(t, err, test.description)
+			assert.Equal(t, test.expected, test.request, test.description)
+		}
+	}
+}
+
+func TestValidate(t *testing.T) {
+	testCases := []struct {
+		description string
+		policy      Policy
+		expected    string
+	}{
+		{
+			description: "Valid",
+			policy:      Policy{Value: "1NYN"},
+			expected:    "",
+		},
+		{
+			description: "Valid - Not Applicable",
+			policy:      Policy{Value: "1---"},
+			expected:    "",
+		},
+		{
+			description: "Valid - Empty",
+			policy:      Policy{Value: ""},
+			expected:    "",
+		},
+		{
+			description: "Invalid Length",
+			policy:      Policy{Value: "1NY"},
+			expected:    "request.regs.ext.us_privacy must contain 4 characters",
+		},
+		{
+			description: "Invalid Version",
+			policy:      Policy{Value: "2---"},
+			expected:    "request.regs.ext.us_privacy must specify version 1",
+		},
+		{
+			description: "Invalid Explicit Notice Char",
+			policy:      Policy{Value: "1X--"},
+			expected:    "request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the explicit notice",
+		},
+		{
+			description: "Invalid Explicit Notice Case",
+			policy:      Policy{Value: "1y--"},
+			expected:    "request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the explicit notice",
+		},
+		{
+			description: "Invalid Opt-Out Sale Char",
+			policy:      Policy{Value: "1-X-"},
+			expected:    "request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the opt-out sale",
+		},
+		{
+			description: "Invalid Opt-Out Sale Case",
+			policy:      Policy{Value: "1-y-"},
+			expected:    "request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the opt-out sale",
+		},
+		{
+			description: "Invalid LSPA Char",
+			policy:      Policy{Value: "1--X"},
+			expected:    "request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the limited service provider agreement",
+		},
+		{
+			description: "Invalid LSPA Case",
+			policy:      Policy{Value: "1--y"},
+			expected:    "request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the limited service provider agreement",
+		},
+	}
+
+	for _, test := range testCases {
+		result := test.policy.Validate()
+
+		if test.expected == "" {
+			assert.NoError(t, result, test.description)
+		} else {
+			assert.EqualError(t, result, test.expected, test.description)
+		}
+	}
+}
+
+func TestShouldEnforce(t *testing.T) {
+	testCases := []struct {
+		description string
+		policy      Policy
+		expected    bool
+	}{
+		{
+			description: "Enforceable",
+			policy:      Policy{Value: "1-Y-"},
+			expected:    true,
+		},
+		{
+			description: "Not Enforceable - Not Present",
+			policy:      Policy{Value: ""},
+			expected:    false,
+		},
+		{
+			description: "Not Enforceable - Opt-Out Unknown",
+			policy:      Policy{Value: "1---"},
+			expected:    false,
+		},
+		{
+			description: "Not Enforceable - Opt-Out Explicitly No",
+			policy:      Policy{Value: "1-N-"},
+			expected:    false,
+		},
+		{
+			description: "Invalid",
+			policy:      Policy{Value: "2---"},
+			expected:    false,
+		},
+	}
+
+	for _, test := range testCases {
+		result := test.policy.ShouldEnforce()
+		assert.Equal(t, test.expected, result, test.description)
+	}
+}
diff --git a/privacy/enforcement_test.go b/privacy/enforcement_test.go
new file mode 100644
index 00000000..ffc9da5d
--- /dev/null
+++ b/privacy/enforcement_test.go
@@ -0,0 +1,214 @@
+package privacy
+
+import (
+	"testing"
+
+	"github.com/mxmCherry/openrtb"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+)
+
+func TestAny(t *testing.T) {
+	testCases := []struct {
+		enforcement Enforcement
+		expected    bool
+		description string
+	}{
+		{
+			enforcement: Enforcement{
+				CCPA:  false,
+				COPPA: false,
+				GDPR:  false,
+			},
+			expected:    false,
+			description: "All False",
+		},
+		{
+			enforcement: Enforcement{
+				CCPA:  true,
+				COPPA: true,
+				GDPR:  true,
+			},
+			expected:    true,
+			description: "All True",
+		},
+		{
+			enforcement: Enforcement{
+				CCPA:  false,
+				COPPA: true,
+				GDPR:  false,
+			},
+			expected:    true,
+			description: "Mixed",
+		},
+	}
+
+	for _, test := range testCases {
+		result := test.enforcement.Any()
+		assert.Equal(t, test.expected, result, test.description)
+	}
+}
+
+func TestApply(t *testing.T) {
+	testCases := []struct {
+		enforcement             Enforcement
+		isAMP                   bool
+		expectedDeviceMacAndIFA bool
+		expectedDeviceIPv6      ScrubStrategyIPV6
+		expectedDeviceGeo       ScrubStrategyGeo
+		expectedUser            ScrubStrategyUser
+		expectedUserGeo         ScrubStrategyGeo
+		description             string
+	}{
+		{
+			enforcement: Enforcement{
+				CCPA:  true,
+				COPPA: true,
+				GDPR:  true,
+			},
+			isAMP:                   true,
+			expectedDeviceMacAndIFA: true,
+			expectedDeviceIPv6:      ScrubStrategyIPV6Lowest32,
+			expectedDeviceGeo:       ScrubStrategyGeoFull,
+			expectedUser:            ScrubStrategyUserFull,
+			expectedUserGeo:         ScrubStrategyGeoFull,
+			description:             "All Enforced - Most Strict",
+		},
+		{
+			enforcement: Enforcement{
+				CCPA:  false,
+				COPPA: true,
+				GDPR:  false,
+			},
+			isAMP:                   false,
+			expectedDeviceMacAndIFA: true,
+			expectedDeviceIPv6:      ScrubStrategyIPV6Lowest32,
+			expectedDeviceGeo:       ScrubStrategyGeoFull,
+			expectedUser:            ScrubStrategyUserFull,
+			expectedUserGeo:         ScrubStrategyGeoFull,
+			description:             "COPPA",
+		},
+		{
+			enforcement: Enforcement{
+				CCPA:  false,
+				COPPA: false,
+				GDPR:  true,
+			},
+			isAMP:                   false,
+			expectedDeviceMacAndIFA: false,
+			expectedDeviceIPv6:      ScrubStrategyIPV6Lowest16,
+			expectedDeviceGeo:       ScrubStrategyGeoReducedPrecision,
+			expectedUser:            ScrubStrategyUserBuyerIDOnly,
+			expectedUserGeo:         ScrubStrategyGeoReducedPrecision,
+			description:             "GDPR",
+		},
+		{
+			enforcement: Enforcement{
+				CCPA:  false,
+				COPPA: false,
+				GDPR:  true,
+			},
+			isAMP:                   true,
+			expectedDeviceMacAndIFA: false,
+			expectedDeviceIPv6:      ScrubStrategyIPV6Lowest16,
+			expectedDeviceGeo:       ScrubStrategyGeoReducedPrecision,
+			expectedUser:            ScrubStrategyUserNone,
+			expectedUserGeo:         ScrubStrategyGeoReducedPrecision,
+			description:             "GDPR For AMP",
+		},
+		{
+			enforcement: Enforcement{
+				CCPA:  true,
+				COPPA: false,
+				GDPR:  false,
+			},
+			isAMP:                   false,
+			expectedDeviceMacAndIFA: false,
+			expectedDeviceIPv6:      ScrubStrategyIPV6Lowest16,
+			expectedDeviceGeo:       ScrubStrategyGeoReducedPrecision,
+			expectedUser:            ScrubStrategyUserBuyerIDOnly,
+			expectedUserGeo:         ScrubStrategyGeoReducedPrecision,
+			description:             "CCPA",
+		},
+		{
+			enforcement: Enforcement{
+				CCPA:  true,
+				COPPA: false,
+				GDPR:  false,
+			},
+			isAMP:                   true,
+			expectedDeviceMacAndIFA: false,
+			expectedDeviceIPv6:      ScrubStrategyIPV6Lowest16,
+			expectedDeviceGeo:       ScrubStrategyGeoReducedPrecision,
+			expectedUser:            ScrubStrategyUserBuyerIDOnly,
+			expectedUserGeo:         ScrubStrategyGeoReducedPrecision,
+			description:             "CCPA For AMP",
+		},
+		{
+			enforcement: Enforcement{
+				CCPA:  true,
+				COPPA: false,
+				GDPR:  true,
+			},
+			isAMP:                   true,
+			expectedDeviceMacAndIFA: false,
+			expectedDeviceIPv6:      ScrubStrategyIPV6Lowest16,
+			expectedDeviceGeo:       ScrubStrategyGeoReducedPrecision,
+			expectedUser:            ScrubStrategyUserNone,
+			expectedUserGeo:         ScrubStrategyGeoReducedPrecision,
+			description:             "GDPR And CCPA For AMP",
+		},
+	}
+
+	for _, test := range testCases {
+		req := &openrtb.BidRequest{
+			Device: &openrtb.Device{DIDSHA1: "before"},
+			User:   &openrtb.User{ID: "before"},
+		}
+		device := &openrtb.Device{DIDSHA1: "after"}
+		user := &openrtb.User{ID: "after"}
+
+		m := &mockScrubber{}
+		m.On("ScrubDevice", req.Device, test.expectedDeviceMacAndIFA, test.expectedDeviceIPv6, test.expectedDeviceGeo).Return(device).Once()
+		m.On("ScrubUser", req.User, test.expectedUser, test.expectedUserGeo).Return(user).Once()
+
+		test.enforcement.apply(req, test.isAMP, m)
+
+		m.AssertExpectations(t)
+		assert.Equal(t, device, req.Device, "Device Set Correctly")
+		assert.Equal(t, user, req.User, "User Set Correctly")
+	}
+}
+
+func TestApplyNoneApplicable(t *testing.T) {
+	enforcement := Enforcement{}
+	device := &openrtb.Device{DIDSHA1: "original"}
+	user := &openrtb.User{ID: "original"}
+	req := &openrtb.BidRequest{
+		Device: device,
+		User:   user,
+	}
+
+	m := &mockScrubber{}
+
+	enforcement.apply(req, true, m)
+
+	m.AssertNotCalled(t, "ScrubDevice")
+	m.AssertNotCalled(t, "ScrubUser")
+	assert.Equal(t, device, req.Device, "Device Set Correctly")
+	assert.Equal(t, user, req.User, "User Set Correctly")
+}
+
+type mockScrubber struct {
+	mock.Mock
+}
+
+func (m *mockScrubber) ScrubDevice(device *openrtb.Device, macAndIFA bool, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb.Device {
+	args := m.Called(device, macAndIFA, ipv6, geo)
+	return args.Get(0).(*openrtb.Device)
+}
+
+func (m *mockScrubber) ScrubUser(user *openrtb.User, strategy ScrubStrategyUser, geo ScrubStrategyGeo) *openrtb.User {
+	args := m.Called(user, strategy, geo)
+	return args.Get(0).(*openrtb.User)
+}
diff --git a/privacy/scrubber_test.go b/privacy/scrubber_test.go
new file mode 100644
index 00000000..168fb5fb
--- /dev/null
+++ b/privacy/scrubber_test.go
@@ -0,0 +1,434 @@
+package privacy
+
+import (
+	"testing"
+
+	"github.com/mxmCherry/openrtb"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestScrubDevice(t *testing.T) {
+	device := &openrtb.Device{
+		DIDMD5:   "anyDIDMD5",
+		DIDSHA1:  "anyDIDSHA1",
+		DPIDMD5:  "anyDPIDMD5",
+		DPIDSHA1: "anyDPIDSHA1",
+		MACSHA1:  "anyMACSHA1",
+		MACMD5:   "anyMACMD5",
+		IFA:      "anyIFA",
+		IP:       "1.2.3.4",
+		IPv6:     "2001:0db8:0000:0000:0000:ff00:0042:8329",
+		Geo: &openrtb.Geo{
+			Lat:   123.456,
+			Lon:   678.89,
+			Metro: "some metro",
+			City:  "some city",
+			ZIP:   "some zip",
+		},
+	}
+
+	testCases := []struct {
+		expected    *openrtb.Device
+		isMacAndIFA bool
+		ipv6        ScrubStrategyIPV6
+		geo         ScrubStrategyGeo
+		description string
+	}{
+		{
+			expected: &openrtb.Device{
+				DIDMD5:   "",
+				DIDSHA1:  "",
+				DPIDMD5:  "",
+				DPIDSHA1: "",
+				MACSHA1:  "",
+				MACMD5:   "",
+				IFA:      "",
+				IP:       "1.2.3.0",
+				IPv6:     "2001:0db8:0000:0000:0000:ff00:0:0",
+				Geo:      &openrtb.Geo{},
+			},
+			isMacAndIFA: true,
+			ipv6:        ScrubStrategyIPV6Lowest32,
+			geo:         ScrubStrategyGeoFull,
+			description: "Full Scrubbing",
+		},
+		{
+			expected: &openrtb.Device{
+				DIDMD5:   "",
+				DIDSHA1:  "",
+				DPIDMD5:  "",
+				DPIDSHA1: "",
+				MACSHA1:  "",
+				MACMD5:   "",
+				IFA:      "",
+				IP:       "1.2.3.0",
+				IPv6:     "2001:0db8:0000:0000:0000:ff00:0042:0",
+				Geo:      &openrtb.Geo{},
+			},
+			isMacAndIFA: true,
+			ipv6:        ScrubStrategyIPV6Lowest16,
+			geo:         ScrubStrategyGeoFull,
+			description: "IPv6 Lowest 16",
+		},
+		{
+			expected: &openrtb.Device{
+				DIDMD5:   "",
+				DIDSHA1:  "",
+				DPIDMD5:  "",
+				DPIDSHA1: "",
+				MACSHA1:  "",
+				MACMD5:   "",
+				IFA:      "",
+				IP:       "1.2.3.0",
+				IPv6:     "2001:0db8:0000:0000:0000:ff00:0042:8329",
+				Geo:      &openrtb.Geo{},
+			},
+			isMacAndIFA: true,
+			ipv6:        ScrubStrategyIPV6None,
+			geo:         ScrubStrategyGeoFull,
+			description: "IPv6 None",
+		},
+		{
+			expected: &openrtb.Device{
+				DIDMD5:   "",
+				DIDSHA1:  "",
+				DPIDMD5:  "",
+				DPIDSHA1: "",
+				MACSHA1:  "",
+				MACMD5:   "",
+				IFA:      "",
+				IP:       "1.2.3.0",
+				IPv6:     "2001:0db8:0000:0000:0000:ff00:0:0",
+				Geo: &openrtb.Geo{
+					Lat:   123.46,
+					Lon:   678.89,
+					Metro: "some metro",
+					City:  "some city",
+					ZIP:   "some zip",
+				},
+			},
+			isMacAndIFA: true,
+			ipv6:        ScrubStrategyIPV6Lowest32,
+			geo:         ScrubStrategyGeoReducedPrecision,
+			description: "Geo Reduced Precision",
+		},
+		{
+			expected: &openrtb.Device{
+				DIDMD5:   "",
+				DIDSHA1:  "",
+				DPIDMD5:  "",
+				DPIDSHA1: "",
+				MACSHA1:  "",
+				MACMD5:   "",
+				IFA:      "",
+				IP:       "1.2.3.0",
+				IPv6:     "2001:0db8:0000:0000:0000:ff00:0:0",
+				Geo: &openrtb.Geo{
+					Lat:   123.456,
+					Lon:   678.89,
+					Metro: "some metro",
+					City:  "some city",
+					ZIP:   "some zip",
+				},
+			},
+			isMacAndIFA: true,
+			ipv6:        ScrubStrategyIPV6Lowest32,
+			geo:         ScrubStrategyGeoNone,
+			description: "Geo None",
+		},
+		{
+			expected: &openrtb.Device{
+				DIDMD5:   "",
+				DIDSHA1:  "",
+				DPIDMD5:  "",
+				DPIDSHA1: "",
+				MACSHA1:  "anyMACSHA1",
+				MACMD5:   "anyMACMD5",
+				IFA:      "anyIFA",
+				IP:       "1.2.3.0",
+				IPv6:     "2001:0db8:0000:0000:0000:ff00:0:0",
+				Geo:      &openrtb.Geo{},
+			},
+			isMacAndIFA: false,
+			ipv6:        ScrubStrategyIPV6Lowest32,
+			geo:         ScrubStrategyGeoFull,
+			description: "Without MAC Address And IFA Scrubbing",
+		},
+	}
+
+	for _, test := range testCases {
+		result := NewScrubber().ScrubDevice(device, test.isMacAndIFA, test.ipv6, test.geo)
+		assert.Equal(t, test.expected, result, test.description)
+	}
+}
+
+func TestScrubUser(t *testing.T) {
+	user := &openrtb.User{
+		BuyerUID: "anyBuyerUID",
+		ID:       "anyID",
+		Yob:      42,
+		Gender:   "anyGender",
+		Geo: &openrtb.Geo{
+			Lat:   123.456,
+			Lon:   678.89,
+			Metro: "some metro",
+			City:  "some city",
+			ZIP:   "some zip",
+		},
+	}
+
+	testCases := []struct {
+		expected    *openrtb.User
+		strategy    ScrubStrategyUser
+		geo         ScrubStrategyGeo
+		description string
+	}{
+		{
+			expected: &openrtb.User{
+				BuyerUID: "",
+				ID:       "",
+				Yob:      0,
+				Gender:   "",
+				Geo:      &openrtb.Geo{},
+			},
+			strategy:    ScrubStrategyUserFull,
+			geo:         ScrubStrategyGeoFull,
+			description: "Full Scrubbing",
+		},
+		{
+			expected: &openrtb.User{
+				BuyerUID: "",
+				ID:       "anyID",
+				Yob:      42,
+				Gender:   "anyGender",
+				Geo:      &openrtb.Geo{},
+			},
+			strategy:    ScrubStrategyUserBuyerIDOnly,
+			geo:         ScrubStrategyGeoFull,
+			description: "User Buyer ID Only",
+		},
+		{
+			expected: &openrtb.User{
+				BuyerUID: "anyBuyerUID",
+				ID:       "anyID",
+				Yob:      42,
+				Gender:   "anyGender",
+				Geo:      &openrtb.Geo{},
+			},
+			strategy:    ScrubStrategyUserNone,
+			geo:         ScrubStrategyGeoFull,
+			description: "User None",
+		},
+		{
+			expected: &openrtb.User{
+				BuyerUID: "",
+				ID:       "",
+				Yob:      0,
+				Gender:   "",
+				Geo: &openrtb.Geo{
+					Lat:   123.46,
+					Lon:   678.89,
+					Metro: "some metro",
+					City:  "some city",
+					ZIP:   "some zip",
+				},
+			},
+			strategy:    ScrubStrategyUserFull,
+			geo:         ScrubStrategyGeoReducedPrecision,
+			description: "Geo Reduced Precision",
+		},
+		{
+			expected: &openrtb.User{
+				BuyerUID: "",
+				ID:       "",
+				Yob:      0,
+				Gender:   "",
+				Geo: &openrtb.Geo{
+					Lat:   123.456,
+					Lon:   678.89,
+					Metro: "some metro",
+					City:  "some city",
+					ZIP:   "some zip",
+				},
+			},
+			strategy:    ScrubStrategyUserFull,
+			geo:         ScrubStrategyGeoNone,
+			description: "Geo None",
+		},
+	}
+
+	for _, test := range testCases {
+		result := NewScrubber().ScrubUser(user, test.strategy, test.geo)
+		assert.Equal(t, test.expected, result, test.description)
+	}
+}
+
+func TestScrubIPV4(t *testing.T) {
+	testCases := []struct {
+		IP          string
+		cleanedIP   string
+		description string
+	}{
+		{
+			IP:          "0.0.0.0",
+			cleanedIP:   "0.0.0.0",
+			description: "Shouldn't do anything for a 0.0.0.0 IP address",
+		},
+		{
+			IP:          "192.127.111.134",
+			cleanedIP:   "192.127.111.0",
+			description: "Should remove the lowest 8 bits",
+		},
+		{
+			IP:          "192.127.111.0",
+			cleanedIP:   "192.127.111.0",
+			description: "Shouldn't change anything if the lowest 8 bits are already 0",
+		},
+		{
+			IP:          "not an ip",
+			cleanedIP:   "",
+			description: "Should return an empty string for a bad IP",
+		},
+		{
+			IP:          "",
+			cleanedIP:   "",
+			description: "Should return an empty string for a bad IP",
+		},
+	}
+
+	for _, test := range testCases {
+		result := scrubIPV4(test.IP)
+		assert.Equal(t, test.cleanedIP, result, test.description)
+	}
+}
+
+func TestScrubIPV6Lowest16Bits(t *testing.T) {
+	testCases := []struct {
+		IP          string
+		cleanedIP   string
+		description string
+	}{
+		{
+			IP:          "0:0:0:0",
+			cleanedIP:   "0:0:0:0",
+			description: "Shouldn't do anything for a 0:0:0:0 IP address",
+		},
+		{
+			IP:          "2001:0db8:0000:0000:0000:ff00:0042:8329",
+			cleanedIP:   "2001:0db8:0000:0000:0000:ff00:0042:0",
+			description: "Should remove lowest 16 bits",
+		},
+		{
+			IP:          "2001:0db8:0000:0000:0000:ff00:0042:0",
+			cleanedIP:   "2001:0db8:0000:0000:0000:ff00:0042:0",
+			description: "Shouldn't do anything if the lowest 16 bits are already 0",
+		},
+		{
+			IP:          "not an ip",
+			cleanedIP:   "",
+			description: "Should return an empty string for a bad IP",
+		},
+		{
+			IP:          "",
+			cleanedIP:   "",
+			description: "Should return an empty string for a bad IP",
+		},
+	}
+
+	for _, test := range testCases {
+		result := scrubIPV6Lowest16Bits(test.IP)
+		assert.Equal(t, test.cleanedIP, result, test.description)
+	}
+}
+
+func TestScrubIPV6Lowest32Bits(t *testing.T) {
+	testCases := []struct {
+		IP          string
+		cleanedIP   string
+		description string
+	}{
+		{
+			IP:          "0:0:0:0",
+			cleanedIP:   "0:0:0:0",
+			description: "Shouldn't do anything for a 0:0:0:0 IP address",
+		},
+		{
+			IP:          "2001:0db8:0000:0000:0000:ff00:0042:8329",
+			cleanedIP:   "2001:0db8:0000:0000:0000:ff00:0:0",
+			description: "Should remove lowest 32 bits",
+		},
+		{
+			IP:          "2001:0db8:0000:0000:0000:ff00:0:0",
+			cleanedIP:   "2001:0db8:0000:0000:0000:ff00:0:0",
+			description: "Shouldn't do anything if the lowest 32 bits are already 0",
+		},
+
+		{
+			IP:          "not an ip",
+			cleanedIP:   "",
+			description: "Should return an empty string for a bad IP",
+		},
+		{
+			IP:          "",
+			cleanedIP:   "",
+			description: "Should return an empty string for a bad IP",
+		},
+	}
+
+	for _, test := range testCases {
+		result := scrubIPV6Lowest32Bits(test.IP)
+		assert.Equal(t, test.cleanedIP, result, test.description)
+	}
+}
+
+func TestScrubGeoFull(t *testing.T) {
+	geo := &openrtb.Geo{
+		Lat:   123.456,
+		Lon:   678.89,
+		Metro: "some metro",
+		City:  "some city",
+		ZIP:   "some zip",
+	}
+	geoExpected := &openrtb.Geo{
+		Lat:   0,
+		Lon:   0,
+		Metro: "",
+		City:  "",
+		ZIP:   "",
+	}
+
+	result := scrubGeoFull(geo)
+
+	assert.Equal(t, geoExpected, result)
+}
+
+func TestScrubGeoFullWhenNil(t *testing.T) {
+	result := scrubGeoFull(nil)
+	assert.Nil(t, result)
+}
+
+func TestScrubGeoPrecision(t *testing.T) {
+	geo := &openrtb.Geo{
+		Lat:   123.456,
+		Lon:   678.89,
+		Metro: "some metro",
+		City:  "some city",
+		ZIP:   "some zip",
+	}
+	geoExpected := &openrtb.Geo{
+		Lat:   123.46,
+		Lon:   678.89,
+		Metro: "some metro",
+		City:  "some city",
+		ZIP:   "some zip",
+	}
+
+	result := scrubGeoPrecision(geo)
+
+	assert.Equal(t, geoExpected, result)
+}
+
+func TestScrubGeoPrecisionWhenNil(t *testing.T) {
+	result := scrubGeoPrecision(nil)
+	assert.Nil(t, result)
+}
diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go
index 54a59434..dd2b5b5d 100644
--- a/usersync/usersyncers/syncer_test.go
+++ b/usersync/usersyncers/syncer_test.go
@@ -39,6 +39,7 @@ func TestNewSyncerMap(t *testing.T) {
 			string(openrtb_ext.BidderIx):               syncConfig,
 			string(openrtb_ext.BidderLifestreet):       syncConfig,
 			string(openrtb_ext.BidderLockerDome):       syncConfig,
+			string(openrtb_ext.BidderMarsmedia):        syncConfig,
 			string(openrtb_ext.BidderMgid):             syncConfig,
 			string(openrtb_ext.BidderOpenx):            syncConfig,
 			string(openrtb_ext.BidderPubmatic):         syncConfig,
@@ -62,6 +63,7 @@ func TestNewSyncerMap(t *testing.T) {
 	}
 
 	adaptersWithoutSyncers := map[openrtb_ext.BidderName]bool{
+		openrtb_ext.BidderApplogy:   true,
 		openrtb_ext.BidderTappx:     true,
 		openrtb_ext.BidderKubient:   true,
 		openrtb_ext.BidderPubnative: true,