Repository: prebid/prebid-server
Pull request #1484 (2020-09-08 21:57:36)

Update conversant adapter for new prebid-server interface
Update the conversant adapter to the new interface


Patch:
diff --git a/adapters/consumable/consumable.go b/adapters/consumable/consumable.go
index ff7451f1..18ece8d4 100644
--- a/adapters/consumable/consumable.go
+++ b/adapters/consumable/consumable.go
@@ -3,15 +3,16 @@ package consumable
 import (
 	"encoding/json"
 	"fmt"
+	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+
 	"github.com/mxmCherry/openrtb"
 	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/privacy/ccpa"
-	"net/http"
-	"net/url"
-	"strconv"
-	"strings"
 )
 
 type ConsumableAdapter struct {
@@ -136,9 +137,9 @@ func (a *ConsumableAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *a
 
 	gdpr := bidGdpr{}
 
-	ccpaPolicy, err := ccpa.ReadPolicy(request)
+	ccpaPolicy, err := ccpa.ReadFromRequest(request)
 	if err == nil {
-		body.CCPA = ccpaPolicy.Value
+		body.CCPA = ccpaPolicy.Consent
 	}
 
 	// TODO: Replace with gdpr.ReadPolicy when it is available
diff --git a/adapters/conversant/cnvr_legacy.go b/adapters/conversant/cnvr_legacy.go
new file mode 100644
index 00000000..bd121f09
--- /dev/null
+++ b/adapters/conversant/cnvr_legacy.go
@@ -0,0 +1,291 @@
+package conversant
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+
+	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/pbs"
+	"golang.org/x/net/context/ctxhttp"
+)
+
+type ConversantLegacyAdapter struct {
+	http *adapters.HTTPAdapter
+	URI  string
+}
+
+// Corresponds to the bidder name in cookies and requests
+func (a *ConversantLegacyAdapter) Name() string {
+	return "conversant"
+}
+
+// Return true so no request will be sent unless user has been sync'ed.
+func (a *ConversantLegacyAdapter) SkipNoCookies() bool {
+	return true
+}
+
+type conversantParams struct {
+	SiteID      string   `json:"site_id"`
+	Secure      *int8    `json:"secure"`
+	TagID       string   `json:"tag_id"`
+	Position    *int8    `json:"position"`
+	BidFloor    float64  `json:"bidfloor"`
+	Mobile      *int8    `json:"mobile"`
+	MIMEs       []string `json:"mimes"`
+	API         []int8   `json:"api"`
+	Protocols   []int8   `json:"protocols"`
+	MaxDuration *int64   `json:"maxduration"`
+}
+
+func (a *ConversantLegacyAdapter) Call(ctx context.Context, req *pbs.PBSRequest, bidder *pbs.PBSBidder) (pbs.PBSBidSlice, error) {
+	mediaTypes := []pbs.MediaType{pbs.MEDIA_TYPE_BANNER, pbs.MEDIA_TYPE_VIDEO}
+	cnvrReq, err := adapters.MakeOpenRTBGeneric(req, bidder, a.Name(), mediaTypes)
+
+	if err != nil {
+		return nil, err
+	}
+
+	// Create a map of impression objects for both request creation
+	// and response parsing.
+
+	impMap := make(map[string]*openrtb.Imp, len(cnvrReq.Imp))
+	for idx := range cnvrReq.Imp {
+		impMap[cnvrReq.Imp[idx].ID] = &cnvrReq.Imp[idx]
+	}
+
+	// Fill in additional info from custom params
+
+	for _, unit := range bidder.AdUnits {
+		var params conversantParams
+
+		imp := impMap[unit.Code]
+		if imp == nil {
+			// Skip ad units that do not have corresponding impressions.
+			continue
+		}
+
+		err := json.Unmarshal(unit.Params, &params)
+		if err != nil {
+			return nil, &errortypes.BadInput{
+				Message: err.Error(),
+			}
+		}
+
+		// Fill in additional Site info
+		if params.SiteID != "" {
+			if cnvrReq.Site != nil {
+				cnvrReq.Site.ID = params.SiteID
+			}
+			if cnvrReq.App != nil {
+				cnvrReq.App.ID = params.SiteID
+			}
+		}
+
+		if params.Mobile != nil && !(cnvrReq.Site == nil) {
+			cnvrReq.Site.Mobile = *params.Mobile
+		}
+
+		// Fill in additional impression info
+
+		imp.DisplayManager = "prebid-s2s"
+		imp.DisplayManagerVer = "1.0.1"
+		imp.BidFloor = params.BidFloor
+		imp.TagID = params.TagID
+
+		var position *openrtb.AdPosition
+		if params.Position != nil {
+			position = openrtb.AdPosition(*params.Position).Ptr()
+		}
+
+		if imp.Banner != nil {
+			imp.Banner.Pos = position
+		} else if imp.Video != nil {
+			imp.Video.Pos = position
+
+			if len(params.API) > 0 {
+				imp.Video.API = make([]openrtb.APIFramework, 0, len(params.API))
+				for _, api := range params.API {
+					imp.Video.API = append(imp.Video.API, openrtb.APIFramework(api))
+				}
+			}
+
+			// Include protocols, mimes, and max duration if specified
+			// These properties can also be specified in ad unit's video object,
+			// but are overridden if the custom params object also contains them.
+
+			if len(params.Protocols) > 0 {
+				imp.Video.Protocols = make([]openrtb.Protocol, 0, len(params.Protocols))
+				for _, protocol := range params.Protocols {
+					imp.Video.Protocols = append(imp.Video.Protocols, openrtb.Protocol(protocol))
+				}
+			}
+
+			if len(params.MIMEs) > 0 {
+				imp.Video.MIMEs = make([]string, len(params.MIMEs))
+				copy(imp.Video.MIMEs, params.MIMEs)
+			}
+
+			if params.MaxDuration != nil {
+				imp.Video.MaxDuration = *params.MaxDuration
+			}
+		}
+
+		// Take care not to override the global secure flag
+
+		if (imp.Secure == nil || *imp.Secure == 0) && params.Secure != nil {
+			imp.Secure = params.Secure
+		}
+	}
+
+	// Do a quick check on required parameters
+
+	if cnvrReq.Site != nil && cnvrReq.Site.ID == "" {
+		return nil, &errortypes.BadInput{
+			Message: "Missing site id",
+		}
+	}
+
+	if cnvrReq.App != nil && cnvrReq.App.ID == "" {
+		return nil, &errortypes.BadInput{
+			Message: "Missing app id",
+		}
+	}
+
+	// Start capturing debug info
+
+	debug := &pbs.BidderDebug{
+		RequestURI: a.URI,
+	}
+
+	if cnvrReq.Device == nil {
+		cnvrReq.Device = &openrtb.Device{}
+	}
+
+	// Convert request to json to be sent over http
+
+	j, _ := json.Marshal(cnvrReq)
+
+	if req.IsDebug {
+		debug.RequestBody = string(j)
+		bidder.Debug = append(bidder.Debug, debug)
+	}
+
+	httpReq, err := http.NewRequest("POST", a.URI, bytes.NewBuffer(j))
+	httpReq.Header.Add("Content-Type", "application/json")
+	httpReq.Header.Add("Accept", "application/json")
+
+	resp, err := ctxhttp.Do(ctx, a.http.Client, httpReq)
+	if err != nil {
+		return nil, err
+	}
+
+	defer resp.Body.Close()
+
+	if req.IsDebug {
+		debug.StatusCode = resp.StatusCode
+	}
+
+	if resp.StatusCode == 204 {
+		return nil, nil
+	}
+
+	body, err := ioutil.ReadAll(resp.Body)
+
+	if err != nil {
+		return nil, err
+	}
+
+	if resp.StatusCode == http.StatusBadRequest {
+		return nil, &errortypes.BadInput{
+			Message: fmt.Sprintf("HTTP status: %d, body: %s", resp.StatusCode, string(body)),
+		}
+	}
+
+	if resp.StatusCode != 200 {
+		return nil, &errortypes.BadServerResponse{
+			Message: fmt.Sprintf("HTTP status: %d, body: %s", resp.StatusCode, string(body)),
+		}
+	}
+
+	if req.IsDebug {
+		debug.ResponseBody = string(body)
+	}
+
+	var bidResp openrtb.BidResponse
+
+	err = json.Unmarshal(body, &bidResp)
+	if err != nil {
+		return nil, &errortypes.BadServerResponse{
+			Message: err.Error(),
+		}
+	}
+
+	bids := make(pbs.PBSBidSlice, 0)
+
+	for _, seatbid := range bidResp.SeatBid {
+		for _, bid := range seatbid.Bid {
+			if bid.Price <= 0 {
+				continue
+			}
+
+			imp := impMap[bid.ImpID]
+			if imp == nil {
+				// All returned bids should have a matching impression
+				return nil, &errortypes.BadServerResponse{
+					Message: fmt.Sprintf("Unknown impression id '%s'", bid.ImpID),
+				}
+			}
+
+			bidID := bidder.LookupBidID(bid.ImpID)
+			if bidID == "" {
+				return nil, &errortypes.BadServerResponse{
+					Message: fmt.Sprintf("Unknown ad unit code '%s'", bid.ImpID),
+				}
+			}
+
+			pbsBid := pbs.PBSBid{
+				BidID:       bidID,
+				AdUnitCode:  bid.ImpID,
+				Price:       bid.Price,
+				Creative_id: bid.CrID,
+				BidderCode:  bidder.BidderCode,
+			}
+
+			if imp.Video != nil {
+				pbsBid.CreativeMediaType = "video"
+				pbsBid.NURL = bid.AdM // Assign to NURL so it'll be interpreted as a vastUrl
+				pbsBid.Width = imp.Video.W
+				pbsBid.Height = imp.Video.H
+			} else {
+				pbsBid.CreativeMediaType = "banner"
+				pbsBid.NURL = bid.NURL
+				pbsBid.Adm = bid.AdM
+				pbsBid.Width = bid.W
+				pbsBid.Height = bid.H
+			}
+
+			bids = append(bids, &pbsBid)
+		}
+	}
+
+	if len(bids) == 0 {
+		return nil, nil
+	}
+
+	return bids, nil
+}
+
+func NewConversantAdapter(config *adapters.HTTPAdapterConfig, uri string) *ConversantLegacyAdapter {
+	a := adapters.NewHTTPAdapter(config)
+
+	return &ConversantLegacyAdapter{
+		http: a,
+		URI:  uri,
+	}
+}
diff --git a/adapters/conversant/conversant.go b/adapters/conversant/conversant.go
index b248e2e9..64ddc38b 100644
--- a/adapters/conversant/conversant.go
+++ b/adapters/conversant/conversant.go
@@ -1,291 +1,176 @@
 package conversant
 
 import (
-	"bytes"
-	"context"
 	"encoding/json"
 	"fmt"
-	"io/ioutil"
-	"net/http"
-
 	"github.com/mxmCherry/openrtb"
 	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/errortypes"
-	"github.com/prebid/prebid-server/pbs"
-	"golang.org/x/net/context/ctxhttp"
+	"github.com/prebid/prebid-server/openrtb_ext"
+	"net/http"
 )
 
 type ConversantAdapter struct {
-	http *adapters.HTTPAdapter
-	URI  string
-}
-
-// Corresponds to the bidder name in cookies and requests
-func (a *ConversantAdapter) Name() string {
-	return "conversant"
+	URI string
 }
 
-// Return true so no request will be sent unless user has been sync'ed.
-func (a *ConversantAdapter) SkipNoCookies() bool {
-	return true
-}
-
-type conversantParams struct {
-	SiteID      string   `json:"site_id"`
-	Secure      *int8    `json:"secure"`
-	TagID       string   `json:"tag_id"`
-	Position    *int8    `json:"position"`
-	BidFloor    float64  `json:"bidfloor"`
-	Mobile      *int8    `json:"mobile"`
-	MIMEs       []string `json:"mimes"`
-	API         []int8   `json:"api"`
-	Protocols   []int8   `json:"protocols"`
-	MaxDuration *int64   `json:"maxduration"`
-}
-
-func (a *ConversantAdapter) Call(ctx context.Context, req *pbs.PBSRequest, bidder *pbs.PBSBidder) (pbs.PBSBidSlice, error) {
-	mediaTypes := []pbs.MediaType{pbs.MEDIA_TYPE_BANNER, pbs.MEDIA_TYPE_VIDEO}
-	cnvrReq, err := adapters.MakeOpenRTBGeneric(req, bidder, a.Name(), mediaTypes)
-
-	if err != nil {
-		return nil, err
-	}
-
-	// Create a map of impression objects for both request creation
-	// and response parsing.
-
-	impMap := make(map[string]*openrtb.Imp, len(cnvrReq.Imp))
-	for idx := range cnvrReq.Imp {
-		impMap[cnvrReq.Imp[idx].ID] = &cnvrReq.Imp[idx]
-	}
-
-	// Fill in additional info from custom params
-
-	for _, unit := range bidder.AdUnits {
-		var params conversantParams
-
-		imp := impMap[unit.Code]
-		if imp == nil {
-			// Skip ad units that do not have corresponding impressions.
-			continue
-		}
-
-		err := json.Unmarshal(unit.Params, &params)
-		if err != nil {
-			return nil, &errortypes.BadInput{
-				Message: err.Error(),
-			}
+func (c ConversantAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	for i := 0; i < len(request.Imp); i++ {
+		var bidderExt adapters.ExtImpBidder
+		if err := json.Unmarshal(request.Imp[i].Ext, &bidderExt); err != nil {
+			return nil, []error{&errortypes.BadInput{
+				Message: fmt.Sprintf("Impression[%d] missing ext object", i),
+			}}
 		}
 
-		// Fill in additional Site info
-		if params.SiteID != "" {
-			if cnvrReq.Site != nil {
-				cnvrReq.Site.ID = params.SiteID
-			}
-			if cnvrReq.App != nil {
-				cnvrReq.App.ID = params.SiteID
-			}
+		var cnvrExt openrtb_ext.ExtImpConversant
+		if err := json.Unmarshal(bidderExt.Bidder, &cnvrExt); err != nil {
+			return nil, []error{&errortypes.BadInput{
+				Message: fmt.Sprintf("Impression[%d] missing ext.bidder object", i),
+			}}
 		}
 
-		if params.Mobile != nil && !(cnvrReq.Site == nil) {
-			cnvrReq.Site.Mobile = *params.Mobile
+		if cnvrExt.SiteID == "" {
+			return nil, []error{&errortypes.BadInput{
+				Message: fmt.Sprintf("Impression[%d] requires ext.bidder.site_id", i),
+			}}
 		}
 
-		// Fill in additional impression info
-
-		imp.DisplayManager = "prebid-s2s"
-		imp.DisplayManagerVer = "1.0.1"
-		imp.BidFloor = params.BidFloor
-		imp.TagID = params.TagID
-
-		var position *openrtb.AdPosition
-		if params.Position != nil {
-			position = openrtb.AdPosition(*params.Position).Ptr()
+		if i == 0 {
+			if request.Site != nil {
+				tmpSite := *request.Site
+				request.Site = &tmpSite
+				request.Site.ID = cnvrExt.SiteID
+			} else if request.App != nil {
+				tmpApp := *request.App
+				request.App = &tmpApp
+				request.App.ID = cnvrExt.SiteID
+			}
 		}
+		parseCnvrParams(&request.Imp[i], cnvrExt)
+	}
 
-		if imp.Banner != nil {
-			imp.Banner.Pos = position
-		} else if imp.Video != nil {
-			imp.Video.Pos = position
+	//create the request body
+	data, err := json.Marshal(request)
+	if err != nil {
+		return nil, []error{&errortypes.BadInput{
+			Message: fmt.Sprintf("Error in packaging request to JSON"),
+		}}
+	}
+	headers := http.Header{}
+	headers.Add("Content-Type", "application/json;charset=utf-8")
+	headers.Add("Accept", "application/json")
+
+	return []*adapters.RequestData{{
+		Method:  "POST",
+		Uri:     c.URI,
+		Body:    data,
+		Headers: headers,
+	}}, nil
+}
 
-			if len(params.API) > 0 {
-				imp.Video.API = make([]openrtb.APIFramework, 0, len(params.API))
-				for _, api := range params.API {
-					imp.Video.API = append(imp.Video.API, openrtb.APIFramework(api))
-				}
-			}
+func parseCnvrParams(imp *openrtb.Imp, cnvrExt openrtb_ext.ExtImpConversant) {
+	imp.DisplayManager = "prebid-s2s"
+	imp.DisplayManagerVer = "2.0.0"
+	imp.BidFloor = cnvrExt.BidFloor
+	imp.TagID = cnvrExt.TagID
 
-			// Include protocols, mimes, and max duration if specified
-			// These properties can also be specified in ad unit's video object,
-			// but are overridden if the custom params object also contains them.
+	// Take care not to override the global secure flag
+	if (imp.Secure == nil || *imp.Secure == 0) && cnvrExt.Secure != nil {
+		imp.Secure = cnvrExt.Secure
+	}
 
-			if len(params.Protocols) > 0 {
-				imp.Video.Protocols = make([]openrtb.Protocol, 0, len(params.Protocols))
-				for _, protocol := range params.Protocols {
-					imp.Video.Protocols = append(imp.Video.Protocols, openrtb.Protocol(protocol))
-				}
-			}
+	var position *openrtb.AdPosition
+	if cnvrExt.Position != nil {
+		position = openrtb.AdPosition(*cnvrExt.Position).Ptr()
+	}
+	if imp.Banner != nil {
+		tmpBanner := *imp.Banner
+		imp.Banner = &tmpBanner
+		imp.Banner.Pos = position
 
-			if len(params.MIMEs) > 0 {
-				imp.Video.MIMEs = make([]string, len(params.MIMEs))
-				copy(imp.Video.MIMEs, params.MIMEs)
-			}
+	} else if imp.Video != nil {
+		tmpVideo := *imp.Video
+		imp.Video = &tmpVideo
+		imp.Video.Pos = position
 
-			if params.MaxDuration != nil {
-				imp.Video.MaxDuration = *params.MaxDuration
+		if len(cnvrExt.API) > 0 {
+			imp.Video.API = make([]openrtb.APIFramework, 0, len(cnvrExt.API))
+			for _, api := range cnvrExt.API {
+				imp.Video.API = append(imp.Video.API, openrtb.APIFramework(api))
 			}
 		}
 
-		// Take care not to override the global secure flag
+		// Include protocols, mimes, and max duration if specified
+		// These properties can also be specified in ad unit's video object,
+		// but are overridden if the custom params object also contains them.
 
-		if (imp.Secure == nil || *imp.Secure == 0) && params.Secure != nil {
-			imp.Secure = params.Secure
+		if len(cnvrExt.Protocols) > 0 {
+			imp.Video.Protocols = make([]openrtb.Protocol, 0, len(cnvrExt.Protocols))
+			for _, protocol := range cnvrExt.Protocols {
+				imp.Video.Protocols = append(imp.Video.Protocols, openrtb.Protocol(protocol))
+			}
 		}
-	}
-
-	// Do a quick check on required parameters
 
-	if cnvrReq.Site != nil && cnvrReq.Site.ID == "" {
-		return nil, &errortypes.BadInput{
-			Message: "Missing site id",
+		if len(cnvrExt.MIMEs) > 0 {
+			imp.Video.MIMEs = make([]string, len(cnvrExt.MIMEs))
+			copy(imp.Video.MIMEs, cnvrExt.MIMEs)
 		}
-	}
 
-	if cnvrReq.App != nil && cnvrReq.App.ID == "" {
-		return nil, &errortypes.BadInput{
-			Message: "Missing app id",
+		if cnvrExt.MaxDuration != nil {
+			imp.Video.MaxDuration = *cnvrExt.MaxDuration
 		}
 	}
+}
 
-	// Start capturing debug info
-
-	debug := &pbs.BidderDebug{
-		RequestURI: a.URI,
-	}
-
-	if cnvrReq.Device == nil {
-		cnvrReq.Device = &openrtb.Device{}
-	}
-
-	// Convert request to json to be sent over http
-
-	j, _ := json.Marshal(cnvrReq)
-
-	if req.IsDebug {
-		debug.RequestBody = string(j)
-		bidder.Debug = append(bidder.Debug, debug)
-	}
-
-	httpReq, err := http.NewRequest("POST", a.URI, bytes.NewBuffer(j))
-	httpReq.Header.Add("Content-Type", "application/json")
-	httpReq.Header.Add("Accept", "application/json")
-
-	resp, err := ctxhttp.Do(ctx, a.http.Client, httpReq)
-	if err != nil {
-		return nil, err
-	}
-
-	defer resp.Body.Close()
-
-	if req.IsDebug {
-		debug.StatusCode = resp.StatusCode
-	}
-
-	if resp.StatusCode == 204 {
-		return nil, nil
-	}
-
-	body, err := ioutil.ReadAll(resp.Body)
-
-	if err != nil {
-		return nil, err
+func (c ConversantAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	if response.StatusCode == http.StatusNoContent {
+		return nil, nil // no bid response
 	}
 
-	if resp.StatusCode == http.StatusBadRequest {
-		return nil, &errortypes.BadInput{
-			Message: fmt.Sprintf("HTTP status: %d, body: %s", resp.StatusCode, string(body)),
-		}
+	if response.StatusCode != http.StatusOK {
+		return nil, []error{&errortypes.BadServerResponse{
+			Message: fmt.Sprintf("Unexpected status code: %d", response.StatusCode),
+		}}
 	}
 
-	if resp.StatusCode != 200 {
-		return nil, &errortypes.BadServerResponse{
-			Message: fmt.Sprintf("HTTP status: %d, body: %s", resp.StatusCode, string(body)),
-		}
+	var resp openrtb.BidResponse
+	if err := json.Unmarshal(response.Body, &resp); err != nil {
+		return nil, []error{&errortypes.BadServerResponse{
+			Message: fmt.Sprintf("bad server response: %d. ", err),
+		}}
 	}
 
-	if req.IsDebug {
-		debug.ResponseBody = string(body)
+	if len(resp.SeatBid) == 0 {
+		return nil, []error{&errortypes.BadServerResponse{
+			Message: fmt.Sprintf("Empty bid request"),
+		}}
 	}
 
-	var bidResp openrtb.BidResponse
-
-	err = json.Unmarshal(body, &bidResp)
-	if err != nil {
-		return nil, &errortypes.BadServerResponse{
-			Message: err.Error(),
-		}
+	bids := resp.SeatBid[0].Bid
+	bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(bids))
+	for i := 0; i < len(bids); i++ {
+		bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
+			Bid:     &bids[i],
+			BidType: getBidType(bids[i].ImpID, internalRequest.Imp),
+		})
 	}
+	return bidResponse, nil
+}
 
-	bids := make(pbs.PBSBidSlice, 0)
-
-	for _, seatbid := range bidResp.SeatBid {
-		for _, bid := range seatbid.Bid {
-			if bid.Price <= 0 {
-				continue
-			}
-
-			imp := impMap[bid.ImpID]
-			if imp == nil {
-				// All returned bids should have a matching impression
-				return nil, &errortypes.BadServerResponse{
-					Message: fmt.Sprintf("Unknown impression id '%s'", bid.ImpID),
-				}
-			}
-
-			bidID := bidder.LookupBidID(bid.ImpID)
-			if bidID == "" {
-				return nil, &errortypes.BadServerResponse{
-					Message: fmt.Sprintf("Unknown ad unit code '%s'", bid.ImpID),
-				}
-			}
-
-			pbsBid := pbs.PBSBid{
-				BidID:       bidID,
-				AdUnitCode:  bid.ImpID,
-				Price:       bid.Price,
-				Creative_id: bid.CrID,
-				BidderCode:  bidder.BidderCode,
-			}
-
+func getBidType(impId string, imps []openrtb.Imp) openrtb_ext.BidType {
+	bidType := openrtb_ext.BidTypeBanner
+	for _, imp := range imps {
+		if imp.ID == impId {
 			if imp.Video != nil {
-				pbsBid.CreativeMediaType = "video"
-				pbsBid.NURL = bid.AdM // Assign to NURL so it'll be interpreted as a vastUrl
-				pbsBid.Width = imp.Video.W
-				pbsBid.Height = imp.Video.H
-			} else {
-				pbsBid.CreativeMediaType = "banner"
-				pbsBid.NURL = bid.NURL
-				pbsBid.Adm = bid.AdM
-				pbsBid.Width = bid.W
-				pbsBid.Height = bid.H
+				bidType = openrtb_ext.BidTypeVideo
 			}
-
-			bids = append(bids, &pbsBid)
+			break
 		}
 	}
-
-	if len(bids) == 0 {
-		return nil, nil
-	}
-
-	return bids, nil
+	return bidType
 }
 
-func NewConversantAdapter(config *adapters.HTTPAdapterConfig, uri string) *ConversantAdapter {
-	a := adapters.NewHTTPAdapter(config)
-
-	return &ConversantAdapter{
-		http: a,
-		URI:  uri,
-	}
+func NewConversantBidder(endpoint string) *ConversantAdapter {
+	return &ConversantAdapter{URI: endpoint}
 }
diff --git a/adapters/rubicon/rubicon.go b/adapters/rubicon/rubicon.go
index 7d6e0e12..56ae7b2f 100644
--- a/adapters/rubicon/rubicon.go
+++ b/adapters/rubicon/rubicon.go
@@ -236,7 +236,6 @@ var rubiSizeMap = map[rubiSize]int{
 	{w: 800, h: 250}:   125,
 	{w: 200, h: 600}:   126,
 	{w: 640, h: 320}:   156,
-	{w: 640, h: 360}:   198,
 }
 
 // defines the contract for bidrequest.user.ext.eids[i].ext
diff --git a/adapters/sharethrough/butler.go b/adapters/sharethrough/butler.go
index 522bbc49..36af79c4 100644
--- a/adapters/sharethrough/butler.go
+++ b/adapters/sharethrough/butler.go
@@ -3,16 +3,17 @@ package sharethrough
 import (
 	"encoding/json"
 	"fmt"
-	"github.com/mxmCherry/openrtb"
-	"github.com/prebid/prebid-server/adapters"
-	"github.com/prebid/prebid-server/errortypes"
-	"github.com/prebid/prebid-server/openrtb_ext"
-	"github.com/prebid/prebid-server/privacy/ccpa"
 	"net/http"
 	"net/url"
 	"regexp"
 	"strconv"
 	"time"
+
+	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/prebid/prebid-server/privacy/ccpa"
 )
 
 const defaultTmax = 10000 // 10 sec
@@ -97,8 +98,8 @@ func (s StrOpenRTBTranslator) requestFromOpenRTB(imp openrtb.Imp, request *openr
 	}
 
 	usPolicySignal := ""
-	if usPolicy, err := ccpa.ReadPolicy(request); err == nil {
-		usPolicySignal = usPolicy.Value
+	if usPolicy, err := ccpa.ReadFromRequest(request); err == nil {
+		usPolicySignal = usPolicy.Consent
 	}
 
 	return &adapters.RequestData{
diff --git a/adapters/syncer.go b/adapters/syncer.go
index c212a436..122bcc7e 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.Value,
+		USPrivacy:   privacyPolicies.CCPA.Consent,
 	})
 	if err != nil {
 		return nil, err
diff --git a/endpoints/auction.go b/endpoints/auction.go
index bf592e43..c6fd5712 100644
--- a/endpoints/auction.go
+++ b/endpoints/auction.go
@@ -24,7 +24,7 @@ import (
 	"github.com/prebid/prebid-server/pbsmetrics"
 	pbc "github.com/prebid/prebid-server/prebid_cache_client"
 	"github.com/prebid/prebid-server/privacy"
-	gdprPolicy "github.com/prebid/prebid-server/privacy/gdpr"
+	gdprPrivacy "github.com/prebid/prebid-server/privacy/gdpr"
 	"github.com/prebid/prebid-server/usersync"
 )
 
@@ -190,7 +190,7 @@ func (a *auction) recoverSafely(inner func(*pbs.PBSBidder, pbsmetrics.AdapterLab
 	}
 }
 
-func (a *auction) shouldUsersync(ctx context.Context, bidder openrtb_ext.BidderName, gdprPrivacyPolicy gdprPolicy.Policy) bool {
+func (a *auction) shouldUsersync(ctx context.Context, bidder openrtb_ext.BidderName, gdprPrivacyPolicy gdprPrivacy.Policy) bool {
 	switch gdprPrivacyPolicy.Signal {
 	case "0":
 		return true
@@ -511,7 +511,7 @@ func (a *auction) processUserSync(req *pbs.PBSRequest, bidder *pbs.PBSBidder, bl
 	if uid == "" {
 		bidder.NoCookie = true
 		privacyPolicies := privacy.Policies{
-			GDPR: gdprPolicy.Policy{
+			GDPR: gdprPrivacy.Policy{
 				Signal:  req.ParseGDPR(),
 				Consent: req.ParseConsent(),
 			},
diff --git a/endpoints/cookie_sync.go b/endpoints/cookie_sync.go
index 9787a8f7..60da4f0b 100644
--- a/endpoints/cookie_sync.go
+++ b/endpoints/cookie_sync.go
@@ -20,7 +20,7 @@ import (
 	"github.com/prebid/prebid-server/pbsmetrics"
 	"github.com/prebid/prebid-server/privacy"
 	"github.com/prebid/prebid-server/privacy/ccpa"
-	gdprPolicy "github.com/prebid/prebid-server/privacy/gdpr"
+	gdprPrivacy "github.com/prebid/prebid-server/privacy/gdpr"
 	"github.com/prebid/prebid-server/usersync"
 )
 
@@ -105,24 +105,30 @@ func (deps *cookieSyncDeps) Endpoint(w http.ResponseWriter, r *http.Request, _ h
 		}
 	}
 
+	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 {
+		adapterSyncs[openrtb_ext.BidderName(b)] = true
+	}
+
 	privacyPolicy := privacy.Policies{
-		GDPR: gdprPolicy.Policy{
+		GDPR: gdprPrivacy.Policy{
 			Signal:  gdprToString(parsedReq.GDPR),
 			Consent: parsedReq.Consent,
 		},
 		CCPA: ccpa.Policy{
-			Value: parsedReq.USPrivacy,
+			Consent: parsedReq.USPrivacy,
 		},
 	}
 
-	parsedReq.filterExistingSyncs(deps.syncers, userSyncCookie, needSyncupForSameSite)
+	parsedReq.filterForGDPR(deps.syncPermissions)
 
-	adapterSyncs := make(map[openrtb_ext.BidderName]bool)
-	// assume all bidders will be privacy blocked
-	for _, b := range parsedReq.Bidders {
-		adapterSyncs[openrtb_ext.BidderName(b)] = true
+	if deps.enforceCCPA {
+		parsedReq.filterForCCPA()
 	}
-	parsedReq.filterForPrivacy(deps.syncPermissions, privacyPolicy, deps.enforceCCPA)
+
 	// surviving bidders are not privacy blocked
 	for _, b := range parsedReq.Bidders {
 		adapterSyncs[openrtb_ext.BidderName(b)] = false
@@ -223,12 +229,7 @@ func (req *cookieSyncRequest) filterExistingSyncs(valid map[openrtb_ext.BidderNa
 	}
 }
 
-func (req *cookieSyncRequest) filterForPrivacy(permissions gdpr.Permissions, privacyPolicies privacy.Policies, enforceCCPA bool) {
-	if enforceCCPA && privacyPolicies.CCPA.ShouldEnforce() {
-		req.Bidders = nil
-		return
-	}
-
+func (req *cookieSyncRequest) filterForGDPR(permissions gdpr.Permissions) {
 	if req.GDPR != nil && *req.GDPR == 0 {
 		return
 	}
@@ -246,6 +247,25 @@ func (req *cookieSyncRequest) filterForPrivacy(permissions gdpr.Permissions, pri
 	}
 }
 
+func (req *cookieSyncRequest) filterForCCPA() {
+	validBidders := make(map[string]struct{})
+	for _, v := range openrtb_ext.BidderMap {
+		validBidders[v.String()] = struct{}{}
+	}
+
+	ccpaPolicy := &ccpa.Policy{Consent: req.USPrivacy}
+	ccpaParsedPolicy, err := ccpaPolicy.Parse(validBidders)
+
+	if err == nil {
+		for i := 0; i < len(req.Bidders); i++ {
+			if ccpaParsedPolicy.ShouldEnforce(req.Bidders[i]) {
+				req.Bidders = append(req.Bidders[:i], req.Bidders[i+1:]...)
+				i--
+			}
+		}
+	}
+}
+
 // filterToLimit will enforce a max limit on cookiesyncs supplied, picking a random subset of syncs to get to the limit if over.
 func (req *cookieSyncRequest) filterToLimit() {
 	if req.Limit <= 0 {
diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go
index 1e92569e..d7442f5e 100644
--- a/endpoints/openrtb2/amp_auction.go
+++ b/endpoints/openrtb2/amp_auction.go
@@ -24,6 +24,8 @@ import (
 	"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"
+	"github.com/prebid/prebid-server/privacy/gdpr"
 	"github.com/prebid/prebid-server/stored_requests"
 	"github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher"
 	"github.com/prebid/prebid-server/usersync"
@@ -403,17 +405,12 @@ func (deps *endpointDeps) overrideWithParams(httpRequest *http.Request, req *ope
 		req.Imp[0].TagID = slot
 	}
 
-	consent := readConsent(httpRequest.URL)
-	if consent != "" {
-		if policies, ok := privacy.ReadPoliciesFromConsent(consent); ok {
-			if err := policies.Write(req); err != nil {
-				return []error{err}
-			}
-		} else {
-			return []error{&errortypes.InvalidPrivacyConsent{
-				Message: fmt.Sprintf("Consent '%s' is not recognized as either CCPA or GDPR TCF.", consent),
-			}}
-		}
+	policyWriter, policyWriterErr := readPolicyFromUrl(httpRequest.URL)
+	if policyWriterErr != nil {
+		return []error{policyWriterErr}
+	}
+	if err := policyWriter.Write(req); err != nil {
+		return []error{err}
 	}
 
 	if timeout, err := strconv.ParseInt(httpRequest.FormValue("timeout"), 10, 64); err == nil {
@@ -558,7 +555,27 @@ func setAmpExt(site *openrtb.Site, value string) {
 	}
 }
 
-func readConsent(url *url.URL) string {
+func readPolicyFromUrl(url *url.URL) (privacy.PolicyWriter, error) {
+	consent := readConsentFromURL(url)
+
+	if len(consent) == 0 {
+		return privacy.NilPolicyWriter{}, nil
+	}
+
+	if gdpr.ValidateConsent(consent) {
+		return gdpr.ConsentWriter{consent}, nil
+	}
+
+	if ccpa.ValidateConsent(consent) {
+		return ccpa.ConsentWriter{consent}, nil
+	}
+
+	return privacy.NilPolicyWriter{}, &errortypes.InvalidPrivacyConsent{
+		Message: fmt.Sprintf("Consent '%s' is not recognized as either CCPA or GDPR TCF.", consent),
+	}
+}
+
+func readConsentFromURL(url *url.URL) string {
 	if v := url.Query().Get("consent_string"); v != "" {
 		return v
 	}
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index d6cbc228..b02b5786 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -318,39 +318,36 @@ func (deps *endpointDeps) validateRequest(req *openrtb.BidRequest) []error {
 	}
 
 	if (req.Site == nil && req.App == nil) || (req.Site != nil && req.App != nil) {
-		errL = append(errL, errors.New("request.site or request.app must be defined, but not both."))
-		return errL
+		return append(errL, errors.New("request.site or request.app must be defined, but not both."))
 	}
 
 	if err := deps.validateSite(req.Site); err != nil {
-		errL = append(errL, err)
-		return errL
+		return append(errL, err)
 	}
 
 	if err := deps.validateApp(req.App); err != nil {
-		errL = append(errL, err)
-		return errL
+		return append(errL, err)
 	}
 
 	if err := validateUser(req.User, aliases); err != nil {
-		errL = append(errL, err)
-		return errL
+		return append(errL, err)
 	}
 
 	if err := validateRegs(req.Regs); err != nil {
-		errL = append(errL, err)
-		return errL
+		return append(errL, err)
 	}
 
-	if policy, err := ccpa.ReadPolicy(req); err != nil {
-		errL = append(errL, errL...)
-		return errL
-	} else if err := policy.Validate(); err != nil {
-		errL = append(errL, &errortypes.InvalidPrivacyConsent{Message: fmt.Sprintf("CCPA consent is invalid and will be ignored. (%v)", err)})
-
-		policy.Value = ""
-		if err := policy.Write(req); err != nil {
-			errL = append(errL, fmt.Errorf("Unable to remove invalid CCPA consent from the request. (%v)", err))
+	if ccpaPolicy, err := ccpa.ReadFromRequest(req); err != nil {
+		return append(errL, err)
+	} else if _, err := ccpaPolicy.Parse(exchange.GetValidBidders(aliases)); err != nil {
+		if _, invalidConsent := err.(*errortypes.InvalidPrivacyConsent); invalidConsent {
+			errL = append(errL, &errortypes.InvalidPrivacyConsent{Message: fmt.Sprintf("CCPA consent is invalid and will be ignored. (%v)", err)})
+			consentWriter := ccpa.ConsentWriter{""}
+			if err := consentWriter.Write(req); err != nil {
+				return append(errL, fmt.Errorf("Unable to remove invalid CCPA consent from the request. (%v)", err))
+			}
+		} else {
+			return append(errL, err)
 		}
 	}
 
diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go
index d4281689..678cfc18 100755
--- a/exchange/adapter_map.go
+++ b/exchange/adapter_map.go
@@ -122,6 +122,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter
 		openrtb_ext.BidderBrightroll:   brightroll.NewBrightrollBidder(cfg.Adapters[string(openrtb_ext.BidderBrightroll)].Endpoint, cfg.Adapters[string(openrtb_ext.BidderBrightroll)].ExtraAdapterInfo),
 		openrtb_ext.BidderColossus:     colossus.NewColossusBidder(cfg.Adapters[string(openrtb_ext.BidderColossus)].Endpoint),
 		openrtb_ext.BidderConsumable:   consumable.NewConsumableBidder(cfg.Adapters[string(openrtb_ext.BidderConsumable)].Endpoint),
+		openrtb_ext.BidderConversant:   conversant.NewConversantBidder(cfg.Adapters[string(openrtb_ext.BidderConversant)].Endpoint),
 		openrtb_ext.BidderCpmstar:      cpmstar.NewCpmstarBidder(cfg.Adapters[string(openrtb_ext.BidderCpmstar)].Endpoint),
 		openrtb_ext.BidderDatablocks:   datablocks.NewDatablocksBidder(cfg.Adapters[string(openrtb_ext.BidderDatablocks)].Endpoint),
 		openrtb_ext.BidderDmx:          dmx.NewDmxBidder(cfg.Adapters[string(openrtb_ext.BidderDmx)].Endpoint),
@@ -186,8 +187,6 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter
 	}
 
 	legacyBidders := map[openrtb_ext.BidderName]adapters.Adapter{
-		// TODO #267: Upgrade the Conversant adapter
-		openrtb_ext.BidderConversant: conversant.NewConversantAdapter(adapters.DefaultHTTPAdapterConfig, cfg.Adapters[string(openrtb_ext.BidderConversant)].Endpoint),
 		// TODO #212: Upgrade the Index adapter
 		openrtb_ext.BidderIx: ix.NewIxAdapter(adapters.DefaultHTTPAdapterConfig, cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderIx))].Endpoint),
 		// TODO #213: Upgrade the Lifestreet adapter
diff --git a/exchange/utils.go b/exchange/utils.go
index 22b28adc..1e49b7ac 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -19,6 +19,8 @@ import (
 	"github.com/prebid/prebid-server/privacy/lmt"
 )
 
+const unknownBidder string = ""
+
 func BidderToPrebidSChains(req *openrtb_ext.ExtRequest) (map[string]*openrtb_ext.ExtRequestPrebidSChainSChain, error) {
 	bidderToSChains := make(map[string]*openrtb_ext.ExtRequestPrebidSChainSChain)
 
@@ -65,31 +67,32 @@ func cleanOpenRTBRequests(ctx context.Context,
 
 	requestsByBidder, errs = splitBidRequest(orig, requestExt, impsByBidder, aliases, usersyncs, blables, labels)
 
+	if len(requestsByBidder) == 0 {
+		return
+	}
+
 	gdpr := extractGDPR(orig, usersyncIfAmbiguous)
 	consent := extractConsent(orig)
 	ampGDPRException := (labels.RType == pbsmetrics.ReqTypeAMP) && gDPR.AMPException()
 
-	var ccpaPolicy ccpa.Policy
-	if privacyConfig.CCPA.Enforce {
-		ccpaPolicy, _ = ccpa.ReadPolicy(orig)
+	ccpaEnforcer, err := extractCCPA(orig, privacyConfig, aliases)
+	if err != nil {
+		errs = append(errs, err)
+		return
 	}
 
-	var lmtPolicy lmt.Policy
-	if privacyConfig.LMT.Enforce {
-		lmtPolicy = lmt.ReadPolicy(orig)
-	}
+	lmtEnforcer := extractLMT(orig, privacyConfig)
 
 	// request level privacy policies
 	privacyEnforcement := privacy.Enforcement{
-		CCPA:  ccpaPolicy.ShouldEnforce(),
 		COPPA: orig.Regs != nil && orig.Regs.COPPA == 1,
-		LMT:   lmtPolicy.ShouldEnforce(),
+		LMT:   lmtEnforcer.ShouldEnforce(unknownBidder),
 	}
 
-	privacyLabels.CCPAProvided = ccpaPolicy.Value != ""
-	privacyLabels.CCPAEnforced = privacyEnforcement.CCPA
+	privacyLabels.CCPAProvided = ccpaEnforcer.CanEnforce()
+	privacyLabels.CCPAEnforced = ccpaEnforcer.ShouldEnforce(unknownBidder)
 	privacyLabels.COPPAEnforced = privacyEnforcement.COPPA
-	privacyLabels.LMTEnforced = privacyEnforcement.LMT
+	privacyLabels.LMTEnforced = lmtEnforcer.ShouldEnforce(unknownBidder)
 
 	if gdpr == 1 {
 		privacyLabels.GDPREnforced = true
@@ -102,7 +105,10 @@ func cleanOpenRTBRequests(ctx context.Context,
 
 	// bidder level privacy policies
 	for bidder, bidReq := range requestsByBidder {
+		// CCPA
+		privacyEnforcement.CCPA = ccpaEnforcer.ShouldEnforce(bidder.String())
 
+		// GDPR
 		if gdpr == 1 {
 			coreBidder := resolveBidder(bidder.String(), aliases)
 
@@ -121,6 +127,32 @@ func cleanOpenRTBRequests(ctx context.Context,
 	return
 }
 
+func extractCCPA(orig *openrtb.BidRequest, privacyConfig config.Privacy, aliases map[string]string) (privacy.PolicyEnforcer, error) {
+	ccpaPolicy, err := ccpa.ReadFromRequest(orig)
+	if err != nil {
+		return privacy.NilPolicyEnforcer{}, err
+	}
+
+	validBidders := GetValidBidders(aliases)
+	ccpaParsedPolicy, err := ccpaPolicy.Parse(validBidders)
+	if err != nil {
+		return privacy.NilPolicyEnforcer{}, err
+	}
+
+	ccpaEnforcer := privacy.EnabledPolicyEnforcer{
+		Enabled:        privacyConfig.CCPA.Enforce,
+		PolicyEnforcer: ccpaParsedPolicy,
+	}
+	return ccpaEnforcer, nil
+}
+
+func extractLMT(orig *openrtb.BidRequest, privacyConfig config.Privacy) privacy.PolicyEnforcer {
+	return privacy.EnabledPolicyEnforcer{
+		Enabled:        privacyConfig.LMT.Enforce,
+		PolicyEnforcer: lmt.ReadFromRequest(orig),
+	}
+}
+
 func splitBidRequest(req *openrtb.BidRequest,
 	requestExt *openrtb_ext.ExtRequest,
 	impsByBidder map[string][]openrtb.Imp,
@@ -429,6 +461,20 @@ func parseAliases(orig *openrtb.BidRequest) (map[string]string, []error) {
 	return aliases, nil
 }
 
+func GetValidBidders(aliases map[string]string) map[string]struct{} {
+	validBidders := make(map[string]struct{})
+
+	for _, v := range openrtb_ext.BidderMap {
+		validBidders[v.String()] = struct{}{}
+	}
+
+	for k := range aliases {
+		validBidders[k] = struct{}{}
+	}
+
+	return validBidders
+}
+
 // Quick little randomizer for a list of strings. Stuffing it in utils to keep other files clean
 func randomizeList(list []openrtb_ext.BidderName) {
 	l := len(list)
diff --git a/openrtb_ext/imp_conversant.go b/openrtb_ext/imp_conversant.go
new file mode 100644
index 00000000..8587e111
--- /dev/null
+++ b/openrtb_ext/imp_conversant.go
@@ -0,0 +1,13 @@
+package openrtb_ext
+
+type ExtImpConversant struct {
+	SiteID      string   `json:"site_id"`
+	Secure      *int8    `json:"secure"`
+	TagID       string   `json:"tag_id"`
+	Position    *int8    `json:"position"`
+	BidFloor    float64  `json:"bidfloor"`
+	MIMEs       []string `json:"mimes"`
+	API         []int8   `json:"api"`
+	Protocols   []int8   `json:"protocols"`
+	MaxDuration *int64   `json:"maxduration"`
+}
diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go
index 42ac9d9d..894be676 100644
--- a/openrtb_ext/request.go
+++ b/openrtb_ext/request.go
@@ -24,6 +24,11 @@ type ExtRequestPrebid struct {
 	Targeting            *ExtRequestTargeting      `json:"targeting,omitempty"`
 	SupportDeals         bool                      `json:"supportdeals,omitempty"`
 	Debug                bool                      `json:"debug,omitempty"`
+
+	// NoSale specifies bidders with whom the publisher has a legal relationship where the
+	// passing of personally identifiable information doesn't constitute a sale per CCPA law.
+	// The array may contain a single sstar ('*') entry to represent all bidders.
+	NoSale []string `json:"nosale,omitempty"`
 }
 
 // ExtRequestPrebid defines the contract for bidrequest.ext.prebid.schains
diff --git a/privacy/ccpa/consentwriter.go b/privacy/ccpa/consentwriter.go
new file mode 100644
index 00000000..48566554
--- /dev/null
+++ b/privacy/ccpa/consentwriter.go
@@ -0,0 +1,25 @@
+package ccpa
+
+import (
+	"github.com/mxmCherry/openrtb"
+)
+
+// ConsentWriter implements the PolicyWriter interface for CCPA.
+type ConsentWriter struct {
+	Consent string
+}
+
+// Write mutates an OpenRTB bid request with the CCPA consent string.
+func (c ConsentWriter) Write(req *openrtb.BidRequest) error {
+	if req == nil {
+		return nil
+	}
+
+	regs, err := buildRegs(c.Consent, req.Regs)
+	if err != nil {
+		return err
+	}
+	req.Regs = regs
+
+	return nil
+}
diff --git a/privacy/ccpa/parsedpolicy.go b/privacy/ccpa/parsedpolicy.go
new file mode 100644
index 00000000..3c934e67
--- /dev/null
+++ b/privacy/ccpa/parsedpolicy.go
@@ -0,0 +1,137 @@
+package ccpa
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/prebid/prebid-server/errortypes"
+)
+
+const (
+	ccpaVersion1      = '1'
+	ccpaYes           = 'Y'
+	ccpaNo            = 'N'
+	ccpaNotApplicable = '-'
+)
+
+const (
+	indexVersion                = 0
+	indexExplicitNotice         = 1
+	indexOptOutSale             = 2
+	indexLSPACoveredTransaction = 3
+)
+
+const allBiddersMarker = "*"
+
+// ValidateConsent returns true if the consent string is empty or valid per the IAB CCPA spec.
+func ValidateConsent(consent string) bool {
+	_, err := parseConsent(consent)
+	return err == nil
+}
+
+// ParsedPolicy represents parsed and validated CCPA regulatory information. Use this struct
+// to make enforcement decisions.
+type ParsedPolicy struct {
+	consentSpecified      bool
+	consentOptOutSale     bool
+	noSaleForAllBidders   bool
+	noSaleSpecificBidders map[string]struct{}
+}
+
+// Parse returns a parsed and validated ParsedPolicy intended for use in enforcement decisions.
+func (p Policy) Parse(validBidders map[string]struct{}) (ParsedPolicy, error) {
+	consentOptOut, err := parseConsent(p.Consent)
+	if err != nil {
+		msg := fmt.Sprintf("request.regs.ext.us_privacy %s", err.Error())
+		return ParsedPolicy{}, &errortypes.InvalidPrivacyConsent{Message: msg}
+	}
+
+	noSaleForAllBidders, noSaleSpecificBidders, err := parseNoSaleBidders(p.NoSaleBidders, validBidders)
+	if err != nil {
+		return ParsedPolicy{}, fmt.Errorf("request.ext.prebid.nosale is invalid: %s", err.Error())
+	}
+
+	return ParsedPolicy{
+		consentSpecified:      p.Consent != "",
+		consentOptOutSale:     consentOptOut,
+		noSaleForAllBidders:   noSaleForAllBidders,
+		noSaleSpecificBidders: noSaleSpecificBidders,
+	}, nil
+}
+
+func parseConsent(consent string) (consentOptOutSale bool, err error) {
+	if consent == "" {
+		return false, nil
+	}
+
+	if len(consent) != 4 {
+		return false, errors.New("must contain 4 characters")
+	}
+
+	if consent[indexVersion] != ccpaVersion1 {
+		return false, errors.New("must specify version 1")
+	}
+
+	var c byte
+
+	c = consent[indexExplicitNotice]
+	if c != ccpaNo && c != ccpaYes && c != ccpaNotApplicable {
+		return false, errors.New("must specify 'N', 'Y', or '-' for the explicit notice")
+	}
+
+	c = consent[indexOptOutSale]
+	if c != ccpaNo && c != ccpaYes && c != ccpaNotApplicable {
+		return false, errors.New("must specify 'N', 'Y', or '-' for the opt-out sale")
+	}
+
+	c = consent[indexLSPACoveredTransaction]
+	if c != ccpaNo && c != ccpaYes && c != ccpaNotApplicable {
+		return false, errors.New("must specify 'N', 'Y', or '-' for the limited service provider agreement")
+	}
+
+	return consent[indexOptOutSale] == ccpaYes, nil
+}
+
+func parseNoSaleBidders(noSaleBidders []string, validBidders map[string]struct{}) (noSaleForAllBidders bool, noSaleSpecificBidders map[string]struct{}, err error) {
+	noSaleSpecificBidders = make(map[string]struct{})
+
+	if len(noSaleBidders) == 1 && noSaleBidders[0] == allBiddersMarker {
+		noSaleForAllBidders = true
+		return
+	}
+
+	for _, bidder := range noSaleBidders {
+		if bidder == allBiddersMarker {
+			err = errors.New("can only specify all bidders if no other bidders are provided")
+			return
+		}
+
+		if _, exists := validBidders[bidder]; exists {
+			noSaleSpecificBidders[bidder] = struct{}{}
+		} else {
+			err = fmt.Errorf("unrecognized bidder '%s'", bidder)
+			return
+		}
+	}
+
+	return
+}
+
+// CanEnforce returns true when consent is specifically provided by the publisher, as opposed to an empty string.
+func (p ParsedPolicy) CanEnforce() bool {
+	return p.consentSpecified
+}
+
+func (p ParsedPolicy) isNoSaleForBidder(bidder string) bool {
+	if p.noSaleForAllBidders {
+		return true
+	}
+
+	_, exists := p.noSaleSpecificBidders[bidder]
+	return exists
+}
+
+// ShouldEnforce returns true when the opt-out signal is explicitly detected.
+func (p ParsedPolicy) ShouldEnforce(bidder string) bool {
+	return !p.isNoSaleForBidder(bidder) && p.consentOptOutSale
+}
diff --git a/privacy/ccpa/policy.go b/privacy/ccpa/policy.go
index 11ac4345..a9f1c49e 100644
--- a/privacy/ccpa/policy.go
+++ b/privacy/ccpa/policy.go
@@ -9,139 +9,190 @@ import (
 	"github.com/prebid/prebid-server/openrtb_ext"
 )
 
-// Policy represents the CCPA regulation for an OpenRTB bid request.
+// Policy represents the CCPA regulatory information from an OpenRTB bid request.
 type Policy struct {
-	Value string
+	Consent       string
+	NoSaleBidders []string
 }
 
-// ReadPolicy extracts the CCPA regulation policy from an OpenRTB request.
-func ReadPolicy(req *openrtb.BidRequest) (Policy, error) {
-	policy := Policy{}
+// ReadFromRequest extracts the CCPA regulatory information from an OpenRTB bid request.
+func ReadFromRequest(req *openrtb.BidRequest) (Policy, error) {
+	var consent string
+	var noSaleBidders []string
 
-	if req != nil && req.Regs != nil && len(req.Regs.Ext) > 0 {
+	if req == nil {
+		return Policy{}, nil
+	}
+
+	// Read consent from request.regs.ext
+	if req.Regs != nil && len(req.Regs.Ext) > 0 {
 		var ext openrtb_ext.ExtRegs
 		if err := json.Unmarshal(req.Regs.Ext, &ext); err != nil {
-			return policy, err
+			return Policy{}, fmt.Errorf("error reading request.regs.ext: %s", err)
 		}
-		policy.Value = ext.USPrivacy
+		consent = ext.USPrivacy
 	}
 
-	return policy, nil
+	// Read no sale bidders from request.ext.prebid
+	if len(req.Ext) > 0 {
+		var ext openrtb_ext.ExtRequest
+		if err := json.Unmarshal(req.Ext, &ext); err != nil {
+			return Policy{}, fmt.Errorf("error reading request.ext.prebid: %s", err)
+		}
+		noSaleBidders = ext.Prebid.NoSale
+	}
+
+	return Policy{consent, noSaleBidders}, nil
 }
 
-// Write mutates an OpenRTB bid request with the context of the CCPA policy.
+// Write mutates an OpenRTB bid request with the CCPA regulatory information.
 func (p Policy) Write(req *openrtb.BidRequest) error {
-	if p.Value == "" {
-		return clearPolicy(req)
-	}
-
 	if req == nil {
 		return nil
 	}
 
-	if req.Regs == nil {
-		req.Regs = &openrtb.Regs{}
+	regs, err := buildRegs(p.Consent, req.Regs)
+	if err != nil {
+		return err
 	}
-
-	if req.Regs.Ext == nil {
-		ext, err := json.Marshal(openrtb_ext.ExtRegs{USPrivacy: p.Value})
-		if err == nil {
-			req.Regs.Ext = ext
-		}
+	ext, err := buildExt(p.NoSaleBidders, req.Ext)
+	if err != nil {
 		return err
 	}
 
-	var extMap map[string]interface{}
-	err := json.Unmarshal(req.Regs.Ext, &extMap)
-	if err == nil {
-		extMap["us_privacy"] = p.Value
-		ext, err := json.Marshal(extMap)
-		if err == nil {
-			req.Regs.Ext = ext
-		}
+	req.Regs = regs
+	req.Ext = ext
+	return nil
+}
+
+func buildRegs(consent string, regs *openrtb.Regs) (*openrtb.Regs, error) {
+	if consent == "" {
+		return buildRegsClear(regs)
 	}
-	return err
+	return buildRegsWrite(consent, regs)
 }
 
-func clearPolicy(req *openrtb.BidRequest) error {
-	if req == nil {
-		return nil
+func buildRegsClear(regs *openrtb.Regs) (*openrtb.Regs, error) {
+	if regs == nil || len(regs.Ext) == 0 {
+		return regs, nil
 	}
 
-	if req.Regs == nil {
-		return nil
+	var extMap map[string]interface{}
+	if err := json.Unmarshal(regs.Ext, &extMap); err != nil {
+		return nil, err
 	}
 
-	if len(req.Regs.Ext) == 0 {
-		return nil
+	delete(extMap, "us_privacy")
+
+	// Remove entire ext if it's now empty
+	if len(extMap) == 0 {
+		regsResult := *regs
+		regsResult.Ext = nil
+		return &regsResult, nil
 	}
 
-	var extMap map[string]interface{}
-	err := json.Unmarshal(req.Regs.Ext, &extMap)
+	// Marshal ext if there are still other fields
+	var regsResult openrtb.Regs
+	ext, err := json.Marshal(extMap)
 	if err == nil {
-		delete(extMap, "us_privacy")
-		if len(extMap) == 0 {
-			req.Regs.Ext = nil
-		} else {
-			ext, err := json.Marshal(extMap)
-			if err == nil {
-				req.Regs.Ext = ext
-			}
-			return err
-		}
+		regsResult = *regs
+		regsResult.Ext = ext
 	}
-
-	return err
+	return &regsResult, err
 }
 
-// Validate returns an error if the CCPA policy does not adhere to the IAB spec.
-func (p Policy) Validate() error {
-	if err := ValidateConsent(p.Value); err != nil {
-		return fmt.Errorf("request.regs.ext.us_privacy %s", err.Error())
+func buildRegsWrite(consent string, regs *openrtb.Regs) (*openrtb.Regs, error) {
+	if regs == nil {
+		return marshalRegsExt(openrtb.Regs{}, openrtb_ext.ExtRegs{USPrivacy: consent})
 	}
 
-	return nil
+	if regs.Ext == nil {
+		return marshalRegsExt(*regs, openrtb_ext.ExtRegs{USPrivacy: consent})
+	}
+
+	var extMap map[string]interface{}
+	if err := json.Unmarshal(regs.Ext, &extMap); err != nil {
+		return nil, err
+	}
+
+	extMap["us_privacy"] = consent
+	return marshalRegsExt(*regs, extMap)
 }
 
-// ValidateConsent returns an error if the CCPA consent string does not adhere to the IAB spec.
-func ValidateConsent(consent string) error {
-	if consent == "" {
-		return nil
+func marshalRegsExt(regs openrtb.Regs, ext interface{}) (*openrtb.Regs, error) {
+	extJSON, err := json.Marshal(ext)
+	if err == nil {
+		regs.Ext = extJSON
 	}
+	return &regs, err
+}
 
-	if len(consent) != 4 {
-		return errors.New("must contain 4 characters")
+func buildExt(noSaleBidders []string, ext json.RawMessage) (json.RawMessage, error) {
+	if len(noSaleBidders) == 0 {
+		return buildExtClear(ext)
 	}
+	return buildExtWrite(noSaleBidders, ext)
+}
 
-	if consent[0] != '1' {
-		return errors.New("must specify version 1")
+func buildExtClear(ext json.RawMessage) (json.RawMessage, error) {
+	if len(ext) == 0 {
+		return ext, nil
 	}
 
-	var c byte
+	var extMap map[string]interface{}
+	if err := json.Unmarshal(ext, &extMap); err != nil {
+		return nil, err
+	}
 
-	c = consent[1]
-	if c != 'N' && c != 'Y' && c != '-' {
-		return errors.New("must specify 'N', 'Y', or '-' for the explicit notice")
+	prebidExt, exists := extMap["prebid"]
+	if !exists {
+		return ext, nil
 	}
 
-	c = consent[2]
-	if c != 'N' && c != 'Y' && c != '-' {
-		return errors.New("must specify 'N', 'Y', or '-' for the opt-out sale")
+	// Verify prebid is an object
+	prebidExtMap, ok := prebidExt.(map[string]interface{})
+	if !ok {
+		return nil, errors.New("request.ext.prebid is not a json object")
 	}
 
-	c = consent[3]
-	if c != 'N' && c != 'Y' && c != '-' {
-		return errors.New("must specify 'N', 'Y', or '-' for the limited service provider agreement")
+	// Remove no sale member
+	delete(prebidExtMap, "nosale")
+	if len(prebidExtMap) == 0 {
+		delete(extMap, "prebid")
 	}
 
-	return nil
+	// Remove entire ext if it's empty
+	if len(extMap) == 0 {
+		return nil, nil
+	}
+
+	return json.Marshal(extMap)
 }
 
-// ShouldEnforce returns true when the opt-out signal is explicitly detected.
-func (p Policy) ShouldEnforce() bool {
-	if err := p.Validate(); err != nil {
-		return false
+func buildExtWrite(noSaleBidders []string, ext json.RawMessage) (json.RawMessage, error) {
+	if len(ext) == 0 {
+		return json.Marshal(openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{NoSale: noSaleBidders}})
+	}
+
+	var extMap map[string]interface{}
+	if err := json.Unmarshal(ext, &extMap); err != nil {
+		return nil, err
+	}
+
+	var prebidExt map[string]interface{}
+	if prebidExtInterface, exists := extMap["prebid"]; exists {
+		// Reference Existing Prebid Ext Map
+		if prebidExtMap, ok := prebidExtInterface.(map[string]interface{}); ok {
+			prebidExt = prebidExtMap
+		} else {
+			return nil, errors.New("request.ext.prebid is not a json object")
+		}
+	} else {
+		// Create New Empty Prebid Ext Map
+		prebidExt = make(map[string]interface{})
+		extMap["prebid"] = prebidExt
 	}
 
-	return p.Value != "" && p.Value[2] == 'Y'
+	prebidExt["nosale"] = noSaleBidders
+	return json.Marshal(extMap)
 }
diff --git a/privacy/enforcer.go b/privacy/enforcer.go
new file mode 100644
index 00000000..0d5ecad5
--- /dev/null
+++ b/privacy/enforcer.go
@@ -0,0 +1,43 @@
+package privacy
+
+// PolicyEnforcer determines if personally identifiable information (PII) should be removed or anonymized per the policy.
+type PolicyEnforcer interface {
+	// CanEnforce returns true when policy information is specifically provided by the publisher.
+	CanEnforce() bool
+
+	// ShouldEnforce returns true when the OpenRTB request should have personally identifiable
+	// information (PII) removed or anonymized per the policy.
+	ShouldEnforce(bidder string) bool
+}
+
+// NilPolicyEnforcer implements the PolicyEnforcer interface but will always return false.
+type NilPolicyEnforcer struct{}
+
+// CanEnforce is hardcoded to always return false.
+func (NilPolicyEnforcer) CanEnforce() bool {
+	return false
+}
+
+// ShouldEnforce is hardcoded to always return false.
+func (NilPolicyEnforcer) ShouldEnforce(bidder string) bool {
+	return false
+}
+
+// EnabledPolicyEnforcer decorates a PolicyEnforcer with an enabled flag.
+type EnabledPolicyEnforcer struct {
+	Enabled        bool
+	PolicyEnforcer PolicyEnforcer
+}
+
+// CanEnforce returns true when the PolicyEnforcer can enforce.
+func (p EnabledPolicyEnforcer) CanEnforce() bool {
+	return p.PolicyEnforcer.CanEnforce()
+}
+
+// ShouldEnforce returns true when the enforcer is enabled the PolicyEnforcer allows enforcement.
+func (p EnabledPolicyEnforcer) ShouldEnforce(bidder string) bool {
+	if p.Enabled {
+		return p.PolicyEnforcer.ShouldEnforce(bidder)
+	}
+	return false
+}
diff --git a/privacy/gdpr/consentwriter.go b/privacy/gdpr/consentwriter.go
new file mode 100644
index 00000000..040bbd6c
--- /dev/null
+++ b/privacy/gdpr/consentwriter.go
@@ -0,0 +1,44 @@
+package gdpr
+
+import (
+	"encoding/json"
+
+	"github.com/prebid/prebid-server/openrtb_ext"
+
+	"github.com/mxmCherry/openrtb"
+)
+
+// ConsentWriter implements the PolicyWriter interface for GDPR TCF.
+type ConsentWriter struct {
+	Consent string
+}
+
+// Write mutates an OpenRTB bid request with the GDPR TCF consent.
+func (c ConsentWriter) Write(req *openrtb.BidRequest) error {
+	if c.Consent == "" {
+		return nil
+	}
+
+	if req.User == nil {
+		req.User = &openrtb.User{}
+	}
+
+	if req.User.Ext == nil {
+		ext, err := json.Marshal(openrtb_ext.ExtUser{Consent: c.Consent})
+		if err == nil {
+			req.User.Ext = ext
+		}
+		return err
+	}
+
+	var extMap map[string]interface{}
+	err := json.Unmarshal(req.User.Ext, &extMap)
+	if err == nil {
+		extMap["consent"] = c.Consent
+		ext, err := json.Marshal(extMap)
+		if err == nil {
+			req.User.Ext = ext
+		}
+	}
+	return err
+}
diff --git a/privacy/gdpr/policy.go b/privacy/gdpr/policy.go
index 4733e1ed..0464a9ff 100644
--- a/privacy/gdpr/policy.go
+++ b/privacy/gdpr/policy.go
@@ -1,10 +1,6 @@
 package gdpr
 
 import (
-	"encoding/json"
-	"github.com/prebid/prebid-server/openrtb_ext"
-
-	"github.com/mxmCherry/openrtb"
 	"github.com/prebid/go-gdpr/vendorconsent"
 )
 
@@ -14,38 +10,8 @@ type Policy struct {
 	Consent string
 }
 
-// Write mutates an OpenRTB bid request with the context of the GDPR policy.
-func (p Policy) Write(req *openrtb.BidRequest) error {
-	if p.Consent == "" {
-		return nil
-	}
-
-	if req.User == nil {
-		req.User = &openrtb.User{}
-	}
-
-	if req.User.Ext == nil {
-		ext, err := json.Marshal(openrtb_ext.ExtUser{Consent: p.Consent})
-		if err == nil {
-			req.User.Ext = ext
-		}
-		return err
-	}
-
-	var extMap map[string]interface{}
-	err := json.Unmarshal(req.User.Ext, &extMap)
-	if err == nil {
-		extMap["consent"] = p.Consent
-		ext, err := json.Marshal(extMap)
-		if err == nil {
-			req.User.Ext = ext
-		}
-	}
-	return err
-}
-
-// ValidateConsent returns an error if the GDPR consent string does not adhere to the IAB TCF spec.
-func ValidateConsent(consent string) error {
+// ValidateConsent returns true if the consent string is empty or valid per the IAB TCF spec.
+func ValidateConsent(consent string) bool {
 	_, err := vendorconsent.ParseString(consent)
-	return err
+	return err == nil
 }
diff --git a/privacy/lmt/policy.go b/privacy/lmt/policy.go
index 79425bf5..295dcc46 100644
--- a/privacy/lmt/policy.go
+++ b/privacy/lmt/policy.go
@@ -15,19 +15,21 @@ type Policy struct {
 	SignalProvided bool
 }
 
-// ReadPolicy extracts the LMT (Limit Ad Tracking) policy from an OpenRTB bid request.
-func ReadPolicy(req *openrtb.BidRequest) Policy {
-	policy := Policy{}
-
+// ReadFromRequest extracts the LMT (Limit Ad Tracking) policy from an OpenRTB bid request.
+func ReadFromRequest(req *openrtb.BidRequest) (policy Policy) {
 	if req != nil && req.Device != nil && req.Device.Lmt != nil {
 		policy.Signal = int(*req.Device.Lmt)
 		policy.SignalProvided = true
 	}
+	return
+}
 
-	return policy
+// CanEnforce returns true the LMT (Limit Ad Tracking) signal is provided by the publisher.
+func (p Policy) CanEnforce() bool {
+	return p.SignalProvided
 }
 
 // ShouldEnforce returns true when the LMT (Limit Ad Tracking) policy is in effect.
-func (p Policy) ShouldEnforce() bool {
+func (p Policy) ShouldEnforce(bidder string) bool {
 	return p.SignalProvided && p.Signal == trackingRestricted
 }
diff --git a/privacy/policies.go b/privacy/policies.go
index cb11c6d0..bc844a4e 100644
--- a/privacy/policies.go
+++ b/privacy/policies.go
@@ -1,60 +1,14 @@
 package privacy
 
 import (
-	"github.com/mxmCherry/openrtb"
-
 	"github.com/prebid/prebid-server/privacy/ccpa"
 	"github.com/prebid/prebid-server/privacy/gdpr"
+	"github.com/prebid/prebid-server/privacy/lmt"
 )
 
 // Policies represents the privacy regulations for an OpenRTB bid request.
 type Policies struct {
-	GDPR gdpr.Policy
 	CCPA ccpa.Policy
-}
-
-type policyWriter interface {
-	Write(req *openrtb.BidRequest) error
-}
-
-// Write mutates an OpenRTB bid request with the policies applied.
-func (p Policies) Write(req *openrtb.BidRequest) error {
-	return writePolicies(req, []policyWriter{
-		p.GDPR, p.CCPA,
-	})
-}
-
-func writePolicies(req *openrtb.BidRequest, writers []policyWriter) error {
-	for _, writer := range writers {
-		if err := writer.Write(req); err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-// ReadPoliciesFromConsent inspects the consent string kind and sets the corresponding values in a new Policies object.
-func ReadPoliciesFromConsent(consent string) (Policies, bool) {
-	if len(consent) == 0 {
-		return Policies{}, false
-	}
-
-	if err := gdpr.ValidateConsent(consent); err == nil {
-		return Policies{
-			GDPR: gdpr.Policy{
-				Consent: consent,
-			},
-		}, true
-	}
-
-	if err := ccpa.ValidateConsent(consent); err == nil {
-		return Policies{
-			CCPA: ccpa.Policy{
-				Value: consent,
-			},
-		}, true
-	}
-
-	return Policies{}, false
+	GDPR gdpr.Policy
+	LMT  lmt.Policy
 }
diff --git a/privacy/writer.go b/privacy/writer.go
new file mode 100644
index 00000000..c61767f1
--- /dev/null
+++ b/privacy/writer.go
@@ -0,0 +1,18 @@
+package privacy
+
+import (
+	"github.com/mxmCherry/openrtb"
+)
+
+// PolicyWriter mutates an OpenRTB bid request with a policy's regulatory information.
+type PolicyWriter interface {
+	Write(req *openrtb.BidRequest) error
+}
+
+// NilPolicyWriter implements the PolicyWriter interface but performs no action.
+type NilPolicyWriter struct{}
+
+// Write is hardcoded to perform no action with the OpenRTB bid request.
+func (NilPolicyWriter) Write(req *openrtb.BidRequest) error {
+	return nil
+}
diff --git a/static/bidder-params/conversant.json b/static/bidder-params/conversant.json
index 4f7200ac..ba9c6bd5 100644
--- a/static/bidder-params/conversant.json
+++ b/static/bidder-params/conversant.json
@@ -24,10 +24,6 @@
       "type": "integer",
       "description": "Ad position on screen."
    	},
-   	"mobile": {
-   	  "type": "integer",
-   	  "description": "Indicate if the site is mobile optimized."
-   	},
    	"mimes": {
    	  "type": "array",
    	  "description": "Array of content MIME types.  For videos only.",


Test patch:
diff --git a/adapters/33across/usersync_test.go b/adapters/33across/usersync_test.go
index a5e301b1..a9eb4e57 100644
--- a/adapters/33across/usersync_test.go
+++ b/adapters/33across/usersync_test.go
@@ -23,7 +23,7 @@ func Test33AcrossSyncer(t *testing.T) {
 			Consent: "B",
 		},
 		CCPA: ccpa.Policy{
-			Value: "C",
+			Consent: "C",
 		},
 	})
 
diff --git a/adapters/adkernel/usersync_test.go b/adapters/adkernel/usersync_test.go
index 0d539d11..aeacf00b 100644
--- a/adapters/adkernel/usersync_test.go
+++ b/adapters/adkernel/usersync_test.go
@@ -23,7 +23,7 @@ func TestAdkernelAdnSyncer(t *testing.T) {
 			Consent: "BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw",
 		},
 		CCPA: ccpa.Policy{
-			Value: "1NYN",
+			Consent: "1NYN",
 		},
 	})
 
diff --git a/adapters/adkernelAdn/usersync_test.go b/adapters/adkernelAdn/usersync_test.go
index ecc759bd..92d688e6 100644
--- a/adapters/adkernelAdn/usersync_test.go
+++ b/adapters/adkernelAdn/usersync_test.go
@@ -23,7 +23,7 @@ func TestAdkernelAdnSyncer(t *testing.T) {
 			Consent: "BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw",
 		},
 		CCPA: ccpa.Policy{
-			Value: "1NYN",
+			Consent: "1NYN",
 		},
 	})
 
diff --git a/adapters/adman/usersync_test.go b/adapters/adman/usersync_test.go
index 55a6e2ce..25da77db 100644
--- a/adapters/adman/usersync_test.go
+++ b/adapters/adman/usersync_test.go
@@ -23,7 +23,7 @@ func TestAdmanSyncer(t *testing.T) {
 			Consent: "ANDFJDS",
 		},
 		CCPA: ccpa.Policy{
-			Value: "1-YY",
+			Consent: "1-YY",
 		},
 	})
 
diff --git a/adapters/admixer/usersync_test.go b/adapters/admixer/usersync_test.go
index a5715c64..d31f7b10 100644
--- a/adapters/admixer/usersync_test.go
+++ b/adapters/admixer/usersync_test.go
@@ -1,12 +1,13 @@
 package admixer
 
 import (
+	"testing"
+	"text/template"
+
 	"github.com/prebid/prebid-server/privacy"
 	"github.com/prebid/prebid-server/privacy/ccpa"
 	"github.com/prebid/prebid-server/privacy/gdpr"
 	"github.com/stretchr/testify/assert"
-	"testing"
-	"text/template"
 )
 
 func TestAdmixerSyncer(t *testing.T) {
@@ -22,7 +23,7 @@ func TestAdmixerSyncer(t *testing.T) {
 			Consent: "B",
 		},
 		CCPA: ccpa.Policy{
-			Value: "C",
+			Consent: "C",
 		},
 	})
 
diff --git a/adapters/adtarget/usersync_test.go b/adapters/adtarget/usersync_test.go
index 3ab2ed5b..ddba9e7a 100644
--- a/adapters/adtarget/usersync_test.go
+++ b/adapters/adtarget/usersync_test.go
@@ -2,10 +2,11 @@ package adtarget
 
 import (
 	"fmt"
-	"github.com/prebid/prebid-server/privacy/ccpa"
 	"testing"
 	"text/template"
 
+	"github.com/prebid/prebid-server/privacy/ccpa"
+
 	"github.com/prebid/prebid-server/privacy"
 	"github.com/prebid/prebid-server/privacy/gdpr"
 	"github.com/stretchr/testify/assert"
@@ -25,7 +26,7 @@ func TestAdtargetSyncer(t *testing.T) {
 			Consent: "123",
 		},
 		CCPA: ccpa.Policy{
-			Value: "1-YY",
+			Consent: "1-YY",
 		},
 	})
 
diff --git a/adapters/aja/usersync_test.go b/adapters/aja/usersync_test.go
index dbb66cc9..4b6c90ef 100644
--- a/adapters/aja/usersync_test.go
+++ b/adapters/aja/usersync_test.go
@@ -1,10 +1,11 @@
 package aja
 
 import (
-	"github.com/prebid/prebid-server/privacy/ccpa"
 	"testing"
 	"text/template"
 
+	"github.com/prebid/prebid-server/privacy/ccpa"
+
 	"github.com/prebid/prebid-server/privacy"
 	"github.com/prebid/prebid-server/privacy/gdpr"
 	"github.com/stretchr/testify/assert"
@@ -23,7 +24,7 @@ func TestAJASyncer(t *testing.T) {
 			Consent: "BOPVK28OVJoTBABABAENBs-AAAAhuAKAANAAoACwAGgAPAAxAB0AHgAQAAiABOADkA",
 		},
 		CCPA: ccpa.Policy{
-			Value: "C",
+			Consent: "C",
 		},
 	})
 
diff --git a/adapters/avocet/usersync_test.go b/adapters/avocet/usersync_test.go
index 8fba403f..3df39b77 100644
--- a/adapters/avocet/usersync_test.go
+++ b/adapters/avocet/usersync_test.go
@@ -23,7 +23,7 @@ func TestAvocetSyncer(t *testing.T) {
 			Consent: "ConsentString",
 		},
 		CCPA: ccpa.Policy{
-			Value: "PrivacyString",
+			Consent: "PrivacyString",
 		},
 	})
 
diff --git a/adapters/beachfront/usersync_test.go b/adapters/beachfront/usersync_test.go
index 0267ac05..db4d825e 100644
--- a/adapters/beachfront/usersync_test.go
+++ b/adapters/beachfront/usersync_test.go
@@ -23,7 +23,7 @@ func TestBeachfrontSyncer(t *testing.T) {
 			Consent: "B",
 		},
 		CCPA: ccpa.Policy{
-			Value: "C",
+			Consent: "C",
 		},
 	})
 
diff --git a/adapters/beintoo/usersync_test.go b/adapters/beintoo/usersync_test.go
index 2cfca010..880d6a84 100644
--- a/adapters/beintoo/usersync_test.go
+++ b/adapters/beintoo/usersync_test.go
@@ -23,7 +23,7 @@ func TestBeintooSyncer(t *testing.T) {
 			Consent: "BOPVK28OVJoTBABABAENBs-AAAAhuAKAANAAoACwAGgAPAAxAB0AHgAQAAiABOADkA",
 		},
 		CCPA: ccpa.Policy{
-			Value: "1NYN",
+			Consent: "1NYN",
 		},
 	})
 
diff --git a/adapters/colossus/usersync_test.go b/adapters/colossus/usersync_test.go
index 79d5483d..52eb6389 100644
--- a/adapters/colossus/usersync_test.go
+++ b/adapters/colossus/usersync_test.go
@@ -23,7 +23,7 @@ func TestColossusSyncer(t *testing.T) {
 			Consent: "A",
 		},
 		CCPA: ccpa.Policy{
-			Value: "1-YY",
+			Consent: "1-YY",
 		},
 	})
 
diff --git a/adapters/consumable/usersync_test.go b/adapters/consumable/usersync_test.go
index 017cb729..ef71c0b1 100644
--- a/adapters/consumable/usersync_test.go
+++ b/adapters/consumable/usersync_test.go
@@ -23,7 +23,7 @@ func TestConsumableSyncer(t *testing.T) {
 			Consent: "B",
 		},
 		CCPA: ccpa.Policy{
-			Value: "C",
+			Consent: "C",
 		},
 	})
 
diff --git a/adapters/conversant/cnvr_legacy_test.go b/adapters/conversant/cnvr_legacy_test.go
new file mode 100644
index 00000000..b958e320
--- /dev/null
+++ b/adapters/conversant/cnvr_legacy_test.go
@@ -0,0 +1,853 @@
+package conversant
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+	"time"
+
+	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/cache/dummycache"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/pbs"
+	"github.com/prebid/prebid-server/usersync"
+)
+
+// Constants
+
+const ExpectedSiteID string = "12345"
+const ExpectedDisplayManager string = "prebid-s2s"
+const ExpectedBuyerUID string = "AQECT_o7M1FLbQJK8QFmAQEBAQE"
+const ExpectedNURL string = "http://test.dotomi.com"
+const ExpectedAdM string = "<img src=\"test.jpg\"/>"
+const ExpectedCrID string = "98765"
+
+const DefaultParam = `{"site_id": "12345"}`
+
+// Test properties of Adapter interface
+
+func TestConversantProperties(t *testing.T) {
+	an := NewConversantAdapter(adapters.DefaultHTTPAdapterConfig, "someUrl")
+
+	assertNotEqual(t, an.Name(), "", "Missing family name")
+	assertTrue(t, an.SkipNoCookies(), "SkipNoCookies should be true")
+}
+
+// Test empty bid requests
+
+func TestConversantEmptyBid(t *testing.T) {
+	an := NewConversantAdapter(adapters.DefaultHTTPAdapterConfig, "someUrl")
+
+	ctx := context.TODO()
+	pbReq := pbs.PBSRequest{}
+	pbBidder := pbs.PBSBidder{}
+	_, err := an.Call(ctx, &pbReq, &pbBidder)
+	assertTrue(t, err != nil, "No error received for an invalid request")
+}
+
+// Test required parameters, which is just the site id for now
+
+func TestConversantRequiredParameters(t *testing.T) {
+	server := httptest.NewServer(
+		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			http.Error(w, http.StatusText(http.StatusNoContent), http.StatusNoContent)
+		}),
+	)
+	defer server.Close()
+
+	an := NewConversantAdapter(adapters.DefaultHTTPAdapterConfig, server.URL)
+	ctx := context.TODO()
+
+	testParams := func(params ...string) (pbs.PBSBidSlice, error) {
+		req, err := CreateBannerRequest(params...)
+		if err != nil {
+			return nil, err
+		}
+		return an.Call(ctx, req, req.Bidders[0])
+	}
+
+	var err error
+
+	if _, err = testParams(`{}`); err == nil {
+		t.Fatal("Failed to catch missing site id")
+	}
+}
+
+// Test handling of 404
+
+func TestConversantBadStatus(t *testing.T) {
+	// Create a test http server that returns after 2 milliseconds
+
+	server := httptest.NewServer(
+		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
+		}),
+	)
+	defer server.Close()
+
+	// Create a adapter to test
+
+	conf := *adapters.DefaultHTTPAdapterConfig
+	an := NewConversantAdapter(&conf, server.URL)
+
+	ctx := context.TODO()
+	pbReq, err := CreateBannerRequest(DefaultParam)
+	if err != nil {
+		t.Fatal("Failed to create a banner request", err)
+	}
+
+	_, err = an.Call(ctx, pbReq, pbReq.Bidders[0])
+	assertTrue(t, err != nil, "Failed to catch 404 error")
+}
+
+// Test handling of HTTP timeout
+
+func TestConversantTimeout(t *testing.T) {
+	// Create a test http server that returns after 2 milliseconds
+
+	server := httptest.NewServer(
+		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			<-time.After(2 * time.Millisecond)
+		}),
+	)
+	defer server.Close()
+
+	// Create a adapter to test
+
+	conf := *adapters.DefaultHTTPAdapterConfig
+	an := NewConversantAdapter(&conf, server.URL)
+
+	// Create a context that expires before http returns
+
+	ctx, cancel := context.WithTimeout(context.Background(), 0)
+	defer cancel()
+
+	// Create a basic request
+	pbReq, err := CreateBannerRequest(DefaultParam)
+	if err != nil {
+		t.Fatal("Failed to create a banner request", err)
+	}
+
+	// Attempt to process the request, which should hit a timeout
+	// immediately
+
+	_, err = an.Call(ctx, pbReq, pbReq.Bidders[0])
+	if err == nil || err != context.DeadlineExceeded {
+		t.Fatal("No timeout recevied for timed out request", err)
+	}
+}
+
+// Test handling of 204
+
+func TestConversantNoBid(t *testing.T) {
+	// Create a test http server that returns after 2 milliseconds
+
+	server := httptest.NewServer(
+		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			http.Error(w, http.StatusText(http.StatusNoContent), http.StatusNoContent)
+		}),
+	)
+	defer server.Close()
+
+	// Create a adapter to test
+
+	conf := *adapters.DefaultHTTPAdapterConfig
+	an := NewConversantAdapter(&conf, server.URL)
+
+	ctx := context.TODO()
+	pbReq, err := CreateBannerRequest(DefaultParam)
+	if err != nil {
+		t.Fatal("Failed to create a banner request", err)
+	}
+
+	resp, err := an.Call(ctx, pbReq, pbReq.Bidders[0])
+	if resp != nil || err != nil {
+		t.Fatal("Failed to handle empty bid", err)
+	}
+}
+
+// Verify an outgoing openrtp request is created correctly
+
+func TestConversantRequestDefault(t *testing.T) {
+	server, lastReq := CreateServer()
+	if server == nil {
+		t.Fatal("server not created")
+	}
+
+	defer server.Close()
+
+	// Create a adapter to test
+
+	conf := *adapters.DefaultHTTPAdapterConfig
+	an := NewConversantAdapter(&conf, server.URL)
+
+	ctx := context.TODO()
+	pbReq, err := CreateBannerRequest(DefaultParam)
+	if err != nil {
+		t.Fatal("Failed to create a banner request", err)
+	}
+
+	_, err = an.Call(ctx, pbReq, pbReq.Bidders[0])
+	if err != nil {
+		t.Fatal("Failed to retrieve bids", err)
+	}
+
+	assertEqual(t, len(lastReq.Imp), 1, "Request number of impressions")
+	imp := &lastReq.Imp[0]
+
+	assertEqual(t, imp.DisplayManager, ExpectedDisplayManager, "Request display manager value")
+	assertEqual(t, lastReq.Site.ID, ExpectedSiteID, "Request site id")
+	assertEqual(t, int(lastReq.Site.Mobile), 0, "Request site mobile flag")
+	assertEqual(t, lastReq.User.BuyerUID, ExpectedBuyerUID, "Request buyeruid")
+	assertTrue(t, imp.Video == nil, "Request video should be nil")
+	assertEqual(t, int(*imp.Secure), 0, "Request secure")
+	assertEqual(t, imp.BidFloor, 0.0, "Request bid floor")
+	assertEqual(t, imp.TagID, "", "Request tag id")
+	assertTrue(t, imp.Banner.Pos == nil, "Request pos")
+	assertEqual(t, int(*imp.Banner.W), 300, "Request width")
+	assertEqual(t, int(*imp.Banner.H), 250, "Request height")
+}
+
+// Verify inapp video request
+func TestConversantInappVideoRequest(t *testing.T) {
+	server, lastReq := CreateServer()
+	if server == nil {
+		t.Fatal("server not created")
+	}
+
+	defer server.Close()
+
+	conf := *adapters.DefaultHTTPAdapterConfig
+	an := NewConversantAdapter(&conf, server.URL)
+
+	requestParam := `{"secure": 1, "site_id": "12345"}`
+	appParam := `{ "bundle": "com.naver.linewebtoon" }`
+	videoParam := `{ "mimes": ["video/x-ms-wmv"],
+		   "protocols": [1, 2],
+		   "maxduration": 90 }`
+
+	ctx := context.TODO()
+	pbReq := CreateRequest(requestParam)
+	pbReq, err := ConvertToVideoRequest(pbReq, videoParam)
+	if err != nil {
+		t.Fatal("failed to parse request")
+	}
+	pbReq, err = ConvertToAppRequest(pbReq, appParam)
+	if err != nil {
+		t.Fatal("failed to parse request")
+	}
+	pbReq, err = ParseRequest(pbReq)
+	if err != nil {
+		t.Fatal("failed to parse request")
+	}
+
+	_, err = an.Call(ctx, pbReq, pbReq.Bidders[0])
+
+	imp := &lastReq.Imp[0]
+	assertEqual(t, int(imp.Video.W), 300, "Request width")
+	assertEqual(t, int(imp.Video.H), 250, "Request height")
+	assertEqual(t, lastReq.App.ID, "12345", "App Id")
+}
+
+// Verify inapp video request
+func TestConversantInappBannerRequest(t *testing.T) {
+	server, lastReq := CreateServer()
+	if server == nil {
+		t.Fatal("server not created")
+	}
+
+	defer server.Close()
+
+	conf := *adapters.DefaultHTTPAdapterConfig
+	an := NewConversantAdapter(&conf, server.URL)
+
+	param := `{ "secure": 1,
+		"site_id": "12345",
+		"tag_id": "top",
+		"position": 2,
+		"bidfloor": 1.01 }`
+	appParam := `{ "bundle": "com.naver.linewebtoon" }`
+
+	ctx := context.TODO()
+	pbReq, _ := CreateBannerRequest(param)
+	pbReq, err := ConvertToAppRequest(pbReq, appParam)
+	if err != nil {
+		t.Fatal("failed to parse request")
+	}
+
+	_, err = an.Call(ctx, pbReq, pbReq.Bidders[0])
+
+	imp := &lastReq.Imp[0]
+	assertEqual(t, lastReq.App.ID, "12345", "App Id")
+	assertEqual(t, int(*imp.Banner.W), 300, "Request width")
+	assertEqual(t, int(*imp.Banner.H), 250, "Request height")
+}
+
+// Verify an outgoing openrtp request with additional conversant parameters is
+// processed correctly
+
+func TestConversantRequest(t *testing.T) {
+	server, lastReq := CreateServer()
+	if server == nil {
+		t.Fatal("server not created")
+	}
+
+	defer server.Close()
+
+	// Create a adapter to test
+
+	conf := *adapters.DefaultHTTPAdapterConfig
+	an := NewConversantAdapter(&conf, server.URL)
+
+	param := `{ "site_id": "12345",
+		"secure": 1,
+		"tag_id": "top",
+		"position": 2,
+		"bidfloor": 1.01,
+		"mobile": 1 }`
+
+	ctx := context.TODO()
+	pbReq, err := CreateBannerRequest(param)
+	if err != nil {
+		t.Fatal("Failed to create a banner request", err)
+	}
+
+	_, err = an.Call(ctx, pbReq, pbReq.Bidders[0])
+	if err != nil {
+		t.Fatal("Failed to retrieve bids", err)
+	}
+
+	assertEqual(t, len(lastReq.Imp), 1, "Request number of impressions")
+	imp := &lastReq.Imp[0]
+
+	assertEqual(t, imp.DisplayManager, ExpectedDisplayManager, "Request display manager value")
+	assertEqual(t, lastReq.Site.ID, ExpectedSiteID, "Request site id")
+	assertEqual(t, int(lastReq.Site.Mobile), 1, "Request site mobile flag")
+	assertEqual(t, lastReq.User.BuyerUID, ExpectedBuyerUID, "Request buyeruid")
+	assertTrue(t, imp.Video == nil, "Request video should be nil")
+	assertEqual(t, int(*imp.Secure), 1, "Request secure")
+	assertEqual(t, imp.BidFloor, 1.01, "Request bid floor")
+	assertEqual(t, imp.TagID, "top", "Request tag id")
+	assertEqual(t, int(*imp.Banner.Pos), 2, "Request pos")
+	assertEqual(t, int(*imp.Banner.W), 300, "Request width")
+	assertEqual(t, int(*imp.Banner.H), 250, "Request height")
+}
+
+// Verify openrtp responses are converted correctly
+
+func TestConversantResponse(t *testing.T) {
+	prices := []float64{0.01, 0.0, 2.01}
+	server, lastReq := CreateServer(prices...)
+	if server == nil {
+		t.Fatal("server not created")
+	}
+
+	defer server.Close()
+
+	// Create a adapter to test
+
+	conf := *adapters.DefaultHTTPAdapterConfig
+	an := NewConversantAdapter(&conf, server.URL)
+
+	param := `{ "site_id": "12345",
+		   "secure": 1,
+		   "tag_id": "top",
+		   "position": 2,
+		   "bidfloor": 1.01,
+		   "mobile" : 1}`
+
+	ctx := context.TODO()
+	pbReq, err := CreateBannerRequest(param, param, param)
+	if err != nil {
+		t.Fatal("Failed to create a banner request", err)
+	}
+
+	resp, err := an.Call(ctx, pbReq, pbReq.Bidders[0])
+	if err != nil {
+		t.Fatal("Failed to retrieve bids", err)
+	}
+
+	prices, imps := FilterZeroPrices(prices, lastReq.Imp)
+
+	assertEqual(t, len(resp), len(prices), "Bad number of responses")
+
+	for i, bid := range resp {
+		assertEqual(t, bid.Price, prices[i], "Bad price in response")
+		assertEqual(t, bid.AdUnitCode, imps[i].ID, "Bad bid id in response")
+
+		if bid.Price > 0 {
+			assertEqual(t, bid.Adm, ExpectedAdM, "Bad ad markup in response")
+			assertEqual(t, bid.NURL, ExpectedNURL, "Bad notification url in response")
+			assertEqual(t, bid.Creative_id, ExpectedCrID, "Bad creative id in response")
+			assertEqual(t, bid.Width, *imps[i].Banner.W, "Bad width in response")
+			assertEqual(t, bid.Height, *imps[i].Banner.H, "Bad height in response")
+		}
+	}
+}
+
+// Test video request
+
+func TestConversantBasicVideoRequest(t *testing.T) {
+	server, lastReq := CreateServer()
+	if server == nil {
+		t.Fatal("server not created")
+	}
+
+	defer server.Close()
+
+	// Create a adapter to test
+
+	conf := *adapters.DefaultHTTPAdapterConfig
+	an := NewConversantAdapter(&conf, server.URL)
+
+	param := `{ "site_id": "12345",
+		   "tag_id": "bottom left",
+		   "position": 3,
+		   "bidfloor": 1.01 }`
+
+	ctx := context.TODO()
+	pbReq, err := CreateVideoRequest(param)
+	if err != nil {
+		t.Fatal("Failed to create a video request", err)
+	}
+
+	_, err = an.Call(ctx, pbReq, pbReq.Bidders[0])
+	if err != nil {
+		t.Fatal("Failed to retrieve bids", err)
+	}
+
+	assertEqual(t, len(lastReq.Imp), 1, "Request number of impressions")
+	imp := &lastReq.Imp[0]
+
+	assertEqual(t, imp.DisplayManager, ExpectedDisplayManager, "Request display manager value")
+	assertEqual(t, lastReq.Site.ID, ExpectedSiteID, "Request site id")
+	assertEqual(t, int(lastReq.Site.Mobile), 0, "Request site mobile flag")
+	assertEqual(t, lastReq.User.BuyerUID, ExpectedBuyerUID, "Request buyeruid")
+	assertTrue(t, imp.Banner == nil, "Request banner should be nil")
+	assertEqual(t, int(*imp.Secure), 0, "Request secure")
+	assertEqual(t, imp.BidFloor, 1.01, "Request bid floor")
+	assertEqual(t, imp.TagID, "bottom left", "Request tag id")
+	assertEqual(t, int(*imp.Video.Pos), 3, "Request pos")
+	assertEqual(t, int(imp.Video.W), 300, "Request width")
+	assertEqual(t, int(imp.Video.H), 250, "Request height")
+
+	assertEqual(t, len(imp.Video.MIMEs), 1, "Request video MIMEs entries")
+	assertEqual(t, imp.Video.MIMEs[0], "video/mp4", "Requst video MIMEs type")
+	assertTrue(t, imp.Video.Protocols == nil, "Request video protocols")
+	assertEqual(t, imp.Video.MaxDuration, int64(0), "Request video 0 max duration")
+	assertTrue(t, imp.Video.API == nil, "Request video api should be nil")
+}
+
+// Test video request with parameters in custom params object
+
+func TestConversantVideoRequestWithParams(t *testing.T) {
+	server, lastReq := CreateServer()
+	if server == nil {
+		t.Fatal("server not created")
+	}
+
+	defer server.Close()
+
+	// Create a adapter to test
+
+	conf := *adapters.DefaultHTTPAdapterConfig
+	an := NewConversantAdapter(&conf, server.URL)
+
+	param := `{ "site_id": "12345",
+		   "tag_id": "bottom left",
+		   "position": 3,
+		   "bidfloor": 1.01,
+		   "mimes": ["video/x-ms-wmv"],
+		   "protocols": [1, 2],
+		   "api": [1, 2],
+		   "maxduration": 90 }`
+
+	ctx := context.TODO()
+	pbReq, err := CreateVideoRequest(param)
+	if err != nil {
+		t.Fatal("Failed to create a video request", err)
+	}
+
+	_, err = an.Call(ctx, pbReq, pbReq.Bidders[0])
+	if err != nil {
+		t.Fatal("Failed to retrieve bids", err)
+	}
+
+	assertEqual(t, len(lastReq.Imp), 1, "Request number of impressions")
+	imp := &lastReq.Imp[0]
+
+	assertEqual(t, imp.DisplayManager, ExpectedDisplayManager, "Request display manager value")
+	assertEqual(t, lastReq.Site.ID, ExpectedSiteID, "Request site id")
+	assertEqual(t, int(lastReq.Site.Mobile), 0, "Request site mobile flag")
+	assertEqual(t, lastReq.User.BuyerUID, ExpectedBuyerUID, "Request buyeruid")
+	assertTrue(t, imp.Banner == nil, "Request banner should be nil")
+	assertEqual(t, int(*imp.Secure), 0, "Request secure")
+	assertEqual(t, imp.BidFloor, 1.01, "Request bid floor")
+	assertEqual(t, imp.TagID, "bottom left", "Request tag id")
+	assertEqual(t, int(*imp.Video.Pos), 3, "Request pos")
+	assertEqual(t, int(imp.Video.W), 300, "Request width")
+	assertEqual(t, int(imp.Video.H), 250, "Request height")
+
+	assertEqual(t, len(imp.Video.MIMEs), 1, "Request video MIMEs entries")
+	assertEqual(t, imp.Video.MIMEs[0], "video/x-ms-wmv", "Requst video MIMEs type")
+	assertEqual(t, len(imp.Video.Protocols), 2, "Request video protocols")
+	assertEqual(t, imp.Video.Protocols[0], openrtb.Protocol(1), "Request video protocols 1")
+	assertEqual(t, imp.Video.Protocols[1], openrtb.Protocol(2), "Request video protocols 2")
+	assertEqual(t, imp.Video.MaxDuration, int64(90), "Request video 0 max duration")
+	assertEqual(t, len(imp.Video.API), 2, "Request video api should be nil")
+	assertEqual(t, imp.Video.API[0], openrtb.APIFramework(1), "Request video api 1")
+	assertEqual(t, imp.Video.API[1], openrtb.APIFramework(2), "Request video api 2")
+}
+
+// Test video request with parameters in the video object
+
+func TestConversantVideoRequestWithParams2(t *testing.T) {
+	server, lastReq := CreateServer()
+	if server == nil {
+		t.Fatal("server not created")
+	}
+
+	defer server.Close()
+
+	// Create a adapter to test
+
+	conf := *adapters.DefaultHTTPAdapterConfig
+	an := NewConversantAdapter(&conf, server.URL)
+
+	param := `{ "site_id": "12345" }`
+	videoParam := `{ "mimes": ["video/x-ms-wmv"],
+		   "protocols": [1, 2],
+		   "maxduration": 90 }`
+
+	ctx := context.TODO()
+	pbReq := CreateRequest(param)
+	pbReq, err := ConvertToVideoRequest(pbReq, videoParam)
+	if err != nil {
+		t.Fatal("Failed to convert to a video request", err)
+	}
+	pbReq, err = ParseRequest(pbReq)
+	if err != nil {
+		t.Fatal("Failed to parse video request", err)
+	}
+
+	_, err = an.Call(ctx, pbReq, pbReq.Bidders[0])
+	if err != nil {
+		t.Fatal("Failed to retrieve bids", err)
+	}
+
+	assertEqual(t, len(lastReq.Imp), 1, "Request number of impressions")
+	imp := &lastReq.Imp[0]
+
+	assertEqual(t, imp.DisplayManager, ExpectedDisplayManager, "Request display manager value")
+	assertEqual(t, lastReq.Site.ID, ExpectedSiteID, "Request site id")
+	assertEqual(t, int(lastReq.Site.Mobile), 0, "Request site mobile flag")
+	assertEqual(t, lastReq.User.BuyerUID, ExpectedBuyerUID, "Request buyeruid")
+	assertTrue(t, imp.Banner == nil, "Request banner should be nil")
+	assertEqual(t, int(*imp.Secure), 0, "Request secure")
+	assertEqual(t, imp.BidFloor, 0.0, "Request bid floor")
+	assertEqual(t, int(imp.Video.W), 300, "Request width")
+	assertEqual(t, int(imp.Video.H), 250, "Request height")
+
+	assertEqual(t, len(imp.Video.MIMEs), 1, "Request video MIMEs entries")
+	assertEqual(t, imp.Video.MIMEs[0], "video/x-ms-wmv", "Requst video MIMEs type")
+	assertEqual(t, len(imp.Video.Protocols), 2, "Request video protocols")
+	assertEqual(t, imp.Video.Protocols[0], openrtb.Protocol(1), "Request video protocols 1")
+	assertEqual(t, imp.Video.Protocols[1], openrtb.Protocol(2), "Request video protocols 2")
+	assertEqual(t, imp.Video.MaxDuration, int64(90), "Request video 0 max duration")
+}
+
+// Test video responses
+
+func TestConversantVideoResponse(t *testing.T) {
+	prices := []float64{0.01, 0.0, 2.01}
+	server, lastReq := CreateServer(prices...)
+	if server == nil {
+		t.Fatal("server not created")
+	}
+
+	defer server.Close()
+
+	// Create a adapter to test
+
+	conf := *adapters.DefaultHTTPAdapterConfig
+	an := NewConversantAdapter(&conf, server.URL)
+
+	param := `{ "site_id": "12345",
+		   "secure": 1,
+		   "tag_id": "top",
+		   "position": 2,
+		   "bidfloor": 1.01,
+		   "mobile" : 1}`
+
+	ctx := context.TODO()
+	pbReq, err := CreateVideoRequest(param, param, param)
+	if err != nil {
+		t.Fatal("Failed to create a video request", err)
+	}
+
+	resp, err := an.Call(ctx, pbReq, pbReq.Bidders[0])
+	if err != nil {
+		t.Fatal("Failed to retrieve bids", err)
+	}
+
+	prices, imps := FilterZeroPrices(prices, lastReq.Imp)
+
+	assertEqual(t, len(resp), len(prices), "Bad number of responses")
+
+	for i, bid := range resp {
+		assertEqual(t, bid.Price, prices[i], "Bad price in response")
+		assertEqual(t, bid.AdUnitCode, imps[i].ID, "Bad bid id in response")
+
+		if bid.Price > 0 {
+			assertEqual(t, bid.Adm, "", "Bad ad markup in response")
+			assertEqual(t, bid.NURL, ExpectedAdM, "Bad notification url in response")
+			assertEqual(t, bid.Creative_id, ExpectedCrID, "Bad creative id in response")
+			assertEqual(t, bid.Width, imps[i].Video.W, "Bad width in response")
+			assertEqual(t, bid.Height, imps[i].Video.H, "Bad height in response")
+		}
+	}
+}
+
+// Helpers to create a banner and video requests
+
+func CreateRequest(params ...string) *pbs.PBSRequest {
+	num := len(params)
+
+	req := pbs.PBSRequest{
+		Tid:       "t-000",
+		AccountID: "1",
+		AdUnits:   make([]pbs.AdUnit, num),
+	}
+
+	for i := 0; i < num; i++ {
+		req.AdUnits[i] = pbs.AdUnit{
+			Code: fmt.Sprintf("au-%03d", i),
+			Sizes: []openrtb.Format{
+				{
+					W: 300,
+					H: 250,
+				},
+			},
+			Bids: []pbs.Bids{
+				{
+					BidderCode: "conversant",
+					BidID:      fmt.Sprintf("b-%03d", i),
+					Params:     json.RawMessage(params[i]),
+				},
+			},
+		}
+	}
+
+	return &req
+}
+
+// Convert a request to a video request by adding required properties
+
+func ConvertToVideoRequest(req *pbs.PBSRequest, videoParams ...string) (*pbs.PBSRequest, error) {
+	for i := 0; i < len(req.AdUnits); i++ {
+		video := pbs.PBSVideo{}
+		if i < len(videoParams) {
+			err := json.Unmarshal([]byte(videoParams[i]), &video)
+			if err != nil {
+				return nil, err
+			}
+		}
+
+		if video.Mimes == nil {
+			video.Mimes = []string{"video/mp4"}
+		}
+
+		req.AdUnits[i].Video = video
+		req.AdUnits[i].MediaTypes = []string{"video"}
+	}
+
+	return req, nil
+}
+
+// Convert a request to an app request by adding required properties
+func ConvertToAppRequest(req *pbs.PBSRequest, appParams string) (*pbs.PBSRequest, error) {
+	app := new(openrtb.App)
+	err := json.Unmarshal([]byte(appParams), &app)
+	if err == nil {
+		req.App = app
+	}
+
+	return req, nil
+}
+
+// Feed the request thru the prebid parser so user id and
+// other private properties are defined
+
+func ParseRequest(req *pbs.PBSRequest) (*pbs.PBSRequest, error) {
+	body := new(bytes.Buffer)
+	_ = json.NewEncoder(body).Encode(req)
+
+	// Need to pass the conversant user id thru uid cookie
+
+	httpReq := httptest.NewRequest("POST", "/foo", body)
+	cookie := usersync.NewPBSCookie()
+	_ = cookie.TrySync("conversant", ExpectedBuyerUID)
+	httpReq.Header.Set("Cookie", cookie.ToHTTPCookie(90*24*time.Hour).String())
+	httpReq.Header.Add("Referer", "http://example.com")
+	cache, _ := dummycache.New()
+	hcc := config.HostCookie{}
+
+	parsedReq, err := pbs.ParsePBSRequest(httpReq, &config.AuctionTimeouts{
+		Default: 2000,
+		Max:     2000,
+	}, cache, &hcc)
+
+	return parsedReq, err
+}
+
+// A helper to create a banner request
+
+func CreateBannerRequest(params ...string) (*pbs.PBSRequest, error) {
+	req := CreateRequest(params...)
+	req, err := ParseRequest(req)
+	return req, err
+}
+
+// A helper to create a video request
+
+func CreateVideoRequest(params ...string) (*pbs.PBSRequest, error) {
+	req := CreateRequest(params...)
+	req, err := ConvertToVideoRequest(req)
+	if err != nil {
+		return nil, err
+	}
+	req, err = ParseRequest(req)
+	return req, err
+}
+
+// Helper to create a test http server that receives and generate openrtb requests and responses
+
+func CreateServer(prices ...float64) (*httptest.Server, *openrtb.BidRequest) {
+	var lastBidRequest openrtb.BidRequest
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		defer r.Body.Close()
+		body, err := ioutil.ReadAll(r.Body)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+
+		var bidReq openrtb.BidRequest
+		var price float64
+		var bids []openrtb.Bid
+		var bid openrtb.Bid
+
+		err = json.Unmarshal(body, &bidReq)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+		}
+
+		lastBidRequest = bidReq
+
+		for i, imp := range bidReq.Imp {
+			if i < len(prices) {
+				price = prices[i]
+			} else {
+				price = 0
+			}
+
+			if price > 0 {
+				bid = openrtb.Bid{
+					ID:    imp.ID,
+					ImpID: imp.ID,
+					Price: price,
+					NURL:  ExpectedNURL,
+					AdM:   ExpectedAdM,
+					CrID:  ExpectedCrID,
+				}
+
+				if imp.Banner != nil {
+					bid.W = *imp.Banner.W
+					bid.H = *imp.Banner.H
+				} else if imp.Video != nil {
+					bid.W = imp.Video.W
+					bid.H = imp.Video.H
+				}
+			} else {
+				bid = openrtb.Bid{
+					ID:    imp.ID,
+					ImpID: imp.ID,
+					Price: 0,
+				}
+			}
+
+			bids = append(bids, bid)
+		}
+
+		if len(bids) == 0 {
+			w.WriteHeader(http.StatusNoContent)
+		} else {
+			js, _ := json.Marshal(openrtb.BidResponse{
+				ID: bidReq.ID,
+				SeatBid: []openrtb.SeatBid{
+					{
+						Bid: bids,
+					},
+				},
+			})
+
+			w.Header().Set("Content-Type", "application/json")
+			_, _ = w.Write(js)
+		}
+	}),
+	)
+
+	return server, &lastBidRequest
+}
+
+// Helper to remove impressions with $0 bids
+
+func FilterZeroPrices(prices []float64, imps []openrtb.Imp) ([]float64, []openrtb.Imp) {
+	prices2 := make([]float64, 0)
+	imps2 := make([]openrtb.Imp, 0)
+
+	for i := range prices {
+		if prices[i] > 0 {
+			prices2 = append(prices2, prices[i])
+			imps2 = append(imps2, imps[i])
+		}
+	}
+
+	return prices2, imps2
+}
+
+// Helpers to test equality
+
+func assertEqual(t *testing.T, actual interface{}, expected interface{}, msg string) {
+	if expected != actual {
+		msg = fmt.Sprintf("%s: act(%v) != exp(%v)", msg, actual, expected)
+		t.Fatal(msg)
+	}
+}
+
+func assertNotEqual(t *testing.T, actual interface{}, expected interface{}, msg string) {
+	if expected == actual {
+		msg = fmt.Sprintf("%s: act(%v) == exp(%v)", msg, actual, expected)
+		t.Fatal(msg)
+	}
+}
+
+func assertTrue(t *testing.T, val bool, msg string) {
+	if val == false {
+		msg = fmt.Sprintf("%s: is false but should be true", msg)
+		t.Fatal(msg)
+	}
+}
+
+func assertFalse(t *testing.T, val bool, msg string) {
+	if val == true {
+		msg = fmt.Sprintf("%s: is true but should be false", msg)
+		t.Fatal(msg)
+	}
+}
diff --git a/adapters/conversant/conversant_test.go b/adapters/conversant/conversant_test.go
index b958e320..e3275030 100644
--- a/adapters/conversant/conversant_test.go
+++ b/adapters/conversant/conversant_test.go
@@ -1,853 +1,10 @@
 package conversant
 
 import (
-	"bytes"
-	"context"
-	"encoding/json"
-	"fmt"
-	"io/ioutil"
-	"net/http"
-	"net/http/httptest"
+	"github.com/prebid/prebid-server/adapters/adapterstest"
 	"testing"
-	"time"
-
-	"github.com/mxmCherry/openrtb"
-	"github.com/prebid/prebid-server/adapters"
-	"github.com/prebid/prebid-server/cache/dummycache"
-	"github.com/prebid/prebid-server/config"
-	"github.com/prebid/prebid-server/pbs"
-	"github.com/prebid/prebid-server/usersync"
 )
 
-// Constants
-
-const ExpectedSiteID string = "12345"
-const ExpectedDisplayManager string = "prebid-s2s"
-const ExpectedBuyerUID string = "AQECT_o7M1FLbQJK8QFmAQEBAQE"
-const ExpectedNURL string = "http://test.dotomi.com"
-const ExpectedAdM string = "<img src=\"test.jpg\"/>"
-const ExpectedCrID string = "98765"
-
-const DefaultParam = `{"site_id": "12345"}`
-
-// Test properties of Adapter interface
-
-func TestConversantProperties(t *testing.T) {
-	an := NewConversantAdapter(adapters.DefaultHTTPAdapterConfig, "someUrl")
-
-	assertNotEqual(t, an.Name(), "", "Missing family name")
-	assertTrue(t, an.SkipNoCookies(), "SkipNoCookies should be true")
-}
-
-// Test empty bid requests
-
-func TestConversantEmptyBid(t *testing.T) {
-	an := NewConversantAdapter(adapters.DefaultHTTPAdapterConfig, "someUrl")
-
-	ctx := context.TODO()
-	pbReq := pbs.PBSRequest{}
-	pbBidder := pbs.PBSBidder{}
-	_, err := an.Call(ctx, &pbReq, &pbBidder)
-	assertTrue(t, err != nil, "No error received for an invalid request")
-}
-
-// Test required parameters, which is just the site id for now
-
-func TestConversantRequiredParameters(t *testing.T) {
-	server := httptest.NewServer(
-		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-			http.Error(w, http.StatusText(http.StatusNoContent), http.StatusNoContent)
-		}),
-	)
-	defer server.Close()
-
-	an := NewConversantAdapter(adapters.DefaultHTTPAdapterConfig, server.URL)
-	ctx := context.TODO()
-
-	testParams := func(params ...string) (pbs.PBSBidSlice, error) {
-		req, err := CreateBannerRequest(params...)
-		if err != nil {
-			return nil, err
-		}
-		return an.Call(ctx, req, req.Bidders[0])
-	}
-
-	var err error
-
-	if _, err = testParams(`{}`); err == nil {
-		t.Fatal("Failed to catch missing site id")
-	}
-}
-
-// Test handling of 404
-
-func TestConversantBadStatus(t *testing.T) {
-	// Create a test http server that returns after 2 milliseconds
-
-	server := httptest.NewServer(
-		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
-		}),
-	)
-	defer server.Close()
-
-	// Create a adapter to test
-
-	conf := *adapters.DefaultHTTPAdapterConfig
-	an := NewConversantAdapter(&conf, server.URL)
-
-	ctx := context.TODO()
-	pbReq, err := CreateBannerRequest(DefaultParam)
-	if err != nil {
-		t.Fatal("Failed to create a banner request", err)
-	}
-
-	_, err = an.Call(ctx, pbReq, pbReq.Bidders[0])
-	assertTrue(t, err != nil, "Failed to catch 404 error")
-}
-
-// Test handling of HTTP timeout
-
-func TestConversantTimeout(t *testing.T) {
-	// Create a test http server that returns after 2 milliseconds
-
-	server := httptest.NewServer(
-		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-			<-time.After(2 * time.Millisecond)
-		}),
-	)
-	defer server.Close()
-
-	// Create a adapter to test
-
-	conf := *adapters.DefaultHTTPAdapterConfig
-	an := NewConversantAdapter(&conf, server.URL)
-
-	// Create a context that expires before http returns
-
-	ctx, cancel := context.WithTimeout(context.Background(), 0)
-	defer cancel()
-
-	// Create a basic request
-	pbReq, err := CreateBannerRequest(DefaultParam)
-	if err != nil {
-		t.Fatal("Failed to create a banner request", err)
-	}
-
-	// Attempt to process the request, which should hit a timeout
-	// immediately
-
-	_, err = an.Call(ctx, pbReq, pbReq.Bidders[0])
-	if err == nil || err != context.DeadlineExceeded {
-		t.Fatal("No timeout recevied for timed out request", err)
-	}
-}
-
-// Test handling of 204
-
-func TestConversantNoBid(t *testing.T) {
-	// Create a test http server that returns after 2 milliseconds
-
-	server := httptest.NewServer(
-		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-			http.Error(w, http.StatusText(http.StatusNoContent), http.StatusNoContent)
-		}),
-	)
-	defer server.Close()
-
-	// Create a adapter to test
-
-	conf := *adapters.DefaultHTTPAdapterConfig
-	an := NewConversantAdapter(&conf, server.URL)
-
-	ctx := context.TODO()
-	pbReq, err := CreateBannerRequest(DefaultParam)
-	if err != nil {
-		t.Fatal("Failed to create a banner request", err)
-	}
-
-	resp, err := an.Call(ctx, pbReq, pbReq.Bidders[0])
-	if resp != nil || err != nil {
-		t.Fatal("Failed to handle empty bid", err)
-	}
-}
-
-// Verify an outgoing openrtp request is created correctly
-
-func TestConversantRequestDefault(t *testing.T) {
-	server, lastReq := CreateServer()
-	if server == nil {
-		t.Fatal("server not created")
-	}
-
-	defer server.Close()
-
-	// Create a adapter to test
-
-	conf := *adapters.DefaultHTTPAdapterConfig
-	an := NewConversantAdapter(&conf, server.URL)
-
-	ctx := context.TODO()
-	pbReq, err := CreateBannerRequest(DefaultParam)
-	if err != nil {
-		t.Fatal("Failed to create a banner request", err)
-	}
-
-	_, err = an.Call(ctx, pbReq, pbReq.Bidders[0])
-	if err != nil {
-		t.Fatal("Failed to retrieve bids", err)
-	}
-
-	assertEqual(t, len(lastReq.Imp), 1, "Request number of impressions")
-	imp := &lastReq.Imp[0]
-
-	assertEqual(t, imp.DisplayManager, ExpectedDisplayManager, "Request display manager value")
-	assertEqual(t, lastReq.Site.ID, ExpectedSiteID, "Request site id")
-	assertEqual(t, int(lastReq.Site.Mobile), 0, "Request site mobile flag")
-	assertEqual(t, lastReq.User.BuyerUID, ExpectedBuyerUID, "Request buyeruid")
-	assertTrue(t, imp.Video == nil, "Request video should be nil")
-	assertEqual(t, int(*imp.Secure), 0, "Request secure")
-	assertEqual(t, imp.BidFloor, 0.0, "Request bid floor")
-	assertEqual(t, imp.TagID, "", "Request tag id")
-	assertTrue(t, imp.Banner.Pos == nil, "Request pos")
-	assertEqual(t, int(*imp.Banner.W), 300, "Request width")
-	assertEqual(t, int(*imp.Banner.H), 250, "Request height")
-}
-
-// Verify inapp video request
-func TestConversantInappVideoRequest(t *testing.T) {
-	server, lastReq := CreateServer()
-	if server == nil {
-		t.Fatal("server not created")
-	}
-
-	defer server.Close()
-
-	conf := *adapters.DefaultHTTPAdapterConfig
-	an := NewConversantAdapter(&conf, server.URL)
-
-	requestParam := `{"secure": 1, "site_id": "12345"}`
-	appParam := `{ "bundle": "com.naver.linewebtoon" }`
-	videoParam := `{ "mimes": ["video/x-ms-wmv"],
-		   "protocols": [1, 2],
-		   "maxduration": 90 }`
-
-	ctx := context.TODO()
-	pbReq := CreateRequest(requestParam)
-	pbReq, err := ConvertToVideoRequest(pbReq, videoParam)
-	if err != nil {
-		t.Fatal("failed to parse request")
-	}
-	pbReq, err = ConvertToAppRequest(pbReq, appParam)
-	if err != nil {
-		t.Fatal("failed to parse request")
-	}
-	pbReq, err = ParseRequest(pbReq)
-	if err != nil {
-		t.Fatal("failed to parse request")
-	}
-
-	_, err = an.Call(ctx, pbReq, pbReq.Bidders[0])
-
-	imp := &lastReq.Imp[0]
-	assertEqual(t, int(imp.Video.W), 300, "Request width")
-	assertEqual(t, int(imp.Video.H), 250, "Request height")
-	assertEqual(t, lastReq.App.ID, "12345", "App Id")
-}
-
-// Verify inapp video request
-func TestConversantInappBannerRequest(t *testing.T) {
-	server, lastReq := CreateServer()
-	if server == nil {
-		t.Fatal("server not created")
-	}
-
-	defer server.Close()
-
-	conf := *adapters.DefaultHTTPAdapterConfig
-	an := NewConversantAdapter(&conf, server.URL)
-
-	param := `{ "secure": 1,
-		"site_id": "12345",
-		"tag_id": "top",
-		"position": 2,
-		"bidfloor": 1.01 }`
-	appParam := `{ "bundle": "com.naver.linewebtoon" }`
-
-	ctx := context.TODO()
-	pbReq, _ := CreateBannerRequest(param)
-	pbReq, err := ConvertToAppRequest(pbReq, appParam)
-	if err != nil {
-		t.Fatal("failed to parse request")
-	}
-
-	_, err = an.Call(ctx, pbReq, pbReq.Bidders[0])
-
-	imp := &lastReq.Imp[0]
-	assertEqual(t, lastReq.App.ID, "12345", "App Id")
-	assertEqual(t, int(*imp.Banner.W), 300, "Request width")
-	assertEqual(t, int(*imp.Banner.H), 250, "Request height")
-}
-
-// Verify an outgoing openrtp request with additional conversant parameters is
-// processed correctly
-
-func TestConversantRequest(t *testing.T) {
-	server, lastReq := CreateServer()
-	if server == nil {
-		t.Fatal("server not created")
-	}
-
-	defer server.Close()
-
-	// Create a adapter to test
-
-	conf := *adapters.DefaultHTTPAdapterConfig
-	an := NewConversantAdapter(&conf, server.URL)
-
-	param := `{ "site_id": "12345",
-		"secure": 1,
-		"tag_id": "top",
-		"position": 2,
-		"bidfloor": 1.01,
-		"mobile": 1 }`
-
-	ctx := context.TODO()
-	pbReq, err := CreateBannerRequest(param)
-	if err != nil {
-		t.Fatal("Failed to create a banner request", err)
-	}
-
-	_, err = an.Call(ctx, pbReq, pbReq.Bidders[0])
-	if err != nil {
-		t.Fatal("Failed to retrieve bids", err)
-	}
-
-	assertEqual(t, len(lastReq.Imp), 1, "Request number of impressions")
-	imp := &lastReq.Imp[0]
-
-	assertEqual(t, imp.DisplayManager, ExpectedDisplayManager, "Request display manager value")
-	assertEqual(t, lastReq.Site.ID, ExpectedSiteID, "Request site id")
-	assertEqual(t, int(lastReq.Site.Mobile), 1, "Request site mobile flag")
-	assertEqual(t, lastReq.User.BuyerUID, ExpectedBuyerUID, "Request buyeruid")
-	assertTrue(t, imp.Video == nil, "Request video should be nil")
-	assertEqual(t, int(*imp.Secure), 1, "Request secure")
-	assertEqual(t, imp.BidFloor, 1.01, "Request bid floor")
-	assertEqual(t, imp.TagID, "top", "Request tag id")
-	assertEqual(t, int(*imp.Banner.Pos), 2, "Request pos")
-	assertEqual(t, int(*imp.Banner.W), 300, "Request width")
-	assertEqual(t, int(*imp.Banner.H), 250, "Request height")
-}
-
-// Verify openrtp responses are converted correctly
-
-func TestConversantResponse(t *testing.T) {
-	prices := []float64{0.01, 0.0, 2.01}
-	server, lastReq := CreateServer(prices...)
-	if server == nil {
-		t.Fatal("server not created")
-	}
-
-	defer server.Close()
-
-	// Create a adapter to test
-
-	conf := *adapters.DefaultHTTPAdapterConfig
-	an := NewConversantAdapter(&conf, server.URL)
-
-	param := `{ "site_id": "12345",
-		   "secure": 1,
-		   "tag_id": "top",
-		   "position": 2,
-		   "bidfloor": 1.01,
-		   "mobile" : 1}`
-
-	ctx := context.TODO()
-	pbReq, err := CreateBannerRequest(param, param, param)
-	if err != nil {
-		t.Fatal("Failed to create a banner request", err)
-	}
-
-	resp, err := an.Call(ctx, pbReq, pbReq.Bidders[0])
-	if err != nil {
-		t.Fatal("Failed to retrieve bids", err)
-	}
-
-	prices, imps := FilterZeroPrices(prices, lastReq.Imp)
-
-	assertEqual(t, len(resp), len(prices), "Bad number of responses")
-
-	for i, bid := range resp {
-		assertEqual(t, bid.Price, prices[i], "Bad price in response")
-		assertEqual(t, bid.AdUnitCode, imps[i].ID, "Bad bid id in response")
-
-		if bid.Price > 0 {
-			assertEqual(t, bid.Adm, ExpectedAdM, "Bad ad markup in response")
-			assertEqual(t, bid.NURL, ExpectedNURL, "Bad notification url in response")
-			assertEqual(t, bid.Creative_id, ExpectedCrID, "Bad creative id in response")
-			assertEqual(t, bid.Width, *imps[i].Banner.W, "Bad width in response")
-			assertEqual(t, bid.Height, *imps[i].Banner.H, "Bad height in response")
-		}
-	}
-}
-
-// Test video request
-
-func TestConversantBasicVideoRequest(t *testing.T) {
-	server, lastReq := CreateServer()
-	if server == nil {
-		t.Fatal("server not created")
-	}
-
-	defer server.Close()
-
-	// Create a adapter to test
-
-	conf := *adapters.DefaultHTTPAdapterConfig
-	an := NewConversantAdapter(&conf, server.URL)
-
-	param := `{ "site_id": "12345",
-		   "tag_id": "bottom left",
-		   "position": 3,
-		   "bidfloor": 1.01 }`
-
-	ctx := context.TODO()
-	pbReq, err := CreateVideoRequest(param)
-	if err != nil {
-		t.Fatal("Failed to create a video request", err)
-	}
-
-	_, err = an.Call(ctx, pbReq, pbReq.Bidders[0])
-	if err != nil {
-		t.Fatal("Failed to retrieve bids", err)
-	}
-
-	assertEqual(t, len(lastReq.Imp), 1, "Request number of impressions")
-	imp := &lastReq.Imp[0]
-
-	assertEqual(t, imp.DisplayManager, ExpectedDisplayManager, "Request display manager value")
-	assertEqual(t, lastReq.Site.ID, ExpectedSiteID, "Request site id")
-	assertEqual(t, int(lastReq.Site.Mobile), 0, "Request site mobile flag")
-	assertEqual(t, lastReq.User.BuyerUID, ExpectedBuyerUID, "Request buyeruid")
-	assertTrue(t, imp.Banner == nil, "Request banner should be nil")
-	assertEqual(t, int(*imp.Secure), 0, "Request secure")
-	assertEqual(t, imp.BidFloor, 1.01, "Request bid floor")
-	assertEqual(t, imp.TagID, "bottom left", "Request tag id")
-	assertEqual(t, int(*imp.Video.Pos), 3, "Request pos")
-	assertEqual(t, int(imp.Video.W), 300, "Request width")
-	assertEqual(t, int(imp.Video.H), 250, "Request height")
-
-	assertEqual(t, len(imp.Video.MIMEs), 1, "Request video MIMEs entries")
-	assertEqual(t, imp.Video.MIMEs[0], "video/mp4", "Requst video MIMEs type")
-	assertTrue(t, imp.Video.Protocols == nil, "Request video protocols")
-	assertEqual(t, imp.Video.MaxDuration, int64(0), "Request video 0 max duration")
-	assertTrue(t, imp.Video.API == nil, "Request video api should be nil")
-}
-
-// Test video request with parameters in custom params object
-
-func TestConversantVideoRequestWithParams(t *testing.T) {
-	server, lastReq := CreateServer()
-	if server == nil {
-		t.Fatal("server not created")
-	}
-
-	defer server.Close()
-
-	// Create a adapter to test
-
-	conf := *adapters.DefaultHTTPAdapterConfig
-	an := NewConversantAdapter(&conf, server.URL)
-
-	param := `{ "site_id": "12345",
-		   "tag_id": "bottom left",
-		   "position": 3,
-		   "bidfloor": 1.01,
-		   "mimes": ["video/x-ms-wmv"],
-		   "protocols": [1, 2],
-		   "api": [1, 2],
-		   "maxduration": 90 }`
-
-	ctx := context.TODO()
-	pbReq, err := CreateVideoRequest(param)
-	if err != nil {
-		t.Fatal("Failed to create a video request", err)
-	}
-
-	_, err = an.Call(ctx, pbReq, pbReq.Bidders[0])
-	if err != nil {
-		t.Fatal("Failed to retrieve bids", err)
-	}
-
-	assertEqual(t, len(lastReq.Imp), 1, "Request number of impressions")
-	imp := &lastReq.Imp[0]
-
-	assertEqual(t, imp.DisplayManager, ExpectedDisplayManager, "Request display manager value")
-	assertEqual(t, lastReq.Site.ID, ExpectedSiteID, "Request site id")
-	assertEqual(t, int(lastReq.Site.Mobile), 0, "Request site mobile flag")
-	assertEqual(t, lastReq.User.BuyerUID, ExpectedBuyerUID, "Request buyeruid")
-	assertTrue(t, imp.Banner == nil, "Request banner should be nil")
-	assertEqual(t, int(*imp.Secure), 0, "Request secure")
-	assertEqual(t, imp.BidFloor, 1.01, "Request bid floor")
-	assertEqual(t, imp.TagID, "bottom left", "Request tag id")
-	assertEqual(t, int(*imp.Video.Pos), 3, "Request pos")
-	assertEqual(t, int(imp.Video.W), 300, "Request width")
-	assertEqual(t, int(imp.Video.H), 250, "Request height")
-
-	assertEqual(t, len(imp.Video.MIMEs), 1, "Request video MIMEs entries")
-	assertEqual(t, imp.Video.MIMEs[0], "video/x-ms-wmv", "Requst video MIMEs type")
-	assertEqual(t, len(imp.Video.Protocols), 2, "Request video protocols")
-	assertEqual(t, imp.Video.Protocols[0], openrtb.Protocol(1), "Request video protocols 1")
-	assertEqual(t, imp.Video.Protocols[1], openrtb.Protocol(2), "Request video protocols 2")
-	assertEqual(t, imp.Video.MaxDuration, int64(90), "Request video 0 max duration")
-	assertEqual(t, len(imp.Video.API), 2, "Request video api should be nil")
-	assertEqual(t, imp.Video.API[0], openrtb.APIFramework(1), "Request video api 1")
-	assertEqual(t, imp.Video.API[1], openrtb.APIFramework(2), "Request video api 2")
-}
-
-// Test video request with parameters in the video object
-
-func TestConversantVideoRequestWithParams2(t *testing.T) {
-	server, lastReq := CreateServer()
-	if server == nil {
-		t.Fatal("server not created")
-	}
-
-	defer server.Close()
-
-	// Create a adapter to test
-
-	conf := *adapters.DefaultHTTPAdapterConfig
-	an := NewConversantAdapter(&conf, server.URL)
-
-	param := `{ "site_id": "12345" }`
-	videoParam := `{ "mimes": ["video/x-ms-wmv"],
-		   "protocols": [1, 2],
-		   "maxduration": 90 }`
-
-	ctx := context.TODO()
-	pbReq := CreateRequest(param)
-	pbReq, err := ConvertToVideoRequest(pbReq, videoParam)
-	if err != nil {
-		t.Fatal("Failed to convert to a video request", err)
-	}
-	pbReq, err = ParseRequest(pbReq)
-	if err != nil {
-		t.Fatal("Failed to parse video request", err)
-	}
-
-	_, err = an.Call(ctx, pbReq, pbReq.Bidders[0])
-	if err != nil {
-		t.Fatal("Failed to retrieve bids", err)
-	}
-
-	assertEqual(t, len(lastReq.Imp), 1, "Request number of impressions")
-	imp := &lastReq.Imp[0]
-
-	assertEqual(t, imp.DisplayManager, ExpectedDisplayManager, "Request display manager value")
-	assertEqual(t, lastReq.Site.ID, ExpectedSiteID, "Request site id")
-	assertEqual(t, int(lastReq.Site.Mobile), 0, "Request site mobile flag")
-	assertEqual(t, lastReq.User.BuyerUID, ExpectedBuyerUID, "Request buyeruid")
-	assertTrue(t, imp.Banner == nil, "Request banner should be nil")
-	assertEqual(t, int(*imp.Secure), 0, "Request secure")
-	assertEqual(t, imp.BidFloor, 0.0, "Request bid floor")
-	assertEqual(t, int(imp.Video.W), 300, "Request width")
-	assertEqual(t, int(imp.Video.H), 250, "Request height")
-
-	assertEqual(t, len(imp.Video.MIMEs), 1, "Request video MIMEs entries")
-	assertEqual(t, imp.Video.MIMEs[0], "video/x-ms-wmv", "Requst video MIMEs type")
-	assertEqual(t, len(imp.Video.Protocols), 2, "Request video protocols")
-	assertEqual(t, imp.Video.Protocols[0], openrtb.Protocol(1), "Request video protocols 1")
-	assertEqual(t, imp.Video.Protocols[1], openrtb.Protocol(2), "Request video protocols 2")
-	assertEqual(t, imp.Video.MaxDuration, int64(90), "Request video 0 max duration")
-}
-
-// Test video responses
-
-func TestConversantVideoResponse(t *testing.T) {
-	prices := []float64{0.01, 0.0, 2.01}
-	server, lastReq := CreateServer(prices...)
-	if server == nil {
-		t.Fatal("server not created")
-	}
-
-	defer server.Close()
-
-	// Create a adapter to test
-
-	conf := *adapters.DefaultHTTPAdapterConfig
-	an := NewConversantAdapter(&conf, server.URL)
-
-	param := `{ "site_id": "12345",
-		   "secure": 1,
-		   "tag_id": "top",
-		   "position": 2,
-		   "bidfloor": 1.01,
-		   "mobile" : 1}`
-
-	ctx := context.TODO()
-	pbReq, err := CreateVideoRequest(param, param, param)
-	if err != nil {
-		t.Fatal("Failed to create a video request", err)
-	}
-
-	resp, err := an.Call(ctx, pbReq, pbReq.Bidders[0])
-	if err != nil {
-		t.Fatal("Failed to retrieve bids", err)
-	}
-
-	prices, imps := FilterZeroPrices(prices, lastReq.Imp)
-
-	assertEqual(t, len(resp), len(prices), "Bad number of responses")
-
-	for i, bid := range resp {
-		assertEqual(t, bid.Price, prices[i], "Bad price in response")
-		assertEqual(t, bid.AdUnitCode, imps[i].ID, "Bad bid id in response")
-
-		if bid.Price > 0 {
-			assertEqual(t, bid.Adm, "", "Bad ad markup in response")
-			assertEqual(t, bid.NURL, ExpectedAdM, "Bad notification url in response")
-			assertEqual(t, bid.Creative_id, ExpectedCrID, "Bad creative id in response")
-			assertEqual(t, bid.Width, imps[i].Video.W, "Bad width in response")
-			assertEqual(t, bid.Height, imps[i].Video.H, "Bad height in response")
-		}
-	}
-}
-
-// Helpers to create a banner and video requests
-
-func CreateRequest(params ...string) *pbs.PBSRequest {
-	num := len(params)
-
-	req := pbs.PBSRequest{
-		Tid:       "t-000",
-		AccountID: "1",
-		AdUnits:   make([]pbs.AdUnit, num),
-	}
-
-	for i := 0; i < num; i++ {
-		req.AdUnits[i] = pbs.AdUnit{
-			Code: fmt.Sprintf("au-%03d", i),
-			Sizes: []openrtb.Format{
-				{
-					W: 300,
-					H: 250,
-				},
-			},
-			Bids: []pbs.Bids{
-				{
-					BidderCode: "conversant",
-					BidID:      fmt.Sprintf("b-%03d", i),
-					Params:     json.RawMessage(params[i]),
-				},
-			},
-		}
-	}
-
-	return &req
-}
-
-// Convert a request to a video request by adding required properties
-
-func ConvertToVideoRequest(req *pbs.PBSRequest, videoParams ...string) (*pbs.PBSRequest, error) {
-	for i := 0; i < len(req.AdUnits); i++ {
-		video := pbs.PBSVideo{}
-		if i < len(videoParams) {
-			err := json.Unmarshal([]byte(videoParams[i]), &video)
-			if err != nil {
-				return nil, err
-			}
-		}
-
-		if video.Mimes == nil {
-			video.Mimes = []string{"video/mp4"}
-		}
-
-		req.AdUnits[i].Video = video
-		req.AdUnits[i].MediaTypes = []string{"video"}
-	}
-
-	return req, nil
-}
-
-// Convert a request to an app request by adding required properties
-func ConvertToAppRequest(req *pbs.PBSRequest, appParams string) (*pbs.PBSRequest, error) {
-	app := new(openrtb.App)
-	err := json.Unmarshal([]byte(appParams), &app)
-	if err == nil {
-		req.App = app
-	}
-
-	return req, nil
-}
-
-// Feed the request thru the prebid parser so user id and
-// other private properties are defined
-
-func ParseRequest(req *pbs.PBSRequest) (*pbs.PBSRequest, error) {
-	body := new(bytes.Buffer)
-	_ = json.NewEncoder(body).Encode(req)
-
-	// Need to pass the conversant user id thru uid cookie
-
-	httpReq := httptest.NewRequest("POST", "/foo", body)
-	cookie := usersync.NewPBSCookie()
-	_ = cookie.TrySync("conversant", ExpectedBuyerUID)
-	httpReq.Header.Set("Cookie", cookie.ToHTTPCookie(90*24*time.Hour).String())
-	httpReq.Header.Add("Referer", "http://example.com")
-	cache, _ := dummycache.New()
-	hcc := config.HostCookie{}
-
-	parsedReq, err := pbs.ParsePBSRequest(httpReq, &config.AuctionTimeouts{
-		Default: 2000,
-		Max:     2000,
-	}, cache, &hcc)
-
-	return parsedReq, err
-}
-
-// A helper to create a banner request
-
-func CreateBannerRequest(params ...string) (*pbs.PBSRequest, error) {
-	req := CreateRequest(params...)
-	req, err := ParseRequest(req)
-	return req, err
-}
-
-// A helper to create a video request
-
-func CreateVideoRequest(params ...string) (*pbs.PBSRequest, error) {
-	req := CreateRequest(params...)
-	req, err := ConvertToVideoRequest(req)
-	if err != nil {
-		return nil, err
-	}
-	req, err = ParseRequest(req)
-	return req, err
-}
-
-// Helper to create a test http server that receives and generate openrtb requests and responses
-
-func CreateServer(prices ...float64) (*httptest.Server, *openrtb.BidRequest) {
-	var lastBidRequest openrtb.BidRequest
-	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		defer r.Body.Close()
-		body, err := ioutil.ReadAll(r.Body)
-		if err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
-			return
-		}
-
-		var bidReq openrtb.BidRequest
-		var price float64
-		var bids []openrtb.Bid
-		var bid openrtb.Bid
-
-		err = json.Unmarshal(body, &bidReq)
-		if err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
-		}
-
-		lastBidRequest = bidReq
-
-		for i, imp := range bidReq.Imp {
-			if i < len(prices) {
-				price = prices[i]
-			} else {
-				price = 0
-			}
-
-			if price > 0 {
-				bid = openrtb.Bid{
-					ID:    imp.ID,
-					ImpID: imp.ID,
-					Price: price,
-					NURL:  ExpectedNURL,
-					AdM:   ExpectedAdM,
-					CrID:  ExpectedCrID,
-				}
-
-				if imp.Banner != nil {
-					bid.W = *imp.Banner.W
-					bid.H = *imp.Banner.H
-				} else if imp.Video != nil {
-					bid.W = imp.Video.W
-					bid.H = imp.Video.H
-				}
-			} else {
-				bid = openrtb.Bid{
-					ID:    imp.ID,
-					ImpID: imp.ID,
-					Price: 0,
-				}
-			}
-
-			bids = append(bids, bid)
-		}
-
-		if len(bids) == 0 {
-			w.WriteHeader(http.StatusNoContent)
-		} else {
-			js, _ := json.Marshal(openrtb.BidResponse{
-				ID: bidReq.ID,
-				SeatBid: []openrtb.SeatBid{
-					{
-						Bid: bids,
-					},
-				},
-			})
-
-			w.Header().Set("Content-Type", "application/json")
-			_, _ = w.Write(js)
-		}
-	}),
-	)
-
-	return server, &lastBidRequest
-}
-
-// Helper to remove impressions with $0 bids
-
-func FilterZeroPrices(prices []float64, imps []openrtb.Imp) ([]float64, []openrtb.Imp) {
-	prices2 := make([]float64, 0)
-	imps2 := make([]openrtb.Imp, 0)
-
-	for i := range prices {
-		if prices[i] > 0 {
-			prices2 = append(prices2, prices[i])
-			imps2 = append(imps2, imps[i])
-		}
-	}
-
-	return prices2, imps2
-}
-
-// Helpers to test equality
-
-func assertEqual(t *testing.T, actual interface{}, expected interface{}, msg string) {
-	if expected != actual {
-		msg = fmt.Sprintf("%s: act(%v) != exp(%v)", msg, actual, expected)
-		t.Fatal(msg)
-	}
-}
-
-func assertNotEqual(t *testing.T, actual interface{}, expected interface{}, msg string) {
-	if expected == actual {
-		msg = fmt.Sprintf("%s: act(%v) == exp(%v)", msg, actual, expected)
-		t.Fatal(msg)
-	}
-}
-
-func assertTrue(t *testing.T, val bool, msg string) {
-	if val == false {
-		msg = fmt.Sprintf("%s: is false but should be true", msg)
-		t.Fatal(msg)
-	}
-}
-
-func assertFalse(t *testing.T, val bool, msg string) {
-	if val == true {
-		msg = fmt.Sprintf("%s: is true but should be false", msg)
-		t.Fatal(msg)
-	}
+func TestJsonSamples(t *testing.T) {
+	adapterstest.RunJSONBidderTest(t, "conversanttest", NewConversantBidder(""))
 }
diff --git a/adapters/conversant/conversanttest/exemplary/banner.json b/adapters/conversant/conversanttest/exemplary/banner.json
new file mode 100644
index 00000000..472e18f7
--- /dev/null
+++ b/adapters/conversant/conversanttest/exemplary/banner.json
@@ -0,0 +1,113 @@
+{
+  "mockBidRequest": {
+    "id": "testauction",
+    "imp": [
+      {
+        "id": "1",
+        "banner": {
+          "format": [{"w": 300, "h": 250}]
+        },
+        "ext": {
+          "bidder": {
+            "site_id": "108060",
+            "bidfloor": 0.01,
+            "tag_id": "mytag",
+            "secure": 1
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36",
+      "ip": "192.168.1.1",
+      "dnt": 1
+    },
+    "site": {
+      "domain": "www.mypage.com"
+    }
+  },
+  "httpCalls": [{
+      "expectedRequest": {
+        "uri": "",
+        "body": {
+          "id": "testauction",
+          "site": {
+            "id": "108060",
+            "domain": "www.mypage.com"
+          },
+          "imp": [
+            {
+              "id": "1",
+              "tagid": "mytag",
+              "secure": 1,
+              "bidfloor": 0.01,
+              "displaymanager": "prebid-s2s",
+              "displaymanagerver": "2.0.0",
+              "banner": {
+                "format": [{"w": 300, "h": 250}]
+              },
+              "ext": {
+                "bidder": {
+                  "site_id": "108060",
+                  "bidfloor": 0.01,
+                  "tag_id": "mytag",
+                  "secure": 1
+                }
+              }
+            }
+          ],
+          "device": {
+            "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36",
+            "ip": "192.168.1.1",
+            "dnt": 1
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "testauction",
+          "bidid": "c8d95f4b-bcbb-4a6c-adbb-4c7f33af3c24",
+          "cur": "USD",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "1",
+                  "impid": "1",
+                  "price": 0.0340,
+                  "nurl": "https:\/\/event.ad.cpe.dotomi.com\/cvx\/event\/imp?enc=eyJ1c2VyaWQiOiI3MTI3MDUzNzM3NTM3MTAzMjIiLCJwYXJ0bmVyVHhpZCI6ImUyZWUzNjZlLWEyMjgtNDI0Mi1hNjJlLTk4ODk3ODhiYzgxNCIsInR4aWQiOiI3MTE1NzQwNDg3NTczODUwMDIiLCJuZXR3b3JrUmVxdWVzdElkIjoiNzExNTc0MDQ4NzU3Mzg1ODc0Iiwic2lkIjoxMTgwOTgsImRpdmlzaW9uSWQiOjgsInRpZCI6OCwibW9iaWxlRGF0YSI6IjU5IiwiYmlkUHJpY2UiOjAuMDY4MCwicHViQ29zdCI6MC4wMzQwLCJwYXJ0bmVyRmVlIjowLjAxMzYsImlwU3RyaW5nIjoiNzMuMTE4LjEzMC4xODYiLCJzdXBwbHlUeXBlIjoxLCJpbnRlZ3JhdGlvblR5cGUiOjQsIm1lZGlhdGlvblR5cGUiOjEyNiwicGxhY2VtZW50SWQiOiIxMTY5ODcwIiwiaGVhZGVyQmlkIjoxLCJpc0RpcmVjdFB1Ymxpc2hlciI6MCwiaGFzQ29uc2VudCI6MSwib3BlcmF0aW9uIjoiQ0xJRU5UX0hFQURFUl8yNSIsImlzQ29yZVNoaWVsZCI6MCwicGFydG5lckNyZWF0aXZlSWQiOiIyNDk2NDRfMzAweDI1MCIsInBhcnRuZXJEb21haW5zIjpbIndhbG1hcnQuY29tIl0sInNlbGxlclJlcXVlc3RJZCI6ImE3ODcyMWQ3LWE2ZmUtNGJiNS1hNjFkLTFhMDg1MzkxZTVlZCIsInNlbGxlckltcElkIjoiMzAwNDIxZDY0NWY2ZjRjOWMifQ&",
+                  "adm": "<div id='beacon_97896' style='position:absolute;left:0px;top:0px;visibility:hidden;'><img src='https:\/\/rtb-us-east1.openx.net\/win\/conversant?p=0.068000&t=2DAABBgABAAECAAIBAAsAAgAAAyIcGAozeG96d1E1ekpoHBbYlLq-s8_-0YUBFu-eiJTkjsmfjAEAHBb79r3dq-TkkToW19-h99Dds9GzAQAW0Nm29AsVBjgkNTIzZDJkNzgtYjc1Yi0wM2VmLTI1Y2MtMmM2MjJmYTlhN2ZhACwcFQIAHBUCABwVAgAAHCbEvIODBBUEFQQmvLyDgwQWruuBgwTWiAEWiAEWiAEWZBZkFmQWZBbUAQAcHCwWgLGKpLvsg60qFuuytdnn4oDnpQEAABam-p2ABBaopLKABBaIrIKBBBaC1NKABBUYHBT0AxTYBAAVBCbUARbUARbUARE1DibUAQAsLBaXzu2B6O7j1_ABFrHv8IeZkI7RzAEAFtDZtvQLBiim-p2ABBaopLKABBaC1NKABBaIrIKBBBgOMjQ5NjQ0XzMwMHgyNTAWjAwW1AElBBZMJQKWwMqMAhEoAk9YDHoU-AMU2gQAFgIYA3J0YoEA3BsEiApiaWRfc291cmNlBVNTUlRCCGJpZF91dWlkJDgzZTVlOGRlLThjNzktNGE4ZC05OTQyLThiOGFkYTc1ZDI2MhhkZWxpdmVyeV9tZWRpdW1fY2F0ZWdvcnkDV0VCIXJldHJpZXZlZF9mcm9tX3NzcnRiX2Vwb2NoX21pbGxpcw0xNTk4NDc3OTI5MDgxAKwYmwJodHRwczovL21wZC5teHB0aW50Lm5ldC8xL1MxL0cxL1Q0ODA2L2RpP3Npej0zMDB4MjUwJmNwZD0yNDk2NDQmY3BzPTI3NSZzc2w9MSZtaWQ9QjMzNkI0X0NGQTRBNTczXzUyRDA1REEmbXBpZD1SMUIzNDJfQzZFRjI3MUVfMTMxRjQwOTZEJmJwPTAuMTA2MDYzNCZkbT0xNDkzREIyMDZGN0M0MUQ2NTRERDI1OUZDRjk2RTVDNSZyaT0wJnY9cTcxbzlGS0d4VVpGNUlONUEycGU5QUpRSWVEYlVSSjFIQ2p2cmRBbmx3M1F2UTFuVkQtSWhnJmFkaWQ9MSZ1dWlkPUE4emxpcHVrLUJJTEZIcl90SDVoT1p3KAt3YWxtYXJ0LmNvbQAAAA&ph=bec2690e-a73d-4d95-9901-75ad2a8d91b8'\/><\/div><script type=\"text\/javascript\" src=\"https:\/\/mpd.mxptint.net\/1\/S1\/G1\/T4806\/js?siz=300x250&cpd=249644&cps=275&ssl=1&mid=B336B4_CFA4A573_52D05DA&mpid=R1B342_C6EF271E_131F4096D&bp=0.1060634&dm=1493DB206F7C41D654DD259FCF96E5C5&ri=0&v=q71o9FKGxUZF5IN5A2pe9AJQIeDbURJ1HCjvrdAnlw3QvQ1nVD-Ihg&adid=1&uuid=A8zlipuk-BILFHr_tH5hOZw&spox=AAABdCy1ioekWBgw3-MoY-mUAAZXsF6Wi8M83w\"><\/script><noscript><iframe src=\"https:\/\/mpd.mxptint.net\/1\/S1\/G1\/T4806\/if?siz=300x250&cpd=249644&cps=275&ssl=1&mid=B336B4_CFA4A573_52D05DA&mpid=R1B342_C6EF271E_131F4096D&bp=0.1060634&dm=1493DB206F7C41D654DD259FCF96E5C5&ri=0&v=q71o9FKGxUZF5IN5A2pe9AJQIeDbURJ1HCjvrdAnlw3QvQ1nVD-Ihg&adid=1&uuid=A8zlipuk-BILFHr_tH5hOZw&spox=AAABdCy1ioekWBgw3-MoY-mUAAZXsF6Wi8M83w\" width=\"300px\" height=\"250px\" marginwidth=\"0\" marginheight=\"0\" frameborder=\"0\" scrolling=\"no\"><img src=\"https:\/\/mpc.mxptint.net\/11S2SEECE726S6C47S3CF2CS12CSFAS12C6SB336B4_CFA4A573_52D05DAS%3fhttp:\/\/noredir.mxptint.net\"\/><\/iframe><\/noscript><iframe src='https:\/\/us-u.openx.net\/w\/1.0\/pd?plm=6&ph=bec2690e-a73d-4d95-9901-75ad2a8d91b8' width='0' height='0' style='display:none;'><\/iframe>",
+                  "adomain": ["walmart.com"],
+                  "crid": "249644_300x250",
+                  "w": 300,
+                  "h": 250
+                }
+              ]
+            }
+          ]
+        }
+      }
+  }],
+  "expectedBidResponses": [
+    {
+      "bids": [{
+        "bid": {
+          "id": "1",
+          "impid": "1",
+          "price": 0.0340,
+          "nurl": "https:\/\/event.ad.cpe.dotomi.com\/cvx\/event\/imp?enc=eyJ1c2VyaWQiOiI3MTI3MDUzNzM3NTM3MTAzMjIiLCJwYXJ0bmVyVHhpZCI6ImUyZWUzNjZlLWEyMjgtNDI0Mi1hNjJlLTk4ODk3ODhiYzgxNCIsInR4aWQiOiI3MTE1NzQwNDg3NTczODUwMDIiLCJuZXR3b3JrUmVxdWVzdElkIjoiNzExNTc0MDQ4NzU3Mzg1ODc0Iiwic2lkIjoxMTgwOTgsImRpdmlzaW9uSWQiOjgsInRpZCI6OCwibW9iaWxlRGF0YSI6IjU5IiwiYmlkUHJpY2UiOjAuMDY4MCwicHViQ29zdCI6MC4wMzQwLCJwYXJ0bmVyRmVlIjowLjAxMzYsImlwU3RyaW5nIjoiNzMuMTE4LjEzMC4xODYiLCJzdXBwbHlUeXBlIjoxLCJpbnRlZ3JhdGlvblR5cGUiOjQsIm1lZGlhdGlvblR5cGUiOjEyNiwicGxhY2VtZW50SWQiOiIxMTY5ODcwIiwiaGVhZGVyQmlkIjoxLCJpc0RpcmVjdFB1Ymxpc2hlciI6MCwiaGFzQ29uc2VudCI6MSwib3BlcmF0aW9uIjoiQ0xJRU5UX0hFQURFUl8yNSIsImlzQ29yZVNoaWVsZCI6MCwicGFydG5lckNyZWF0aXZlSWQiOiIyNDk2NDRfMzAweDI1MCIsInBhcnRuZXJEb21haW5zIjpbIndhbG1hcnQuY29tIl0sInNlbGxlclJlcXVlc3RJZCI6ImE3ODcyMWQ3LWE2ZmUtNGJiNS1hNjFkLTFhMDg1MzkxZTVlZCIsInNlbGxlckltcElkIjoiMzAwNDIxZDY0NWY2ZjRjOWMifQ&",
+          "adm": "<div id='beacon_97896' style='position:absolute;left:0px;top:0px;visibility:hidden;'><img src='https:\/\/rtb-us-east1.openx.net\/win\/conversant?p=0.068000&t=2DAABBgABAAECAAIBAAsAAgAAAyIcGAozeG96d1E1ekpoHBbYlLq-s8_-0YUBFu-eiJTkjsmfjAEAHBb79r3dq-TkkToW19-h99Dds9GzAQAW0Nm29AsVBjgkNTIzZDJkNzgtYjc1Yi0wM2VmLTI1Y2MtMmM2MjJmYTlhN2ZhACwcFQIAHBUCABwVAgAAHCbEvIODBBUEFQQmvLyDgwQWruuBgwTWiAEWiAEWiAEWZBZkFmQWZBbUAQAcHCwWgLGKpLvsg60qFuuytdnn4oDnpQEAABam-p2ABBaopLKABBaIrIKBBBaC1NKABBUYHBT0AxTYBAAVBCbUARbUARbUARE1DibUAQAsLBaXzu2B6O7j1_ABFrHv8IeZkI7RzAEAFtDZtvQLBiim-p2ABBaopLKABBaC1NKABBaIrIKBBBgOMjQ5NjQ0XzMwMHgyNTAWjAwW1AElBBZMJQKWwMqMAhEoAk9YDHoU-AMU2gQAFgIYA3J0YoEA3BsEiApiaWRfc291cmNlBVNTUlRCCGJpZF91dWlkJDgzZTVlOGRlLThjNzktNGE4ZC05OTQyLThiOGFkYTc1ZDI2MhhkZWxpdmVyeV9tZWRpdW1fY2F0ZWdvcnkDV0VCIXJldHJpZXZlZF9mcm9tX3NzcnRiX2Vwb2NoX21pbGxpcw0xNTk4NDc3OTI5MDgxAKwYmwJodHRwczovL21wZC5teHB0aW50Lm5ldC8xL1MxL0cxL1Q0ODA2L2RpP3Npej0zMDB4MjUwJmNwZD0yNDk2NDQmY3BzPTI3NSZzc2w9MSZtaWQ9QjMzNkI0X0NGQTRBNTczXzUyRDA1REEmbXBpZD1SMUIzNDJfQzZFRjI3MUVfMTMxRjQwOTZEJmJwPTAuMTA2MDYzNCZkbT0xNDkzREIyMDZGN0M0MUQ2NTRERDI1OUZDRjk2RTVDNSZyaT0wJnY9cTcxbzlGS0d4VVpGNUlONUEycGU5QUpRSWVEYlVSSjFIQ2p2cmRBbmx3M1F2UTFuVkQtSWhnJmFkaWQ9MSZ1dWlkPUE4emxpcHVrLUJJTEZIcl90SDVoT1p3KAt3YWxtYXJ0LmNvbQAAAA&ph=bec2690e-a73d-4d95-9901-75ad2a8d91b8'\/><\/div><script type=\"text\/javascript\" src=\"https:\/\/mpd.mxptint.net\/1\/S1\/G1\/T4806\/js?siz=300x250&cpd=249644&cps=275&ssl=1&mid=B336B4_CFA4A573_52D05DA&mpid=R1B342_C6EF271E_131F4096D&bp=0.1060634&dm=1493DB206F7C41D654DD259FCF96E5C5&ri=0&v=q71o9FKGxUZF5IN5A2pe9AJQIeDbURJ1HCjvrdAnlw3QvQ1nVD-Ihg&adid=1&uuid=A8zlipuk-BILFHr_tH5hOZw&spox=AAABdCy1ioekWBgw3-MoY-mUAAZXsF6Wi8M83w\"><\/script><noscript><iframe src=\"https:\/\/mpd.mxptint.net\/1\/S1\/G1\/T4806\/if?siz=300x250&cpd=249644&cps=275&ssl=1&mid=B336B4_CFA4A573_52D05DA&mpid=R1B342_C6EF271E_131F4096D&bp=0.1060634&dm=1493DB206F7C41D654DD259FCF96E5C5&ri=0&v=q71o9FKGxUZF5IN5A2pe9AJQIeDbURJ1HCjvrdAnlw3QvQ1nVD-Ihg&adid=1&uuid=A8zlipuk-BILFHr_tH5hOZw&spox=AAABdCy1ioekWBgw3-MoY-mUAAZXsF6Wi8M83w\" width=\"300px\" height=\"250px\" marginwidth=\"0\" marginheight=\"0\" frameborder=\"0\" scrolling=\"no\"><img src=\"https:\/\/mpc.mxptint.net\/11S2SEECE726S6C47S3CF2CS12CSFAS12C6SB336B4_CFA4A573_52D05DAS%3fhttp:\/\/noredir.mxptint.net\"\/><\/iframe><\/noscript><iframe src='https:\/\/us-u.openx.net\/w\/1.0\/pd?plm=6&ph=bec2690e-a73d-4d95-9901-75ad2a8d91b8' width='0' height='0' style='display:none;'><\/iframe>",
+          "adomain": ["walmart.com"],
+          "crid": "249644_300x250",
+          "w": 300,
+          "h": 250
+        },
+        "type": "banner"
+      }]
+    }
+  ]
+}
+
+
+
diff --git a/adapters/conversant/conversanttest/exemplary/simple_app.json b/adapters/conversant/conversanttest/exemplary/simple_app.json
new file mode 100644
index 00000000..303c60f7
--- /dev/null
+++ b/adapters/conversant/conversanttest/exemplary/simple_app.json
@@ -0,0 +1,114 @@
+{
+  "mockBidRequest": {
+    "id": "testauction",
+    "app": {
+      "id": "108060",
+      "bundle": "12341234",
+      "domain": "www.mypage.com"
+    },
+    "imp": [
+      {
+        "id": "1",
+        "banner": {
+          "format": [{"w": 300, "h": 250}]
+        },
+        "ext": {
+          "bidder": {
+            "site_id": "108060",
+            "bidfloor": 0.01,
+            "tag_id": "mytag",
+            "secure": 1
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36",
+      "ip": "192.168.1.1",
+      "dnt": 1
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "",
+        "body": {
+          "id": "testauction",
+          "app": {
+            "id": "108060",
+            "bundle": "12341234",
+            "domain": "www.mypage.com"
+          },
+          "imp": [
+            {
+              "id": "1",
+              "tagid": "mytag",
+              "secure": 1,
+              "bidfloor": 0.01,
+              "displaymanager": "prebid-s2s",
+              "displaymanagerver": "2.0.0",
+              "banner": {
+                "format": [{"w": 300, "h": 250}]
+              },
+              "ext": {
+                "bidder": {
+                  "site_id": "108060",
+                  "bidfloor": 0.01,
+                  "tag_id": "mytag",
+                  "secure": 1
+                }
+              }
+            }
+          ],
+          "device": {
+            "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36",
+            "ip": "192.168.1.1",
+            "dnt": 1
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "testauction",
+          "bidid": "c8d95f4b-bcbb-4a6c-adbb-4c7f33af3c24",
+          "cur": "USD",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "1",
+                  "impid": "1",
+                  "price": 0.0340,
+                  "nurl": "https:\/\/event.ad.cpe.dotomi.com\/cvx\/event\/imp?enc=eyJ1c2VyaWQiOiI3MTI3MDUzNzM3NTM3MTAzMjIiLCJwYXJ0bmVyVHhpZCI6ImUyZWUzNjZlLWEyMjgtNDI0Mi1hNjJlLTk4ODk3ODhiYzgxNCIsInR4aWQiOiI3MTE1NzQwNDg3NTczODUwMDIiLCJuZXR3b3JrUmVxdWVzdElkIjoiNzExNTc0MDQ4NzU3Mzg1ODc0Iiwic2lkIjoxMTgwOTgsImRpdmlzaW9uSWQiOjgsInRpZCI6OCwibW9iaWxlRGF0YSI6IjU5IiwiYmlkUHJpY2UiOjAuMDY4MCwicHViQ29zdCI6MC4wMzQwLCJwYXJ0bmVyRmVlIjowLjAxMzYsImlwU3RyaW5nIjoiNzMuMTE4LjEzMC4xODYiLCJzdXBwbHlUeXBlIjoxLCJpbnRlZ3JhdGlvblR5cGUiOjQsIm1lZGlhdGlvblR5cGUiOjEyNiwicGxhY2VtZW50SWQiOiIxMTY5ODcwIiwiaGVhZGVyQmlkIjoxLCJpc0RpcmVjdFB1Ymxpc2hlciI6MCwiaGFzQ29uc2VudCI6MSwib3BlcmF0aW9uIjoiQ0xJRU5UX0hFQURFUl8yNSIsImlzQ29yZVNoaWVsZCI6MCwicGFydG5lckNyZWF0aXZlSWQiOiIyNDk2NDRfMzAweDI1MCIsInBhcnRuZXJEb21haW5zIjpbIndhbG1hcnQuY29tIl0sInNlbGxlclJlcXVlc3RJZCI6ImE3ODcyMWQ3LWE2ZmUtNGJiNS1hNjFkLTFhMDg1MzkxZTVlZCIsInNlbGxlckltcElkIjoiMzAwNDIxZDY0NWY2ZjRjOWMifQ&",
+                  "adm": "<div id='beacon_97896' style='position:absolute;left:0px;top:0px;visibility:hidden;'><img src='https:\/\/rtb-us-east1.openx.net\/win\/conversant?p=0.068000&t=2DAABBgABAAECAAIBAAsAAgAAAyIcGAozeG96d1E1ekpoHBbYlLq-s8_-0YUBFu-eiJTkjsmfjAEAHBb79r3dq-TkkToW19-h99Dds9GzAQAW0Nm29AsVBjgkNTIzZDJkNzgtYjc1Yi0wM2VmLTI1Y2MtMmM2MjJmYTlhN2ZhACwcFQIAHBUCABwVAgAAHCbEvIODBBUEFQQmvLyDgwQWruuBgwTWiAEWiAEWiAEWZBZkFmQWZBbUAQAcHCwWgLGKpLvsg60qFuuytdnn4oDnpQEAABam-p2ABBaopLKABBaIrIKBBBaC1NKABBUYHBT0AxTYBAAVBCbUARbUARbUARE1DibUAQAsLBaXzu2B6O7j1_ABFrHv8IeZkI7RzAEAFtDZtvQLBiim-p2ABBaopLKABBaC1NKABBaIrIKBBBgOMjQ5NjQ0XzMwMHgyNTAWjAwW1AElBBZMJQKWwMqMAhEoAk9YDHoU-AMU2gQAFgIYA3J0YoEA3BsEiApiaWRfc291cmNlBVNTUlRCCGJpZF91dWlkJDgzZTVlOGRlLThjNzktNGE4ZC05OTQyLThiOGFkYTc1ZDI2MhhkZWxpdmVyeV9tZWRpdW1fY2F0ZWdvcnkDV0VCIXJldHJpZXZlZF9mcm9tX3NzcnRiX2Vwb2NoX21pbGxpcw0xNTk4NDc3OTI5MDgxAKwYmwJodHRwczovL21wZC5teHB0aW50Lm5ldC8xL1MxL0cxL1Q0ODA2L2RpP3Npej0zMDB4MjUwJmNwZD0yNDk2NDQmY3BzPTI3NSZzc2w9MSZtaWQ9QjMzNkI0X0NGQTRBNTczXzUyRDA1REEmbXBpZD1SMUIzNDJfQzZFRjI3MUVfMTMxRjQwOTZEJmJwPTAuMTA2MDYzNCZkbT0xNDkzREIyMDZGN0M0MUQ2NTRERDI1OUZDRjk2RTVDNSZyaT0wJnY9cTcxbzlGS0d4VVpGNUlONUEycGU5QUpRSWVEYlVSSjFIQ2p2cmRBbmx3M1F2UTFuVkQtSWhnJmFkaWQ9MSZ1dWlkPUE4emxpcHVrLUJJTEZIcl90SDVoT1p3KAt3YWxtYXJ0LmNvbQAAAA&ph=bec2690e-a73d-4d95-9901-75ad2a8d91b8'\/><\/div><script type=\"text\/javascript\" src=\"https:\/\/mpd.mxptint.net\/1\/S1\/G1\/T4806\/js?siz=300x250&cpd=249644&cps=275&ssl=1&mid=B336B4_CFA4A573_52D05DA&mpid=R1B342_C6EF271E_131F4096D&bp=0.1060634&dm=1493DB206F7C41D654DD259FCF96E5C5&ri=0&v=q71o9FKGxUZF5IN5A2pe9AJQIeDbURJ1HCjvrdAnlw3QvQ1nVD-Ihg&adid=1&uuid=A8zlipuk-BILFHr_tH5hOZw&spox=AAABdCy1ioekWBgw3-MoY-mUAAZXsF6Wi8M83w\"><\/script><noscript><iframe src=\"https:\/\/mpd.mxptint.net\/1\/S1\/G1\/T4806\/if?siz=300x250&cpd=249644&cps=275&ssl=1&mid=B336B4_CFA4A573_52D05DA&mpid=R1B342_C6EF271E_131F4096D&bp=0.1060634&dm=1493DB206F7C41D654DD259FCF96E5C5&ri=0&v=q71o9FKGxUZF5IN5A2pe9AJQIeDbURJ1HCjvrdAnlw3QvQ1nVD-Ihg&adid=1&uuid=A8zlipuk-BILFHr_tH5hOZw&spox=AAABdCy1ioekWBgw3-MoY-mUAAZXsF6Wi8M83w\" width=\"300px\" height=\"250px\" marginwidth=\"0\" marginheight=\"0\" frameborder=\"0\" scrolling=\"no\"><img src=\"https:\/\/mpc.mxptint.net\/11S2SEECE726S6C47S3CF2CS12CSFAS12C6SB336B4_CFA4A573_52D05DAS%3fhttp:\/\/noredir.mxptint.net\"\/><\/iframe><\/noscript><iframe src='https:\/\/us-u.openx.net\/w\/1.0\/pd?plm=6&ph=bec2690e-a73d-4d95-9901-75ad2a8d91b8' width='0' height='0' style='display:none;'><\/iframe>",
+                  "adomain": ["walmart.com"],
+                  "crid": "249644_300x250",
+                  "w": 300,
+                  "h": 250
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }],
+  "expectedBidResponses": [
+    {
+      "bids": [{
+        "bid": {
+          "id": "1",
+          "impid": "1",
+          "price": 0.0340,
+          "nurl": "https:\/\/event.ad.cpe.dotomi.com\/cvx\/event\/imp?enc=eyJ1c2VyaWQiOiI3MTI3MDUzNzM3NTM3MTAzMjIiLCJwYXJ0bmVyVHhpZCI6ImUyZWUzNjZlLWEyMjgtNDI0Mi1hNjJlLTk4ODk3ODhiYzgxNCIsInR4aWQiOiI3MTE1NzQwNDg3NTczODUwMDIiLCJuZXR3b3JrUmVxdWVzdElkIjoiNzExNTc0MDQ4NzU3Mzg1ODc0Iiwic2lkIjoxMTgwOTgsImRpdmlzaW9uSWQiOjgsInRpZCI6OCwibW9iaWxlRGF0YSI6IjU5IiwiYmlkUHJpY2UiOjAuMDY4MCwicHViQ29zdCI6MC4wMzQwLCJwYXJ0bmVyRmVlIjowLjAxMzYsImlwU3RyaW5nIjoiNzMuMTE4LjEzMC4xODYiLCJzdXBwbHlUeXBlIjoxLCJpbnRlZ3JhdGlvblR5cGUiOjQsIm1lZGlhdGlvblR5cGUiOjEyNiwicGxhY2VtZW50SWQiOiIxMTY5ODcwIiwiaGVhZGVyQmlkIjoxLCJpc0RpcmVjdFB1Ymxpc2hlciI6MCwiaGFzQ29uc2VudCI6MSwib3BlcmF0aW9uIjoiQ0xJRU5UX0hFQURFUl8yNSIsImlzQ29yZVNoaWVsZCI6MCwicGFydG5lckNyZWF0aXZlSWQiOiIyNDk2NDRfMzAweDI1MCIsInBhcnRuZXJEb21haW5zIjpbIndhbG1hcnQuY29tIl0sInNlbGxlclJlcXVlc3RJZCI6ImE3ODcyMWQ3LWE2ZmUtNGJiNS1hNjFkLTFhMDg1MzkxZTVlZCIsInNlbGxlckltcElkIjoiMzAwNDIxZDY0NWY2ZjRjOWMifQ&",
+          "adm": "<div id='beacon_97896' style='position:absolute;left:0px;top:0px;visibility:hidden;'><img src='https:\/\/rtb-us-east1.openx.net\/win\/conversant?p=0.068000&t=2DAABBgABAAECAAIBAAsAAgAAAyIcGAozeG96d1E1ekpoHBbYlLq-s8_-0YUBFu-eiJTkjsmfjAEAHBb79r3dq-TkkToW19-h99Dds9GzAQAW0Nm29AsVBjgkNTIzZDJkNzgtYjc1Yi0wM2VmLTI1Y2MtMmM2MjJmYTlhN2ZhACwcFQIAHBUCABwVAgAAHCbEvIODBBUEFQQmvLyDgwQWruuBgwTWiAEWiAEWiAEWZBZkFmQWZBbUAQAcHCwWgLGKpLvsg60qFuuytdnn4oDnpQEAABam-p2ABBaopLKABBaIrIKBBBaC1NKABBUYHBT0AxTYBAAVBCbUARbUARbUARE1DibUAQAsLBaXzu2B6O7j1_ABFrHv8IeZkI7RzAEAFtDZtvQLBiim-p2ABBaopLKABBaC1NKABBaIrIKBBBgOMjQ5NjQ0XzMwMHgyNTAWjAwW1AElBBZMJQKWwMqMAhEoAk9YDHoU-AMU2gQAFgIYA3J0YoEA3BsEiApiaWRfc291cmNlBVNTUlRCCGJpZF91dWlkJDgzZTVlOGRlLThjNzktNGE4ZC05OTQyLThiOGFkYTc1ZDI2MhhkZWxpdmVyeV9tZWRpdW1fY2F0ZWdvcnkDV0VCIXJldHJpZXZlZF9mcm9tX3NzcnRiX2Vwb2NoX21pbGxpcw0xNTk4NDc3OTI5MDgxAKwYmwJodHRwczovL21wZC5teHB0aW50Lm5ldC8xL1MxL0cxL1Q0ODA2L2RpP3Npej0zMDB4MjUwJmNwZD0yNDk2NDQmY3BzPTI3NSZzc2w9MSZtaWQ9QjMzNkI0X0NGQTRBNTczXzUyRDA1REEmbXBpZD1SMUIzNDJfQzZFRjI3MUVfMTMxRjQwOTZEJmJwPTAuMTA2MDYzNCZkbT0xNDkzREIyMDZGN0M0MUQ2NTRERDI1OUZDRjk2RTVDNSZyaT0wJnY9cTcxbzlGS0d4VVpGNUlONUEycGU5QUpRSWVEYlVSSjFIQ2p2cmRBbmx3M1F2UTFuVkQtSWhnJmFkaWQ9MSZ1dWlkPUE4emxpcHVrLUJJTEZIcl90SDVoT1p3KAt3YWxtYXJ0LmNvbQAAAA&ph=bec2690e-a73d-4d95-9901-75ad2a8d91b8'\/><\/div><script type=\"text\/javascript\" src=\"https:\/\/mpd.mxptint.net\/1\/S1\/G1\/T4806\/js?siz=300x250&cpd=249644&cps=275&ssl=1&mid=B336B4_CFA4A573_52D05DA&mpid=R1B342_C6EF271E_131F4096D&bp=0.1060634&dm=1493DB206F7C41D654DD259FCF96E5C5&ri=0&v=q71o9FKGxUZF5IN5A2pe9AJQIeDbURJ1HCjvrdAnlw3QvQ1nVD-Ihg&adid=1&uuid=A8zlipuk-BILFHr_tH5hOZw&spox=AAABdCy1ioekWBgw3-MoY-mUAAZXsF6Wi8M83w\"><\/script><noscript><iframe src=\"https:\/\/mpd.mxptint.net\/1\/S1\/G1\/T4806\/if?siz=300x250&cpd=249644&cps=275&ssl=1&mid=B336B4_CFA4A573_52D05DA&mpid=R1B342_C6EF271E_131F4096D&bp=0.1060634&dm=1493DB206F7C41D654DD259FCF96E5C5&ri=0&v=q71o9FKGxUZF5IN5A2pe9AJQIeDbURJ1HCjvrdAnlw3QvQ1nVD-Ihg&adid=1&uuid=A8zlipuk-BILFHr_tH5hOZw&spox=AAABdCy1ioekWBgw3-MoY-mUAAZXsF6Wi8M83w\" width=\"300px\" height=\"250px\" marginwidth=\"0\" marginheight=\"0\" frameborder=\"0\" scrolling=\"no\"><img src=\"https:\/\/mpc.mxptint.net\/11S2SEECE726S6C47S3CF2CS12CSFAS12C6SB336B4_CFA4A573_52D05DAS%3fhttp:\/\/noredir.mxptint.net\"\/><\/iframe><\/noscript><iframe src='https:\/\/us-u.openx.net\/w\/1.0\/pd?plm=6&ph=bec2690e-a73d-4d95-9901-75ad2a8d91b8' width='0' height='0' style='display:none;'><\/iframe>",
+          "adomain": ["walmart.com"],
+          "crid": "249644_300x250",
+          "w": 300,
+          "h": 250
+        },
+        "type": "banner"
+      }]
+    }
+  ]
+}
diff --git a/adapters/conversant/conversanttest/exemplary/video.json b/adapters/conversant/conversanttest/exemplary/video.json
new file mode 100644
index 00000000..475dd796
--- /dev/null
+++ b/adapters/conversant/conversanttest/exemplary/video.json
@@ -0,0 +1,138 @@
+{
+  "mockBidRequest": {
+    "id": "testauction",
+    "imp": [
+      {
+        "id": "1",
+        "video": {
+          "w": 300,
+          "h": 250
+        },
+        "ext": {
+          "bidder": {
+            "site_id": "108060",
+            "bidfloor": 0.01,
+            "tag_id": "mytag",
+            "secure": 1,
+            "mimes": ["video/mp4", "video/x-flv"],
+            "position": 1,
+            "mobile": 0,
+            "api": [1,2,3,4],
+            "protocols": [1,2,3,5,6],
+            "maxduration": 60
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36",
+      "ip": "192.168.1.1",
+      "dnt": 1
+    },
+    "site": {
+      "domain": "www.mypage.com"
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "",
+        "body": {
+          "id": "testauction",
+          "site": {
+            "id": "108060",
+            "domain": "www.mypage.com"
+          },
+          "imp": [
+            {
+              "id": "1",
+              "tagid": "mytag",
+              "secure": 1,
+              "bidfloor": 0.01,
+              "displaymanager": "prebid-s2s",
+              "displaymanagerver": "2.0.0",
+              "video": {
+                "w": 300,
+                "h": 250,
+                "mimes": [
+                  "video/mp4",
+                  "video/x-flv"
+                ],
+                "pos": 1,
+                "api": [ 1, 2, 3, 4 ],
+                "protocols": [ 1, 2, 3, 5, 6 ],
+                "maxduration": 60
+              },
+              "ext": {
+                "bidder": {
+                  "site_id": "108060",
+                  "bidfloor": 0.01,
+                  "tag_id": "mytag",
+                  "secure": 1,
+                  "mimes": [
+                    "video/mp4",
+                    "video/x-flv"
+                  ],
+                  "position": 1,
+                  "mobile": 0,
+                  "api": [ 1, 2, 3, 4 ],
+                  "protocols": [ 1, 2, 3, 5, 6 ],
+                  "maxduration": 60
+                }
+              }
+            }
+          ],
+          "device": {
+            "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36",
+            "ip": "192.168.1.1",
+            "dnt": 1
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "testauction",
+          "bidid": "c8d95f4b-bcbb-4a6c-adbb-4c7f33af3c24",
+          "cur": "USD",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "1",
+                  "impid": "1",
+                  "price": 0.0340,
+                  "nurl": "https:\/\/event.ad.cpe.dotomi.com\/cvx\/event\/imp?enc=eyJ1c2VyaWQiOiI3MTI3MDUzNzM3NTM3MTAzMjIiLCJwYXJ0bmVyVHhpZCI6ImUyZWUzNjZlLWEyMjgtNDI0Mi1hNjJlLTk4ODk3ODhiYzgxNCIsInR4aWQiOiI3MTE1NzQwNDg3NTczODUwMDIiLCJuZXR3b3JrUmVxdWVzdElkIjoiNzExNTc0MDQ4NzU3Mzg1ODc0Iiwic2lkIjoxMTgwOTgsImRpdmlzaW9uSWQiOjgsInRpZCI6OCwibW9iaWxlRGF0YSI6IjU5IiwiYmlkUHJpY2UiOjAuMDY4MCwicHViQ29zdCI6MC4wMzQwLCJwYXJ0bmVyRmVlIjowLjAxMzYsImlwU3RyaW5nIjoiNzMuMTE4LjEzMC4xODYiLCJzdXBwbHlUeXBlIjoxLCJpbnRlZ3JhdGlvblR5cGUiOjQsIm1lZGlhdGlvblR5cGUiOjEyNiwicGxhY2VtZW50SWQiOiIxMTY5ODcwIiwiaGVhZGVyQmlkIjoxLCJpc0RpcmVjdFB1Ymxpc2hlciI6MCwiaGFzQ29uc2VudCI6MSwib3BlcmF0aW9uIjoiQ0xJRU5UX0hFQURFUl8yNSIsImlzQ29yZVNoaWVsZCI6MCwicGFydG5lckNyZWF0aXZlSWQiOiIyNDk2NDRfMzAweDI1MCIsInBhcnRuZXJEb21haW5zIjpbIndhbG1hcnQuY29tIl0sInNlbGxlclJlcXVlc3RJZCI6ImE3ODcyMWQ3LWE2ZmUtNGJiNS1hNjFkLTFhMDg1MzkxZTVlZCIsInNlbGxlckltcElkIjoiMzAwNDIxZDY0NWY2ZjRjOWMifQ&",
+                  "adm": "<div id='beacon_97896' style='position:absolute;left:0px;top:0px;visibility:hidden;'><img src='https:\/\/rtb-us-east1.openx.net\/win\/conversant?p=0.068000&t=2DAABBgABAAECAAIBAAsAAgAAAyIcGAozeG96d1E1ekpoHBbYlLq-s8_-0YUBFu-eiJTkjsmfjAEAHBb79r3dq-TkkToW19-h99Dds9GzAQAW0Nm29AsVBjgkNTIzZDJkNzgtYjc1Yi0wM2VmLTI1Y2MtMmM2MjJmYTlhN2ZhACwcFQIAHBUCABwVAgAAHCbEvIODBBUEFQQmvLyDgwQWruuBgwTWiAEWiAEWiAEWZBZkFmQWZBbUAQAcHCwWgLGKpLvsg60qFuuytdnn4oDnpQEAABam-p2ABBaopLKABBaIrIKBBBaC1NKABBUYHBT0AxTYBAAVBCbUARbUARbUARE1DibUAQAsLBaXzu2B6O7j1_ABFrHv8IeZkI7RzAEAFtDZtvQLBiim-p2ABBaopLKABBaC1NKABBaIrIKBBBgOMjQ5NjQ0XzMwMHgyNTAWjAwW1AElBBZMJQKWwMqMAhEoAk9YDHoU-AMU2gQAFgIYA3J0YoEA3BsEiApiaWRfc291cmNlBVNTUlRCCGJpZF91dWlkJDgzZTVlOGRlLThjNzktNGE4ZC05OTQyLThiOGFkYTc1ZDI2MhhkZWxpdmVyeV9tZWRpdW1fY2F0ZWdvcnkDV0VCIXJldHJpZXZlZF9mcm9tX3NzcnRiX2Vwb2NoX21pbGxpcw0xNTk4NDc3OTI5MDgxAKwYmwJodHRwczovL21wZC5teHB0aW50Lm5ldC8xL1MxL0cxL1Q0ODA2L2RpP3Npej0zMDB4MjUwJmNwZD0yNDk2NDQmY3BzPTI3NSZzc2w9MSZtaWQ9QjMzNkI0X0NGQTRBNTczXzUyRDA1REEmbXBpZD1SMUIzNDJfQzZFRjI3MUVfMTMxRjQwOTZEJmJwPTAuMTA2MDYzNCZkbT0xNDkzREIyMDZGN0M0MUQ2NTRERDI1OUZDRjk2RTVDNSZyaT0wJnY9cTcxbzlGS0d4VVpGNUlONUEycGU5QUpRSWVEYlVSSjFIQ2p2cmRBbmx3M1F2UTFuVkQtSWhnJmFkaWQ9MSZ1dWlkPUE4emxpcHVrLUJJTEZIcl90SDVoT1p3KAt3YWxtYXJ0LmNvbQAAAA&ph=bec2690e-a73d-4d95-9901-75ad2a8d91b8'\/><\/div><script type=\"text\/javascript\" src=\"https:\/\/mpd.mxptint.net\/1\/S1\/G1\/T4806\/js?siz=300x250&cpd=249644&cps=275&ssl=1&mid=B336B4_CFA4A573_52D05DA&mpid=R1B342_C6EF271E_131F4096D&bp=0.1060634&dm=1493DB206F7C41D654DD259FCF96E5C5&ri=0&v=q71o9FKGxUZF5IN5A2pe9AJQIeDbURJ1HCjvrdAnlw3QvQ1nVD-Ihg&adid=1&uuid=A8zlipuk-BILFHr_tH5hOZw&spox=AAABdCy1ioekWBgw3-MoY-mUAAZXsF6Wi8M83w\"><\/script><noscript><iframe src=\"https:\/\/mpd.mxptint.net\/1\/S1\/G1\/T4806\/if?siz=300x250&cpd=249644&cps=275&ssl=1&mid=B336B4_CFA4A573_52D05DA&mpid=R1B342_C6EF271E_131F4096D&bp=0.1060634&dm=1493DB206F7C41D654DD259FCF96E5C5&ri=0&v=q71o9FKGxUZF5IN5A2pe9AJQIeDbURJ1HCjvrdAnlw3QvQ1nVD-Ihg&adid=1&uuid=A8zlipuk-BILFHr_tH5hOZw&spox=AAABdCy1ioekWBgw3-MoY-mUAAZXsF6Wi8M83w\" width=\"300px\" height=\"250px\" marginwidth=\"0\" marginheight=\"0\" frameborder=\"0\" scrolling=\"no\"><img src=\"https:\/\/mpc.mxptint.net\/11S2SEECE726S6C47S3CF2CS12CSFAS12C6SB336B4_CFA4A573_52D05DAS%3fhttp:\/\/noredir.mxptint.net\"\/><\/iframe><\/noscript><iframe src='https:\/\/us-u.openx.net\/w\/1.0\/pd?plm=6&ph=bec2690e-a73d-4d95-9901-75ad2a8d91b8' width='0' height='0' style='display:none;'><\/iframe>",
+                  "adomain": [
+                    "walmart.com"
+                  ],
+                  "crid": "249644_300x250",
+                  "w": 300,
+                  "h": 250
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }],
+  "expectedBidResponses": [
+    {
+      "bids": [{
+        "bid": {
+          "id": "1",
+          "impid": "1",
+          "price": 0.0340,
+          "nurl": "https:\/\/event.ad.cpe.dotomi.com\/cvx\/event\/imp?enc=eyJ1c2VyaWQiOiI3MTI3MDUzNzM3NTM3MTAzMjIiLCJwYXJ0bmVyVHhpZCI6ImUyZWUzNjZlLWEyMjgtNDI0Mi1hNjJlLTk4ODk3ODhiYzgxNCIsInR4aWQiOiI3MTE1NzQwNDg3NTczODUwMDIiLCJuZXR3b3JrUmVxdWVzdElkIjoiNzExNTc0MDQ4NzU3Mzg1ODc0Iiwic2lkIjoxMTgwOTgsImRpdmlzaW9uSWQiOjgsInRpZCI6OCwibW9iaWxlRGF0YSI6IjU5IiwiYmlkUHJpY2UiOjAuMDY4MCwicHViQ29zdCI6MC4wMzQwLCJwYXJ0bmVyRmVlIjowLjAxMzYsImlwU3RyaW5nIjoiNzMuMTE4LjEzMC4xODYiLCJzdXBwbHlUeXBlIjoxLCJpbnRlZ3JhdGlvblR5cGUiOjQsIm1lZGlhdGlvblR5cGUiOjEyNiwicGxhY2VtZW50SWQiOiIxMTY5ODcwIiwiaGVhZGVyQmlkIjoxLCJpc0RpcmVjdFB1Ymxpc2hlciI6MCwiaGFzQ29uc2VudCI6MSwib3BlcmF0aW9uIjoiQ0xJRU5UX0hFQURFUl8yNSIsImlzQ29yZVNoaWVsZCI6MCwicGFydG5lckNyZWF0aXZlSWQiOiIyNDk2NDRfMzAweDI1MCIsInBhcnRuZXJEb21haW5zIjpbIndhbG1hcnQuY29tIl0sInNlbGxlclJlcXVlc3RJZCI6ImE3ODcyMWQ3LWE2ZmUtNGJiNS1hNjFkLTFhMDg1MzkxZTVlZCIsInNlbGxlckltcElkIjoiMzAwNDIxZDY0NWY2ZjRjOWMifQ&",
+          "adm": "<div id='beacon_97896' style='position:absolute;left:0px;top:0px;visibility:hidden;'><img src='https:\/\/rtb-us-east1.openx.net\/win\/conversant?p=0.068000&t=2DAABBgABAAECAAIBAAsAAgAAAyIcGAozeG96d1E1ekpoHBbYlLq-s8_-0YUBFu-eiJTkjsmfjAEAHBb79r3dq-TkkToW19-h99Dds9GzAQAW0Nm29AsVBjgkNTIzZDJkNzgtYjc1Yi0wM2VmLTI1Y2MtMmM2MjJmYTlhN2ZhACwcFQIAHBUCABwVAgAAHCbEvIODBBUEFQQmvLyDgwQWruuBgwTWiAEWiAEWiAEWZBZkFmQWZBbUAQAcHCwWgLGKpLvsg60qFuuytdnn4oDnpQEAABam-p2ABBaopLKABBaIrIKBBBaC1NKABBUYHBT0AxTYBAAVBCbUARbUARbUARE1DibUAQAsLBaXzu2B6O7j1_ABFrHv8IeZkI7RzAEAFtDZtvQLBiim-p2ABBaopLKABBaC1NKABBaIrIKBBBgOMjQ5NjQ0XzMwMHgyNTAWjAwW1AElBBZMJQKWwMqMAhEoAk9YDHoU-AMU2gQAFgIYA3J0YoEA3BsEiApiaWRfc291cmNlBVNTUlRCCGJpZF91dWlkJDgzZTVlOGRlLThjNzktNGE4ZC05OTQyLThiOGFkYTc1ZDI2MhhkZWxpdmVyeV9tZWRpdW1fY2F0ZWdvcnkDV0VCIXJldHJpZXZlZF9mcm9tX3NzcnRiX2Vwb2NoX21pbGxpcw0xNTk4NDc3OTI5MDgxAKwYmwJodHRwczovL21wZC5teHB0aW50Lm5ldC8xL1MxL0cxL1Q0ODA2L2RpP3Npej0zMDB4MjUwJmNwZD0yNDk2NDQmY3BzPTI3NSZzc2w9MSZtaWQ9QjMzNkI0X0NGQTRBNTczXzUyRDA1REEmbXBpZD1SMUIzNDJfQzZFRjI3MUVfMTMxRjQwOTZEJmJwPTAuMTA2MDYzNCZkbT0xNDkzREIyMDZGN0M0MUQ2NTRERDI1OUZDRjk2RTVDNSZyaT0wJnY9cTcxbzlGS0d4VVpGNUlONUEycGU5QUpRSWVEYlVSSjFIQ2p2cmRBbmx3M1F2UTFuVkQtSWhnJmFkaWQ9MSZ1dWlkPUE4emxpcHVrLUJJTEZIcl90SDVoT1p3KAt3YWxtYXJ0LmNvbQAAAA&ph=bec2690e-a73d-4d95-9901-75ad2a8d91b8'\/><\/div><script type=\"text\/javascript\" src=\"https:\/\/mpd.mxptint.net\/1\/S1\/G1\/T4806\/js?siz=300x250&cpd=249644&cps=275&ssl=1&mid=B336B4_CFA4A573_52D05DA&mpid=R1B342_C6EF271E_131F4096D&bp=0.1060634&dm=1493DB206F7C41D654DD259FCF96E5C5&ri=0&v=q71o9FKGxUZF5IN5A2pe9AJQIeDbURJ1HCjvrdAnlw3QvQ1nVD-Ihg&adid=1&uuid=A8zlipuk-BILFHr_tH5hOZw&spox=AAABdCy1ioekWBgw3-MoY-mUAAZXsF6Wi8M83w\"><\/script><noscript><iframe src=\"https:\/\/mpd.mxptint.net\/1\/S1\/G1\/T4806\/if?siz=300x250&cpd=249644&cps=275&ssl=1&mid=B336B4_CFA4A573_52D05DA&mpid=R1B342_C6EF271E_131F4096D&bp=0.1060634&dm=1493DB206F7C41D654DD259FCF96E5C5&ri=0&v=q71o9FKGxUZF5IN5A2pe9AJQIeDbURJ1HCjvrdAnlw3QvQ1nVD-Ihg&adid=1&uuid=A8zlipuk-BILFHr_tH5hOZw&spox=AAABdCy1ioekWBgw3-MoY-mUAAZXsF6Wi8M83w\" width=\"300px\" height=\"250px\" marginwidth=\"0\" marginheight=\"0\" frameborder=\"0\" scrolling=\"no\"><img src=\"https:\/\/mpc.mxptint.net\/11S2SEECE726S6C47S3CF2CS12CSFAS12C6SB336B4_CFA4A573_52D05DAS%3fhttp:\/\/noredir.mxptint.net\"\/><\/iframe><\/noscript><iframe src='https:\/\/us-u.openx.net\/w\/1.0\/pd?plm=6&ph=bec2690e-a73d-4d95-9901-75ad2a8d91b8' width='0' height='0' style='display:none;'><\/iframe>",
+          "adomain": ["walmart.com"],
+          "crid": "249644_300x250",
+          "w": 300,
+          "h": 250
+        },
+        "type": "video"
+      }]
+    }
+  ]
+}
diff --git a/adapters/conversant/conversanttest/supplemental/missing_cnvrext.json b/adapters/conversant/conversanttest/supplemental/missing_cnvrext.json
new file mode 100644
index 00000000..3f71023b
--- /dev/null
+++ b/adapters/conversant/conversanttest/supplemental/missing_cnvrext.json
@@ -0,0 +1,32 @@
+{
+  "mockBidRequest": {
+    "id": "testauction",
+    "imp": [
+      {
+        "id": "1",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+        }
+      }
+    ],
+    "device": {
+      "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36",
+      "ip": "192.168.1.1",
+      "dnt": 1
+    },
+    "site": {
+      "domain": "www.mypage.com"
+    }
+  },
+  "expectedMakeRequestsErrors": [{
+    "value": "Impression[0] missing ext.bidder object",
+    "comparison": "literal"
+  }]
+}
diff --git a/adapters/conversant/conversanttest/supplemental/missing_ext.json b/adapters/conversant/conversanttest/supplemental/missing_ext.json
new file mode 100644
index 00000000..74f20a65
--- /dev/null
+++ b/adapters/conversant/conversanttest/supplemental/missing_ext.json
@@ -0,0 +1,30 @@
+{
+  "mockBidRequest": {
+    "id": "testauction",
+    "imp": [
+      {
+        "id": "1",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        }
+      }
+    ],
+    "device": {
+      "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36",
+      "ip": "192.168.1.1",
+      "dnt": 1
+    },
+    "site": {
+      "domain": "www.mypage.com"
+    }
+  },
+  "expectedMakeRequestsErrors": [{
+    "value": "Impression[0] missing ext object",
+    "comparison": "literal"
+  }]
+}
diff --git a/adapters/conversant/conversanttest/supplemental/missing_siteid.json b/adapters/conversant/conversanttest/supplemental/missing_siteid.json
new file mode 100644
index 00000000..30174f88
--- /dev/null
+++ b/adapters/conversant/conversanttest/supplemental/missing_siteid.json
@@ -0,0 +1,35 @@
+{
+  "mockBidRequest": {
+    "id": "testauction",
+    "imp": [
+      {
+        "id": "1",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "bidfloor": 0.01
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36",
+      "ip": "192.168.1.1",
+      "dnt": 1
+    },
+    "site": {
+      "domain": "www.mypage.com"
+    }
+  },
+  "expectedMakeRequestsErrors": [{
+    "value": "Impression[0] requires ext.bidder.site_id",
+    "comparison": "literal"
+  }]
+}
diff --git a/adapters/conversant/conversanttest/supplemental/server_badresponse.json b/adapters/conversant/conversanttest/supplemental/server_badresponse.json
new file mode 100644
index 00000000..96cb4b46
--- /dev/null
+++ b/adapters/conversant/conversanttest/supplemental/server_badresponse.json
@@ -0,0 +1,57 @@
+{
+  "mockBidRequest": {
+    "id": "testauction",
+    "imp": [
+      {
+        "id": "1",
+        "banner": {
+          "format": [{"w": 300, "h": 250}]
+        },
+        "ext": {
+          "bidder": {
+            "site_id": "108060"
+          }
+        }
+      }
+    ],
+    "site": {
+      "domain": "www.mypage.com"
+    }
+  },
+  "httpCalls": [{
+    "expectedRequest": {
+      "uri": "",
+      "body": {
+        "id": "testauction",
+        "site": {
+          "id": "108060",
+          "domain": "www.mypage.com"
+        },
+        "imp": [
+          {
+            "id": "1",
+            "displaymanager": "prebid-s2s",
+            "displaymanagerver": "2.0.0",
+            "banner": {
+              "format": [{"w": 300, "h": 250}]
+            },
+            "ext": {
+              "bidder": {
+                "site_id": "108060"
+              }
+            }
+          }
+        ]
+      }
+    },
+    "mockResponse": {
+      "status": 200,
+      "body": {
+      }
+    }
+  }],
+  "expectedMakeBidsErrors": [{
+    "value": "Empty bid request",
+    "comparison": "literal"
+  }]
+}
diff --git a/adapters/conversant/conversanttest/supplemental/server_nocontent.json b/adapters/conversant/conversanttest/supplemental/server_nocontent.json
new file mode 100644
index 00000000..f1436452
--- /dev/null
+++ b/adapters/conversant/conversanttest/supplemental/server_nocontent.json
@@ -0,0 +1,51 @@
+{
+  "mockBidRequest": {
+    "id": "testauction",
+    "imp": [
+      {
+        "id": "1",
+        "banner": {
+          "format": [{"w": 300, "h": 250}]
+        },
+        "ext": {
+          "bidder": {
+            "site_id": "108060"
+          }
+        }
+      }
+    ],
+    "site": {
+      "domain": "www.mypage.com"
+    }
+  },
+  "httpCalls": [{
+    "expectedRequest": {
+      "uri": "",
+      "body": {
+        "id": "testauction",
+        "site": {
+          "id": "108060",
+          "domain": "www.mypage.com"
+        },
+        "imp": [
+          {
+            "id": "1",
+            "displaymanager": "prebid-s2s",
+            "displaymanagerver": "2.0.0",
+            "banner": {
+              "format": [{"w": 300, "h": 250}]
+            },
+            "ext": {
+              "bidder": {
+                "site_id": "108060"
+              }
+            }
+          }
+        ]
+      }
+    },
+    "mockResponse": {
+      "status": 204
+    }
+  }]
+}
diff --git a/adapters/conversant/conversanttest/supplemental/server_unknownstatus.json b/adapters/conversant/conversanttest/supplemental/server_unknownstatus.json
new file mode 100644
index 00000000..85586f06
--- /dev/null
+++ b/adapters/conversant/conversanttest/supplemental/server_unknownstatus.json
@@ -0,0 +1,55 @@
+{
+  "mockBidRequest": {
+    "id": "testauction",
+    "imp": [
+      {
+        "id": "1",
+        "banner": {
+          "format": [{"w": 300, "h": 250}]
+        },
+        "ext": {
+          "bidder": {
+            "site_id": "108060"
+          }
+        }
+      }
+    ],
+    "site": {
+      "domain": "www.mypage.com"
+    }
+  },
+  "httpCalls": [{
+    "expectedRequest": {
+      "uri": "",
+      "body": {
+        "id": "testauction",
+        "site": {
+          "id": "108060",
+          "domain": "www.mypage.com"
+        },
+        "imp": [
+          {
+            "id": "1",
+            "displaymanager": "prebid-s2s",
+            "displaymanagerver": "2.0.0",
+            "banner": {
+              "format": [{"w": 300, "h": 250}]
+            },
+            "ext": {
+              "bidder": {
+                "site_id": "108060"
+              }
+            }
+          }
+        ]
+      }
+    },
+    "mockResponse": {
+      "status": 302
+    }
+  }],
+  "expectedMakeBidsErrors": [{
+    "value": "Unexpected status code: 302",
+    "comparison": "literal"
+  }]
+}
diff --git a/adapters/datablocks/usersync_test.go b/adapters/datablocks/usersync_test.go
index f8500ab9..a7518e9b 100644
--- a/adapters/datablocks/usersync_test.go
+++ b/adapters/datablocks/usersync_test.go
@@ -23,7 +23,7 @@ func TestDatablocksSyncer(t *testing.T) {
 			Consent: "BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw",
 		},
 		CCPA: ccpa.Policy{
-			Value: "1NYN",
+			Consent: "1NYN",
 		},
 	})
 
diff --git a/adapters/emx_digital/usersync_test.go b/adapters/emx_digital/usersync_test.go
index 0e76936c..59d66d87 100644
--- a/adapters/emx_digital/usersync_test.go
+++ b/adapters/emx_digital/usersync_test.go
@@ -23,7 +23,7 @@ func TestEMXDigitalSyncer(t *testing.T) {
 			Consent: "BOPVK28OVJoTBABABAENBs-AAAAhuAKAANAAoACwAGgAPAAxAB0AHgAQAAiABOADkA",
 		},
 		CCPA: ccpa.Policy{
-			Value: "1NYN",
+			Consent: "1NYN",
 		},
 	})
 
diff --git a/adapters/engagebdr/usersync_test.go b/adapters/engagebdr/usersync_test.go
index 45e1e41e..3a6c179a 100644
--- a/adapters/engagebdr/usersync_test.go
+++ b/adapters/engagebdr/usersync_test.go
@@ -23,7 +23,7 @@ func TestEngageBDRSyncer(t *testing.T) {
 			Consent: "BOPVK28OVJoTBABABAENBs-AAAAhuAKAANAAoACwAGgAPAAxAB0AHgAQAAiABOADkA",
 		},
 		CCPA: ccpa.Policy{
-			Value: "1NYN",
+			Consent: "1NYN",
 		},
 	})
 
diff --git a/adapters/gamoshi/usersync_test.go b/adapters/gamoshi/usersync_test.go
index b8e3e327..43dc88a4 100644
--- a/adapters/gamoshi/usersync_test.go
+++ b/adapters/gamoshi/usersync_test.go
@@ -18,7 +18,7 @@ func TestGamoshiSyncer(t *testing.T) {
 	syncer := NewGamoshiSyncer(syncURLTemplate)
 	syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{
 		CCPA: ccpa.Policy{
-			Value: "anyValue",
+			Consent: "anyValue",
 		},
 	})
 
diff --git a/adapters/gumgum/usersync_test.go b/adapters/gumgum/usersync_test.go
index 3606f6ae..9c6dc420 100644
--- a/adapters/gumgum/usersync_test.go
+++ b/adapters/gumgum/usersync_test.go
@@ -23,7 +23,7 @@ func TestGumGumSyncer(t *testing.T) {
 			Consent: "BOPVK28OVJoTBABABAENBs-AAAAhuAKAANAAoACwAGgAPAAxAB0AHgAQAAiABOADkA",
 		},
 		CCPA: ccpa.Policy{
-			Value: "1NYN",
+			Consent: "1NYN",
 		},
 	})
 
diff --git a/adapters/improvedigital/usersync_test.go b/adapters/improvedigital/usersync_test.go
index c928ebf1..35ea89cf 100644
--- a/adapters/improvedigital/usersync_test.go
+++ b/adapters/improvedigital/usersync_test.go
@@ -23,7 +23,7 @@ func TestImprovedigitalSyncer(t *testing.T) {
 			Consent: "B",
 		},
 		CCPA: ccpa.Policy{
-			Value: "C",
+			Consent: "C",
 		},
 	})
 
diff --git a/adapters/marsmedia/usersync_test.go b/adapters/marsmedia/usersync_test.go
index 67276a35..f019c014 100644
--- a/adapters/marsmedia/usersync_test.go
+++ b/adapters/marsmedia/usersync_test.go
@@ -23,7 +23,7 @@ func TestMarsmediaSyncer(t *testing.T) {
 			Consent: "B",
 		},
 		CCPA: ccpa.Policy{
-			Value: "C",
+			Consent: "C",
 		},
 	})
 
diff --git a/adapters/nanointeractive/usersync_test.go b/adapters/nanointeractive/usersync_test.go
index ec9787bc..fa786649 100644
--- a/adapters/nanointeractive/usersync_test.go
+++ b/adapters/nanointeractive/usersync_test.go
@@ -1,11 +1,12 @@
 package nanointeractive
 
 import (
-	"github.com/prebid/prebid-server/privacy/ccpa"
-	"github.com/prebid/prebid-server/privacy/gdpr"
 	"testing"
 	"text/template"
 
+	"github.com/prebid/prebid-server/privacy/ccpa"
+	"github.com/prebid/prebid-server/privacy/gdpr"
+
 	"github.com/prebid/prebid-server/privacy"
 	"github.com/stretchr/testify/assert"
 )
@@ -17,16 +18,15 @@ func TestNewNanoInteractiveSyncer(t *testing.T) {
 	)
 
 	userSync := NewNanoInteractiveSyncer(syncURLTemplate)
-	syncInfo, err := userSync.GetUsersyncInfo(
-		privacy.Policies{
-			GDPR: gdpr.Policy{
-				Signal:  "1",
-				Consent: "BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw",
-			},
-			CCPA: ccpa.Policy{
-				Value: "1NYN",
-			},
-		})
+	syncInfo, err := userSync.GetUsersyncInfo(privacy.Policies{
+		GDPR: gdpr.Policy{
+			Signal:  "1",
+			Consent: "BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw",
+		},
+		CCPA: ccpa.Policy{
+			Consent: "1NYN",
+		},
+	})
 
 	assert.NoError(t, err)
 	assert.Equal(t, "https://ad.audiencemanager.de/hbs/cookie_sync?gdpr=1&consent=BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw&us_privacy=1NYN&redirectUri=http%3A%2F%2Flocalhost%2Fsetuid%3Fbidder%3Dnanointeractive%26gdpr%3D1%26gdpr_consent%3DBONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw%26uid%3D%24UID", syncInfo.URL)
diff --git a/adapters/pubmatic/usersync_test.go b/adapters/pubmatic/usersync_test.go
index dd4a086c..d6cd9f78 100644
--- a/adapters/pubmatic/usersync_test.go
+++ b/adapters/pubmatic/usersync_test.go
@@ -23,7 +23,7 @@ func TestPubmaticSyncer(t *testing.T) {
 			Consent: "B",
 		},
 		CCPA: ccpa.Policy{
-			Value: "C",
+			Consent: "C",
 		},
 	})
 
diff --git a/adapters/rhythmone/usersync_test.go b/adapters/rhythmone/usersync_test.go
index cee6e9b0..85ecba2a 100644
--- a/adapters/rhythmone/usersync_test.go
+++ b/adapters/rhythmone/usersync_test.go
@@ -23,7 +23,7 @@ func TestRhythmoneSyncer(t *testing.T) {
 			Consent: "BOPVK28OVJoTBABABAENBs-AAAAhuAKAANAAoACwAGgAPAAxAB0AHgAQAAiABOADkA",
 		},
 		CCPA: ccpa.Policy{
-			Value: "1NYN",
+			Consent: "1NYN",
 		},
 	})
 
diff --git a/adapters/smartadserver/usersync_test.go b/adapters/smartadserver/usersync_test.go
index e279b49e..c4e66606 100644
--- a/adapters/smartadserver/usersync_test.go
+++ b/adapters/smartadserver/usersync_test.go
@@ -23,7 +23,7 @@ func TestSmartadserverSyncer(t *testing.T) {
 			Consent: "COyASAoOyASAoAfAAAENAfCAAAAAAAAAAAAAAAAAAAAA",
 		},
 		CCPA: ccpa.Policy{
-			Value: "1YNN",
+			Consent: "1YNN",
 		},
 	})
 
diff --git a/adapters/syncer_test.go b/adapters/syncer_test.go
index 9be52309..ca33a9a1 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{
-			Value: "C",
+			Consent: "C",
 		},
 	}
 
diff --git a/adapters/unruly/usersync_test.go b/adapters/unruly/usersync_test.go
index bdab254f..2f0e07d8 100644
--- a/adapters/unruly/usersync_test.go
+++ b/adapters/unruly/usersync_test.go
@@ -23,7 +23,7 @@ func TestUnrulySyncer(t *testing.T) {
 			Consent: "B",
 		},
 		CCPA: ccpa.Policy{
-			Value: "C",
+			Consent: "C",
 		},
 	})
 
diff --git a/adapters/valueimpression/usersync_test.go b/adapters/valueimpression/usersync_test.go
index 63f12305..ffb3f372 100644
--- a/adapters/valueimpression/usersync_test.go
+++ b/adapters/valueimpression/usersync_test.go
@@ -23,7 +23,7 @@ func TestValueImpressionSyncer(t *testing.T) {
 			Consent: "BOPVK28OVJoTBABABAENBs-AAAAhuAKAANAAoACwAGgAPAAxAB0AHgAQAAiABOADkA",
 		},
 		CCPA: ccpa.Policy{
-			Value: "1NYN",
+			Consent: "1NYN",
 		},
 	})
 
diff --git a/adapters/visx/usersync_test.go b/adapters/visx/usersync_test.go
index a77136c9..b410cda6 100644
--- a/adapters/visx/usersync_test.go
+++ b/adapters/visx/usersync_test.go
@@ -23,7 +23,7 @@ func TestVisxSyncer(t *testing.T) {
 			Consent: "B",
 		},
 		CCPA: ccpa.Policy{
-			Value: "C",
+			Consent: "C",
 		},
 	})
 
diff --git a/adapters/zeroclickfraud/usersync_test.go b/adapters/zeroclickfraud/usersync_test.go
index 30ade771..5e8f8fdf 100644
--- a/adapters/zeroclickfraud/usersync_test.go
+++ b/adapters/zeroclickfraud/usersync_test.go
@@ -23,7 +23,7 @@ func TestZeroClickFraudSyncer(t *testing.T) {
 			Consent: "BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw",
 		},
 		CCPA: ccpa.Policy{
-			Value: "1NYN",
+			Consent: "1NYN",
 		},
 	})
 
diff --git a/endpoints/auction_test.go b/endpoints/auction_test.go
index 028f1196..1e41b02a 100644
--- a/endpoints/auction_test.go
+++ b/endpoints/auction_test.go
@@ -387,11 +387,12 @@ func TestShouldUsersync(t *testing.T) {
 			},
 			metricsEngine: nil,
 		}
-		privacyPolicy := gdprPolicy.Policy{
+		gdprPrivacyPolicy := gdprPolicy.Policy{
 			Signal:  gdprApplies,
 			Consent: consent,
 		}
-		allowSyncs := deps.shouldUsersync(context.Background(), openrtb_ext.BidderAdform, privacyPolicy)
+
+		allowSyncs := deps.shouldUsersync(context.Background(), openrtb_ext.BidderAdform, gdprPrivacyPolicy)
 		if allowSyncs != expectAllow {
 			t.Errorf("Expected syncs: %t, allowed syncs: %t", expectAllow, allowSyncs)
 		}
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index 925cffce..58913bb5 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -1339,6 +1339,54 @@ func TestCCPAInvalid(t *testing.T) {
 	assert.Empty(t, req.Regs.Ext, "Invalid Consent Removed From Request")
 }
 
+func TestNoSaleInvalid(t *testing.T) {
+	deps := &endpointDeps{
+		&nobidExchange{},
+		newParamsValidator(t),
+		&mockStoredReqFetcher{},
+		empty_fetcher.EmptyFetcher{},
+		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,
+		nil,
+		nil,
+		hardcodedResponseIPValidator{response: true},
+	}
+
+	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":"1NYN"}`),
+		},
+		Ext: json.RawMessage(`{"prebid":{"nosale":["*", "appnexus"]}}`),
+	}
+
+	errL := deps.validateRequest(&req)
+
+	expectedError := errors.New("request.ext.prebid.nosale is invalid: can only specify all bidders if no other bidders are provided")
+	assert.ElementsMatch(t, errL, []error{expectedError})
+}
+
 func TestValidateSourceTID(t *testing.T) {
 	cfg := &config.Configuration{
 		AutoGenSourceTID: true,
diff --git a/exchange/exchangetest/ccpa-nosale-any-bidder.json b/exchange/exchangetest/ccpa-nosale-any-bidder.json
new file mode 100644
index 00000000..f7abd91f
--- /dev/null
+++ b/exchange/exchangetest/ccpa-nosale-any-bidder.json
@@ -0,0 +1,75 @@
+{
+    "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-"
+                }
+            },
+            "ext": {
+                "prebid": {
+                    "nosale": ["*"]
+                }
+            },
+            "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-"
+                        }
+                    },
+                    "ext": {
+                        "prebid": {
+                            "nosale": ["*"]
+                        }
+                    },
+                    "user": {
+                        "buyeruid": "some-buyer-id"
+                    }
+                },
+                "bidAdjustment": 1.0
+            },
+            "mockResponse": {
+                "errors": ["appnexus-error"]
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/exchange/exchangetest/ccpa-nosale-specific-bidder.json b/exchange/exchangetest/ccpa-nosale-specific-bidder.json
new file mode 100644
index 00000000..b89e29ae
--- /dev/null
+++ b/exchange/exchangetest/ccpa-nosale-specific-bidder.json
@@ -0,0 +1,75 @@
+{
+    "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-"
+                }
+            },
+            "ext": {
+                "prebid": {
+                    "nosale": ["appnexus"]
+                }
+            },
+            "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-"
+                        }
+                    },
+                    "ext": {
+                        "prebid": {
+                            "nosale": ["appnexus"]
+                        }
+                    },
+                    "user": {
+                        "buyeruid": "some-buyer-id"
+                    }
+                },
+                "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 528e875a..0dd6c031 100644
--- a/exchange/utils_test.go
+++ b/exchange/utils_test.go
@@ -3,11 +3,13 @@ package exchange
 import (
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"testing"
 
 	"github.com/mxmCherry/openrtb"
 	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/pbsmetrics"
 	"github.com/stretchr/testify/assert"
@@ -93,6 +95,7 @@ func TestCleanOpenRTBRequests(t *testing.T) {
 func TestCleanOpenRTBRequestsCCPA(t *testing.T) {
 	testCases := []struct {
 		description         string
+		reqExt              json.RawMessage
 		ccpaConsent         string
 		enforceCCPA         bool
 		expectDataScrub     bool
@@ -118,13 +121,46 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) {
 				CCPAEnforced: false,
 			},
 		},
+		{
+			description:     "Feature Flag Enabled - No Sale Star - Doesn't Scrub",
+			reqExt:          json.RawMessage(`{"prebid":{"nosale":["*"]}}`),
+			ccpaConsent:     "1-Y-",
+			enforceCCPA:     true,
+			expectDataScrub: false,
+			expectPrivacyLabels: pbsmetrics.PrivacyLabels{
+				CCPAProvided: true,
+				CCPAEnforced: false,
+			},
+		},
+		{
+			description:     "Feature Flag Enabled - No Sale Specific Bidder - Doesn't Scrub",
+			reqExt:          json.RawMessage(`{"prebid":{"nosale":["appnexus"]}}`),
+			ccpaConsent:     "1-Y-",
+			enforceCCPA:     true,
+			expectDataScrub: false,
+			expectPrivacyLabels: pbsmetrics.PrivacyLabels{
+				CCPAProvided: true,
+				CCPAEnforced: true,
+			},
+		},
+		{
+			description:     "Feature Flag Enabled - No Sale Different Bidder - Scrubs",
+			reqExt:          json.RawMessage(`{"prebid":{"nosale":["rubicon"]}}`),
+			ccpaConsent:     "1-Y-",
+			enforceCCPA:     true,
+			expectDataScrub: true,
+			expectPrivacyLabels: pbsmetrics.PrivacyLabels{
+				CCPAProvided: true,
+				CCPAEnforced: true,
+			},
+		},
 		{
 			description:     "Feature Flag Disabled",
 			ccpaConsent:     "1-Y-",
 			enforceCCPA:     false,
 			expectDataScrub: false,
 			expectPrivacyLabels: pbsmetrics.PrivacyLabels{
-				CCPAProvided: false,
+				CCPAProvided: true,
 				CCPAEnforced: false,
 			},
 		},
@@ -132,6 +168,7 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) {
 
 	for _, test := range testCases {
 		req := newBidRequest(t)
+		req.Ext = test.reqExt
 		req.Regs = &openrtb.Regs{
 			Ext: json.RawMessage(`{"us_privacy":"` + test.ccpaConsent + `"}`),
 		}
@@ -157,6 +194,47 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) {
 	}
 }
 
+func TestCleanOpenRTBRequestsCCPAErrors(t *testing.T) {
+	testCases := []struct {
+		description string
+		reqExt      json.RawMessage
+		reqRegsExt  json.RawMessage
+		expectError error
+	}{
+		{
+			description: "Invalid Consent",
+			reqExt:      json.RawMessage(`{"prebid":{"nosale":["*"]}}`),
+			reqRegsExt:  json.RawMessage(`{"us_privacy":"malformed"}`),
+			expectError: &errortypes.InvalidPrivacyConsent{"request.regs.ext.us_privacy must contain 4 characters"},
+		},
+		{
+			description: "Invalid No Sale Bidders",
+			reqExt:      json.RawMessage(`{"prebid":{"nosale":["*", "another"]}}`),
+			reqRegsExt:  json.RawMessage(`{"us_privacy":"1NYN"}`),
+			expectError: errors.New("request.ext.prebid.nosale is invalid: can only specify all bidders if no other bidders are provided"),
+		},
+	}
+
+	for _, test := range testCases {
+		req := newBidRequest(t)
+		req.Ext = test.reqExt
+		req.Regs = &openrtb.Regs{Ext: test.reqRegsExt}
+
+		var reqExtStruct openrtb_ext.ExtRequest
+		err := json.Unmarshal(req.Ext, &reqExtStruct)
+		assert.NoError(t, err, test.description+":marshal_ext")
+
+		privacyConfig := config.Privacy{
+			CCPA: config.CCPA{
+				Enforce: true,
+			},
+		}
+		_, _, _, errs := cleanOpenRTBRequests(context.Background(), req, &reqExtStruct, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{personalInfoAllowed: true}, true, privacyConfig)
+
+		assert.ElementsMatch(t, []error{test.expectError}, errs, test.description)
+	}
+}
+
 func TestCleanOpenRTBRequestsCOPPA(t *testing.T) {
 	testCases := []struct {
 		description         string
diff --git a/privacy/ccpa/consentwriter_test.go b/privacy/ccpa/consentwriter_test.go
new file mode 100644
index 00000000..57a7f8f4
--- /dev/null
+++ b/privacy/ccpa/consentwriter_test.go
@@ -0,0 +1,51 @@
+package ccpa
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/mxmCherry/openrtb"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestConsentWriter(t *testing.T) {
+	consent := "anyConsent"
+	testCases := []struct {
+		description   string
+		request       *openrtb.BidRequest
+		expected      *openrtb.BidRequest
+		expectedError bool
+	}{
+		{
+			description: "Nil Request",
+			request:     nil,
+			expected:    nil,
+		},
+		{
+			description: "Success",
+			request:     &openrtb.BidRequest{},
+			expected: &openrtb.BidRequest{
+				Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)},
+			},
+		},
+		{
+			description: "Error With Regs.Ext - Does Not Mutate",
+			request: &openrtb.BidRequest{
+				Regs: &openrtb.Regs{Ext: json.RawMessage(`malformed}`)},
+			},
+			expectedError: true,
+			expected: &openrtb.BidRequest{
+				Regs: &openrtb.Regs{Ext: json.RawMessage(`malformed}`)},
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		writer := ConsentWriter{consent}
+
+		err := writer.Write(test.request)
+
+		assertError(t, test.expectedError, err, test.description)
+		assert.Equal(t, test.expected, test.request, test.description)
+	}
+}
diff --git a/privacy/ccpa/parsedpolicy_test.go b/privacy/ccpa/parsedpolicy_test.go
new file mode 100644
index 00000000..2f7e8bfd
--- /dev/null
+++ b/privacy/ccpa/parsedpolicy_test.go
@@ -0,0 +1,391 @@
+package ccpa
+
+import (
+	"testing"
+
+	"github.com/mxmCherry/openrtb"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+)
+
+func TestValidateConsent(t *testing.T) {
+	testCases := []struct {
+		description string
+		consent     string
+		expected    bool
+	}{
+		{
+			description: "Empty String",
+			consent:     "",
+			expected:    true,
+		},
+		{
+			description: "Valid Consent With Opt Out",
+			consent:     "1NYN",
+			expected:    true,
+		},
+		{
+			description: "Valid Consent Without Opt Out",
+			consent:     "1NNN",
+			expected:    true,
+		},
+		{
+			description: "Invalid",
+			consent:     "malformed",
+			expected:    false,
+		},
+	}
+
+	for _, test := range testCases {
+		result := ValidateConsent(test.consent)
+		assert.Equal(t, test.expected, result, test.description)
+	}
+}
+
+func TestParse(t *testing.T) {
+	validBidders := map[string]struct{}{"a": {}}
+
+	testCases := []struct {
+		description    string
+		consent        string
+		noSaleBidders  []string
+		expectedPolicy ParsedPolicy
+		expectedError  string
+	}{
+		{
+			description:    "Consent Error",
+			consent:        "malformed",
+			noSaleBidders:  []string{},
+			expectedPolicy: ParsedPolicy{},
+			expectedError:  "request.regs.ext.us_privacy must contain 4 characters",
+		},
+		{
+			description:    "No Sale Error",
+			consent:        "1NYN",
+			noSaleBidders:  []string{"b"},
+			expectedPolicy: ParsedPolicy{},
+			expectedError:  "request.ext.prebid.nosale is invalid: unrecognized bidder 'b'",
+		},
+		{
+			description:   "Success",
+			consent:       "1NYN",
+			noSaleBidders: []string{"a"},
+			expectedPolicy: ParsedPolicy{
+				consentSpecified:      true,
+				consentOptOutSale:     true,
+				noSaleForAllBidders:   false,
+				noSaleSpecificBidders: map[string]struct{}{"a": {}},
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		policy := Policy{test.consent, test.noSaleBidders}
+
+		result, err := policy.Parse(validBidders)
+
+		if test.expectedError == "" {
+			assert.NoError(t, err, test.description)
+		} else {
+			assert.EqualError(t, err, test.expectedError, test.description)
+		}
+
+		assert.Equal(t, test.expectedPolicy, result, test.description)
+	}
+}
+
+func TestParseConsent(t *testing.T) {
+	testCases := []struct {
+		description    string
+		consent        string
+		expectedResult bool
+		expectedError  string
+	}{
+		{
+			description:    "Valid",
+			consent:        "1NYN",
+			expectedResult: true,
+		},
+		{
+			description:    "Valid - Not Sale",
+			consent:        "1NNN",
+			expectedResult: false,
+		},
+		{
+			description:    "Valid - Not Applicable",
+			consent:        "1---",
+			expectedResult: false,
+		},
+		{
+			description:    "Valid - Empty",
+			consent:        "",
+			expectedResult: false,
+		},
+		{
+			description:    "Wrong Length",
+			consent:        "1NY",
+			expectedResult: false,
+			expectedError:  "must contain 4 characters",
+		},
+		{
+			description:    "Wrong Version",
+			consent:        "2---",
+			expectedResult: false,
+			expectedError:  "must specify version 1",
+		},
+		{
+			description:    "Explicit Notice Char",
+			consent:        "1X--",
+			expectedResult: false,
+			expectedError:  "must specify 'N', 'Y', or '-' for the explicit notice",
+		},
+		{
+			description:    "Invalid Explicit Notice Case",
+			consent:        "1y--",
+			expectedResult: false,
+			expectedError:  "must specify 'N', 'Y', or '-' for the explicit notice",
+		},
+		{
+			description:    "Invalid Opt-Out Sale Char",
+			consent:        "1-X-",
+			expectedResult: false,
+			expectedError:  "must specify 'N', 'Y', or '-' for the opt-out sale",
+		},
+		{
+			description:    "Invalid Opt-Out Sale Case",
+			consent:        "1-y-",
+			expectedResult: false,
+			expectedError:  "must specify 'N', 'Y', or '-' for the opt-out sale",
+		},
+		{
+			description:    "Invalid LSPA Char",
+			consent:        "1--X",
+			expectedResult: false,
+			expectedError:  "must specify 'N', 'Y', or '-' for the limited service provider agreement",
+		},
+		{
+			description:    "Invalid LSPA Case",
+			consent:        "1--y",
+			expectedResult: false,
+			expectedError:  "must specify 'N', 'Y', or '-' for the limited service provider agreement",
+		},
+	}
+
+	for _, test := range testCases {
+		result, err := parseConsent(test.consent)
+
+		if test.expectedError == "" {
+			assert.NoError(t, err, test.description)
+		} else {
+			assert.EqualError(t, err, test.expectedError, test.description)
+		}
+
+		assert.Equal(t, test.expectedResult, result, test.description)
+	}
+}
+
+func TestParseNoSaleBidders(t *testing.T) {
+	testCases := []struct {
+		description                   string
+		noSaleBidders                 []string
+		validBidders                  []string
+		expectedNoSaleForAllBidders   bool
+		expectedNoSaleSpecificBidders map[string]struct{}
+		expectedError                 string
+	}{
+		{
+			description:                   "Valid - No Bidders",
+			noSaleBidders:                 []string{},
+			validBidders:                  []string{"a"},
+			expectedNoSaleForAllBidders:   false,
+			expectedNoSaleSpecificBidders: map[string]struct{}{},
+		},
+		{
+			description:                   "Valid - 1 Bidder",
+			noSaleBidders:                 []string{"a"},
+			validBidders:                  []string{"a"},
+			expectedNoSaleForAllBidders:   false,
+			expectedNoSaleSpecificBidders: map[string]struct{}{"a": {}},
+		},
+		{
+			description:                   "Valid - 1+ Bidders",
+			noSaleBidders:                 []string{"a", "b"},
+			validBidders:                  []string{"a", "b"},
+			expectedNoSaleForAllBidders:   false,
+			expectedNoSaleSpecificBidders: map[string]struct{}{"a": {}, "b": {}},
+		},
+		{
+			description:                   "Valid - All Bidders",
+			noSaleBidders:                 []string{"*"},
+			validBidders:                  []string{"a"},
+			expectedNoSaleForAllBidders:   true,
+			expectedNoSaleSpecificBidders: map[string]struct{}{},
+		},
+		{
+			description:                   "Bidder Not Valid",
+			noSaleBidders:                 []string{"b"},
+			validBidders:                  []string{"a"},
+			expectedError:                 "unrecognized bidder 'b'",
+			expectedNoSaleForAllBidders:   false,
+			expectedNoSaleSpecificBidders: map[string]struct{}{},
+		},
+		{
+			description:                   "All Bidder Mixed With Other Bidders Is Invalid",
+			noSaleBidders:                 []string{"*", "a"},
+			validBidders:                  []string{"a"},
+			expectedError:                 "can only specify all bidders if no other bidders are provided",
+			expectedNoSaleForAllBidders:   false,
+			expectedNoSaleSpecificBidders: map[string]struct{}{},
+		},
+		{
+			description:                   "Valid Bidders Case Sensitive",
+			noSaleBidders:                 []string{"a"},
+			validBidders:                  []string{"A"},
+			expectedError:                 "unrecognized bidder 'a'",
+			expectedNoSaleForAllBidders:   false,
+			expectedNoSaleSpecificBidders: map[string]struct{}{},
+		},
+	}
+
+	for _, test := range testCases {
+		validBiddersMap := make(map[string]struct{})
+		for _, v := range test.validBidders {
+			validBiddersMap[v] = struct{}{}
+		}
+
+		resultNoSaleForAllBidders, resultNoSaleSpecificBidders, err := parseNoSaleBidders(test.noSaleBidders, validBiddersMap)
+
+		if test.expectedError == "" {
+			assert.NoError(t, err, test.description+":err")
+		} else {
+			assert.EqualError(t, err, test.expectedError, test.description+":err")
+		}
+
+		assert.Equal(t, test.expectedNoSaleForAllBidders, resultNoSaleForAllBidders, test.description+":allBidders")
+		assert.Equal(t, test.expectedNoSaleSpecificBidders, resultNoSaleSpecificBidders, test.description+":specificBidders")
+	}
+}
+
+func TestCanEnforce(t *testing.T) {
+	testCases := []struct {
+		description string
+		policy      ParsedPolicy
+		expected    bool
+	}{
+		{
+			description: "Specified",
+			policy: ParsedPolicy{
+				consentSpecified:      true,
+				consentOptOutSale:     false,
+				noSaleForAllBidders:   false,
+				noSaleSpecificBidders: map[string]struct{}{},
+			},
+			expected: true,
+		},
+		{
+			description: "Not Specified",
+			policy: ParsedPolicy{
+				consentSpecified:      false,
+				consentOptOutSale:     false,
+				noSaleForAllBidders:   false,
+				noSaleSpecificBidders: map[string]struct{}{},
+			},
+			expected: false,
+		},
+	}
+
+	for _, test := range testCases {
+		result := test.policy.CanEnforce()
+		assert.Equal(t, test.expected, result, test.description)
+	}
+}
+
+func TestShouldEnforce(t *testing.T) {
+	testCases := []struct {
+		description string
+		policy      ParsedPolicy
+		bidder      string
+		expected    bool
+	}{
+		{
+			description: "Not Enforced - All Bidders No Sale",
+			policy: ParsedPolicy{
+				consentSpecified:      true,
+				consentOptOutSale:     true,
+				noSaleForAllBidders:   true,
+				noSaleSpecificBidders: map[string]struct{}{},
+			},
+			bidder:   "a",
+			expected: false,
+		},
+		{
+			description: "Not Enforced - Specific Bidders No Sale",
+			policy: ParsedPolicy{
+				consentSpecified:      true,
+				consentOptOutSale:     true,
+				noSaleForAllBidders:   false,
+				noSaleSpecificBidders: map[string]struct{}{"a": {}},
+			},
+			bidder:   "a",
+			expected: false,
+		},
+		{
+			description: "Not Enforced - No Bidder No Sale",
+			policy: ParsedPolicy{
+				consentSpecified:      true,
+				consentOptOutSale:     false,
+				noSaleForAllBidders:   false,
+				noSaleSpecificBidders: map[string]struct{}{},
+			},
+			bidder:   "a",
+			expected: false,
+		},
+		{
+			description: "Not Enforced - No Sale Case Sensitive",
+			policy: ParsedPolicy{
+				consentSpecified:      true,
+				consentOptOutSale:     false,
+				noSaleForAllBidders:   false,
+				noSaleSpecificBidders: map[string]struct{}{"A": {}},
+			},
+			bidder:   "a",
+			expected: false,
+		},
+		{
+			description: "Enforced - No Bidder No Sale",
+			policy: ParsedPolicy{
+				consentSpecified:      true,
+				consentOptOutSale:     true,
+				noSaleForAllBidders:   false,
+				noSaleSpecificBidders: map[string]struct{}{},
+			},
+			bidder:   "a",
+			expected: true,
+		},
+		{
+			description: "Enforced - No Sale Case Sensitive",
+			policy: ParsedPolicy{
+				consentSpecified:      true,
+				consentOptOutSale:     true,
+				noSaleForAllBidders:   false,
+				noSaleSpecificBidders: map[string]struct{}{"A": {}},
+			},
+			bidder:   "a",
+			expected: true,
+		},
+	}
+
+	for _, test := range testCases {
+		result := test.policy.ShouldEnforce(test.bidder)
+		assert.Equal(t, test.expected, result, test.description)
+	}
+}
+
+type mockPolicWriter struct {
+	mock.Mock
+}
+
+func (m *mockPolicWriter) Write(req *openrtb.BidRequest) error {
+	args := m.Called(req)
+	return args.Error(0)
+}
diff --git a/privacy/ccpa/policy_test.go b/privacy/ccpa/policy_test.go
index e9b4c452..7ff896e9 100644
--- a/privacy/ccpa/policy_test.go
+++ b/privacy/ccpa/policy_test.go
@@ -8,7 +8,7 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func TestRead(t *testing.T) {
+func TestReadFromRequest(t *testing.T) {
 	testCases := []struct {
 		description    string
 		request        *openrtb.BidRequest
@@ -18,83 +18,146 @@ func TestRead(t *testing.T) {
 		{
 			description: "Success",
 			request: &openrtb.BidRequest{
-				Regs: &openrtb.Regs{
-					Ext: json.RawMessage(`{"us_privacy":"ABC"}`),
-				},
+				Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)},
+				Ext:  json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`),
 			},
 			expectedPolicy: Policy{
-				Value: "ABC",
+				Consent:       "ABC",
+				NoSaleBidders: []string{"a", "b"},
 			},
 		},
 		{
-			description: "Empty - No Request",
+			description: "Nil Request",
 			request:     nil,
 			expectedPolicy: Policy{
-				Value: "",
+				Consent:       "",
+				NoSaleBidders: nil,
 			},
 		},
 		{
-			description: "Empty - No Regs",
+			description: "Nil Regs",
 			request: &openrtb.BidRequest{
 				Regs: nil,
+				Ext:  json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`),
 			},
 			expectedPolicy: Policy{
-				Value: "",
+				Consent:       "",
+				NoSaleBidders: []string{"a", "b"},
 			},
 		},
 		{
-			description: "Empty - No Ext",
+			description: "Nil Regs.Ext",
 			request: &openrtb.BidRequest{
 				Regs: &openrtb.Regs{},
+				Ext:  json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`),
 			},
 			expectedPolicy: Policy{
-				Value: "",
+				Consent:       "",
+				NoSaleBidders: []string{"a", "b"},
 			},
 		},
 		{
-			description: "Empty - No Value",
+			description: "Empty Regs.Ext",
 			request: &openrtb.BidRequest{
-				Regs: &openrtb.Regs{
-					Ext: json.RawMessage(`{"anythingElse":"42"}`),
-				},
+				Regs: &openrtb.Regs{Ext: json.RawMessage(`{}`)},
+				Ext:  json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`),
 			},
 			expectedPolicy: Policy{
-				Value: "",
+				Consent:       "",
+				NoSaleBidders: []string{"a", "b"},
 			},
 		},
 		{
-			description: "Serialization Issue",
+			description: "Missing Regs.Ext USPrivacy Value",
 			request: &openrtb.BidRequest{
-				Regs: &openrtb.Regs{
-					Ext: json.RawMessage(`malformed`),
-				},
+				Regs: &openrtb.Regs{Ext: json.RawMessage(`{"anythingElse":"42"}`)},
+				Ext:  json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`),
+			},
+			expectedPolicy: Policy{
+				Consent:       "",
+				NoSaleBidders: []string{"a", "b"},
+			},
+		},
+		{
+			description: "Malformed Regs.Ext",
+			request: &openrtb.BidRequest{
+				Regs: &openrtb.Regs{Ext: json.RawMessage(`malformed`)},
+				Ext:  json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`),
+			},
+			expectedError: true,
+		},
+		{
+			description: "Invalid Regs.Ext Type",
+			request: &openrtb.BidRequest{
+				Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":123`)},
+				Ext:  json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`),
+			},
+			expectedError: true,
+		},
+		{
+			description: "Nil Ext",
+			request: &openrtb.BidRequest{
+				Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)},
+				Ext:  nil,
+			},
+			expectedPolicy: Policy{
+				Consent:       "ABC",
+				NoSaleBidders: nil,
+			},
+		},
+		{
+			description: "Empty Ext",
+			request: &openrtb.BidRequest{
+				Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)},
+				Ext:  json.RawMessage(`{}`),
+			},
+			expectedPolicy: Policy{
+				Consent:       "ABC",
+				NoSaleBidders: nil,
+			},
+		},
+		{
+			description: "Missing Ext.Prebid No Sale Value",
+			request: &openrtb.BidRequest{
+				Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)},
+				Ext:  json.RawMessage(`{"anythingElse":"42"}`),
+			},
+			expectedPolicy: Policy{
+				Consent:       "ABC",
+				NoSaleBidders: nil,
+			},
+		},
+		{
+			description: "Malformed Ext",
+			request: &openrtb.BidRequest{
+				Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)},
+				Ext:  json.RawMessage(`malformed`),
+			},
+			expectedError: true,
+		},
+		{
+			description: "Invalid Ext.Prebid.NoSale Type",
+			request: &openrtb.BidRequest{
+				Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)},
+				Ext:  json.RawMessage(`{"prebid":{"nosale":"wrongtype"}}`),
 			},
 			expectedError: true,
 		},
 		{
 			description: "Injection Attack",
 			request: &openrtb.BidRequest{
-				Regs: &openrtb.Regs{
-					Ext: json.RawMessage(`{"us_privacy":"1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}`),
-				},
+				Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}`)},
 			},
 			expectedPolicy: Policy{
-				Value: "1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\"",
+				Consent: "1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\"",
 			},
 		},
 	}
 
 	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)
+		result, err := ReadFromRequest(test.request)
+		assertError(t, test.expectedError, err, test.description)
+		assert.Equal(t, test.expectedPolicy, result, test.description)
 	}
 }
 
@@ -107,313 +170,422 @@ func TestWrite(t *testing.T) {
 		expectedError bool
 	}{
 		{
-			description: "Disabled",
-			policy:      Policy{Value: ""},
-			request:     &openrtb.BidRequest{},
-			expected:    &openrtb.BidRequest{},
-		},
-		{
-			description: "Disabled - Nil Request",
-			policy:      Policy{Value: ""},
+			description: "Nil Request",
+			policy:      Policy{Consent: "anyConsent", NoSaleBidders: []string{"a", "b"}},
 			request:     nil,
 			expected:    nil,
 		},
 		{
-			description: "Disabled - Empty Regs.Ext",
-			policy:      Policy{Value: ""},
-			request:     &openrtb.BidRequest{Regs: &openrtb.Regs{}},
-			expected:    &openrtb.BidRequest{Regs: &openrtb.Regs{}},
+			description: "Success",
+			policy:      Policy{Consent: "anyConsent", NoSaleBidders: []string{"a", "b"}},
+			request:     &openrtb.BidRequest{},
+			expected: &openrtb.BidRequest{
+				Regs: &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)},
+				Ext:  json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`),
+			},
 		},
 		{
-			description: "Disabled - Remove From Request",
-			policy:      Policy{Value: ""},
-			request: &openrtb.BidRequest{Regs: &openrtb.Regs{
-				Ext: json.RawMessage(`{"us_privacy":"toBeRemoved"}`)}},
-			expected: &openrtb.BidRequest{Regs: &openrtb.Regs{}},
+			description: "Error Regs.Ext - No Partial Update To Request",
+			policy:      Policy{Consent: "anyConsent", NoSaleBidders: []string{"a", "b"}},
+			request: &openrtb.BidRequest{
+				Regs: &openrtb.Regs{Ext: json.RawMessage(`malformed}`)},
+			},
+			expectedError: true,
+			expected: &openrtb.BidRequest{
+				Regs: &openrtb.Regs{Ext: json.RawMessage(`malformed}`)},
+			},
 		},
 		{
-			description: "Disabled - Remove From Request, Leave Other req Values",
-			policy:      Policy{Value: ""},
-			request: &openrtb.BidRequest{Regs: &openrtb.Regs{
-				COPPA: 42,
-				Ext:   json.RawMessage(`{"us_privacy":"toBeRemoved"}`)}},
-			expected: &openrtb.BidRequest{Regs: &openrtb.Regs{
-				COPPA: 42}},
+			description: "Error Ext - No Partial Update To Request",
+			policy:      Policy{Consent: "anyConsent", NoSaleBidders: []string{"a", "b"}},
+			request: &openrtb.BidRequest{
+				Ext: json.RawMessage(`malformed}`),
+			},
+			expectedError: true,
+			expected: &openrtb.BidRequest{
+				Ext: json.RawMessage(`malformed}`),
+			},
 		},
+	}
+
+	for _, test := range testCases {
+		err := test.policy.Write(test.request)
+		assertError(t, test.expectedError, err, test.description)
+		assert.Equal(t, test.expected, test.request, test.description)
+	}
+}
+
+func TestBuildRegs(t *testing.T) {
+	testCases := []struct {
+		description   string
+		consent       string
+		regs          *openrtb.Regs
+		expected      *openrtb.Regs
+		expectedError bool
+	}{
 		{
-			description: "Disabled - Remove From Request, Leave Other req.ext Values",
-			policy:      Policy{Value: ""},
-			request: &openrtb.BidRequest{Regs: &openrtb.Regs{
-				Ext: json.RawMessage(`{"existing":"any","us_privacy":"toBeRemoved"}`)}},
-			expected: &openrtb.BidRequest{Regs: &openrtb.Regs{
-				Ext: json.RawMessage(`{"existing":"any"}`)}},
+			description: "Clear",
+			consent:     "",
+			regs: &openrtb.Regs{
+				Ext: json.RawMessage(`{"us_privacy":"ABC"}`),
+			},
+			expected: &openrtb.Regs{},
 		},
 		{
-			description: "Enabled - Nil Request",
-			policy:      Policy{Value: "anyValue"},
-			request:     nil,
-			expected:    nil,
+			description: "Clear - Error",
+			consent:     "",
+			regs: &openrtb.Regs{
+				Ext: json.RawMessage(`malformed`),
+			},
+			expectedError: true,
 		},
 		{
-			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: "Write",
+			consent:     "anyConsent",
+			regs:        nil,
+			expected: &openrtb.Regs{
+				Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`),
+			},
 		},
 		{
-			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: "Write - Error",
+			consent:     "anyConsent",
+			regs: &openrtb.Regs{
+				Ext: json.RawMessage(`malformed`),
+			},
+			expectedError: true,
 		},
+	}
+
+	for _, test := range testCases {
+		result, err := buildRegs(test.consent, test.regs)
+		assertError(t, test.expectedError, err, test.description)
+		assert.Equal(t, test.expected, result, test.description)
+	}
+}
+
+func TestBuildRegsClear(t *testing.T) {
+	testCases := []struct {
+		description   string
+		regs          *openrtb.Regs
+		expected      *openrtb.Regs
+		expectedError bool
+	}{
 		{
-			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: "Nil Regs",
+			regs:        nil,
+			expected:    nil,
 		},
 		{
-			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: "Nil Regs.Ext",
+			regs:        &openrtb.Regs{Ext: nil},
+			expected:    &openrtb.Regs{Ext: nil},
 		},
 		{
-			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,
+			description: "Empty Regs.Ext",
+			regs:        &openrtb.Regs{Ext: json.RawMessage(`{}`)},
+			expected:    &openrtb.Regs{},
 		},
 		{
-			description: "Injection Attack With Nil Request Regs Object",
-			policy:      Policy{Value: "1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""},
-			request:     &openrtb.BidRequest{},
-			expected: &openrtb.BidRequest{Regs: &openrtb.Regs{
-				Ext: json.RawMessage(`{"us_privacy":"1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}`),
-			}},
+			description: "Removes Regs.Ext Entirely",
+			regs:        &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)},
+			expected:    &openrtb.Regs{},
+		},
+		{
+			description: "Leaves Other Regs.Ext Values",
+			regs:        &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC", "other":"any"}`)},
+			expected:    &openrtb.Regs{Ext: json.RawMessage(`{"other":"any"}`)},
 		},
 		{
-			description: "Injection Attack With Nil Request Regs Ext Object",
-			policy:      Policy{Value: "1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""},
-			request:     &openrtb.BidRequest{Regs: &openrtb.Regs{}},
-			expected: &openrtb.BidRequest{Regs: &openrtb.Regs{
-				Ext: json.RawMessage(`{"us_privacy":"1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}`),
-			}},
+			description: "Invalid Regs.Ext Type - Still Cleared",
+			regs:        &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":123}`)},
+			expected:    &openrtb.Regs{},
 		},
 		{
-			description: "Injection Attack With Existing Request Regs Ext Object",
-			policy:      Policy{Value: "1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""},
-			request: &openrtb.BidRequest{Regs: &openrtb.Regs{
-				Ext: json.RawMessage(`{"existing":"any"}`),
-			}},
-			expected: &openrtb.BidRequest{Regs: &openrtb.Regs{
-				Ext: json.RawMessage(`{"existing":"any","us_privacy":"1YYY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}`),
-			}},
+			description:   "Malformed Regs.Ext",
+			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)
-		}
+		result, err := buildRegsClear(test.regs)
+		assertError(t, test.expectedError, err, test.description)
+		assert.Equal(t, test.expected, result, test.description)
 	}
 }
 
-func TestValidate(t *testing.T) {
+func TestBuildRegsWrite(t *testing.T) {
 	testCases := []struct {
 		description   string
-		policy        Policy
-		expectedError string
+		consent       string
+		regs          *openrtb.Regs
+		expected      *openrtb.Regs
+		expectedError bool
 	}{
 		{
-			description:   "Valid",
-			policy:        Policy{Value: "1NYN"},
-			expectedError: "",
+			description: "Nil Regs",
+			consent:     "anyConsent",
+			regs:        nil,
+			expected:    &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)},
 		},
 		{
-			description:   "Valid - Not Applicable",
-			policy:        Policy{Value: "1---"},
-			expectedError: "",
+			description: "Nil Regs.Ext",
+			consent:     "anyConsent",
+			regs:        &openrtb.Regs{Ext: nil},
+			expected:    &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)},
 		},
 		{
-			description:   "Valid - Empty",
-			policy:        Policy{Value: ""},
-			expectedError: "",
+			description: "Empty Regs.Ext",
+			consent:     "anyConsent",
+			regs:        &openrtb.Regs{Ext: json.RawMessage(`{}`)},
+			expected:    &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)},
 		},
 		{
-			description:   "Invalid Length",
-			policy:        Policy{Value: "1NY"},
-			expectedError: "request.regs.ext.us_privacy must contain 4 characters",
+			description: "Overwrites Existing",
+			consent:     "anyConsent",
+			regs:        &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"ABC"}`)},
+			expected:    &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)},
 		},
 		{
-			description:   "Invalid Version",
-			policy:        Policy{Value: "2---"},
-			expectedError: "request.regs.ext.us_privacy must specify version 1",
+			description: "Leaves Other Ext Values",
+			consent:     "anyConsent",
+			regs:        &openrtb.Regs{Ext: json.RawMessage(`{"other":"any"}`)},
+			expected:    &openrtb.Regs{Ext: json.RawMessage(`{"other":"any","us_privacy":"anyConsent"}`)},
 		},
 		{
-			description:   "Invalid Explicit Notice Char",
-			policy:        Policy{Value: "1X--"},
-			expectedError: "request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the explicit notice",
+			description: "Invalid Regs.Ext Type - Still Overwrites",
+			consent:     "anyConsent",
+			regs:        &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":123}`)},
+			expected:    &openrtb.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)},
 		},
 		{
-			description:   "Invalid Explicit Notice Case",
-			policy:        Policy{Value: "1y--"},
-			expectedError: "request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the explicit notice",
+			description:   "Malformed Regs.Ext",
+			consent:       "anyConsent",
+			regs:          &openrtb.Regs{Ext: json.RawMessage(`malformed`)},
+			expectedError: true,
 		},
+	}
+
+	for _, test := range testCases {
+		result, err := buildRegsWrite(test.consent, test.regs)
+		assertError(t, test.expectedError, err, test.description)
+		assert.Equal(t, test.expected, result, test.description)
+	}
+}
+
+func TestBuildExt(t *testing.T) {
+	testCases := []struct {
+		description   string
+		noSaleBidders []string
+		ext           json.RawMessage
+		expected      json.RawMessage
+		expectedError bool
+	}{
 		{
-			description:   "Invalid Opt-Out Sale Char",
-			policy:        Policy{Value: "1-X-"},
-			expectedError: "request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the opt-out sale",
+			description:   "Clear - Nil",
+			noSaleBidders: nil,
+			ext:           json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`),
+			expected:      nil,
 		},
 		{
-			description:   "Invalid Opt-Out Sale Case",
-			policy:        Policy{Value: "1-y-"},
-			expectedError: "request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the opt-out sale",
+			description:   "Clear - Empty",
+			noSaleBidders: []string{},
+			ext:           json.RawMessage(`{"prebid":{"nosale":["a", "b"]}}`),
+			expected:      nil,
 		},
 		{
-			description:   "Invalid LSPA Char",
-			policy:        Policy{Value: "1--X"},
-			expectedError: "request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the limited service provider agreement",
+			description:   "Clear - Error",
+			noSaleBidders: []string{},
+			ext:           json.RawMessage(`malformed`),
+			expectedError: true,
 		},
 		{
-			description:   "Invalid LSPA Case",
-			policy:        Policy{Value: "1--y"},
-			expectedError: "request.regs.ext.us_privacy must specify 'N', 'Y', or '-' for the limited service provider agreement",
+			description:   "Write",
+			noSaleBidders: []string{"a", "b"},
+			ext:           nil,
+			expected:      json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`),
+		},
+		{
+			description:   "Write - Error",
+			noSaleBidders: []string{"a", "b"},
+			ext:           json.RawMessage(`malformed`),
+			expectedError: true,
 		},
 	}
 
 	for _, test := range testCases {
-		result := test.policy.Validate()
-
-		if test.expectedError == "" {
-			assert.NoError(t, result, test.description)
-		} else {
-			assert.EqualError(t, result, test.expectedError, test.description)
-		}
+		result, err := buildExt(test.noSaleBidders, test.ext)
+		assertError(t, test.expectedError, err, test.description)
+		assert.Equal(t, test.expected, result, test.description)
 	}
 }
 
-func TestValidateConsent(t *testing.T) {
+func TestBuildExtClear(t *testing.T) {
 	testCases := []struct {
 		description   string
-		consent       string
-		expectedError string
+		ext           json.RawMessage
+		expected      json.RawMessage
+		expectedError bool
 	}{
 		{
-			description:   "Valid",
-			consent:       "1NYN",
-			expectedError: "",
+			description: "Nil Ext",
+			ext:         nil,
+			expected:    nil,
 		},
 		{
-			description:   "Valid - Not Applicable",
-			consent:       "1---",
-			expectedError: "",
+			description: "Empty Ext",
+			ext:         json.RawMessage(``),
+			expected:    json.RawMessage(``),
 		},
 		{
-			description:   "Invalid Empty",
-			consent:       "",
-			expectedError: "",
+			description: "Empty Ext Object",
+			ext:         json.RawMessage(`{}`),
+			expected:    json.RawMessage(`{}`),
 		},
 		{
-			description:   "Invalid Length",
-			consent:       "1NY",
-			expectedError: "must contain 4 characters",
+			description: "Empty Ext.Prebid",
+			ext:         json.RawMessage(`{"prebid":{}}`),
+			expected:    nil,
 		},
 		{
-			description:   "Invalid Version",
-			consent:       "2---",
-			expectedError: "must specify version 1",
+			description: "Removes Ext Entirely",
+			ext:         json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`),
+			expected:    nil,
 		},
 		{
-			description:   "Invalid Explicit Notice Char",
-			consent:       "1X--",
-			expectedError: "must specify 'N', 'Y', or '-' for the explicit notice",
+			description: "Leaves Other Ext Values",
+			ext:         json.RawMessage(`{"other":"any","prebid":{"nosale":["a","b"]}}`),
+			expected:    json.RawMessage(`{"other":"any"}`),
 		},
 		{
-			description:   "Invalid Explicit Notice Case",
-			consent:       "1y--",
-			expectedError: "must specify 'N', 'Y', or '-' for the explicit notice",
+			description: "Leaves Other Ext.Prebid Values",
+			ext:         json.RawMessage(`{"prebid":{"nosale":["a","b"],"other":"any"}}`),
+			expected:    json.RawMessage(`{"prebid":{"other":"any"}}`),
 		},
 		{
-			description:   "Invalid Opt-Out Sale Char",
-			consent:       "1-X-",
-			expectedError: "must specify 'N', 'Y', or '-' for the opt-out sale",
+			description: "Leaves All Other Values",
+			ext:         json.RawMessage(`{"other":"ABC","prebid":{"nosale":["a","b"],"other":"123"}}`),
+			expected:    json.RawMessage(`{"other":"ABC","prebid":{"other":"123"}}`),
 		},
 		{
-			description:   "Invalid Opt-Out Sale Case",
-			consent:       "1-y-",
-			expectedError: "must specify 'N', 'Y', or '-' for the opt-out sale",
+			description:   "Malformed Ext",
+			ext:           json.RawMessage(`malformed`),
+			expectedError: true,
 		},
 		{
-			description:   "Invalid LSPA Char",
-			consent:       "1--X",
-			expectedError: "must specify 'N', 'Y', or '-' for the limited service provider agreement",
+			description:   "Malformed Ext.Prebid",
+			ext:           json.RawMessage(`{"prebid":malformed}`),
+			expectedError: true,
 		},
 		{
-			description:   "Invalid LSPA Case",
-			consent:       "1--y",
-			expectedError: "must specify 'N', 'Y', or '-' for the limited service provider agreement",
+			description:   "Invalid Ext.Prebid Type",
+			ext:           json.RawMessage(`{"prebid":123}`),
+			expectedError: true,
 		},
 	}
 
 	for _, test := range testCases {
-		result := ValidateConsent(test.consent)
-
-		if test.expectedError == "" {
-			assert.NoError(t, result, test.description)
-		} else {
-			assert.EqualError(t, result, test.expectedError, test.description)
-		}
+		result, err := buildExtClear(test.ext)
+		assertError(t, test.expectedError, err, test.description)
+		assert.Equal(t, test.expected, result, test.description)
 	}
 }
 
-func TestShouldEnforce(t *testing.T) {
+func TestBuildExtWrite(t *testing.T) {
 	testCases := []struct {
-		description string
-		policy      Policy
-		expected    bool
+		description   string
+		noSaleBidders []string
+		ext           json.RawMessage
+		expected      json.RawMessage
+		expectedError bool
 	}{
 		{
-			description: "Enforceable",
-			policy:      Policy{Value: "1-Y-"},
-			expected:    true,
+			description:   "Nil Ext",
+			noSaleBidders: []string{"a", "b"},
+			ext:           nil,
+			expected:      json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`),
+		},
+		{
+			description:   "Empty Ext",
+			noSaleBidders: []string{"a", "b"},
+			ext:           json.RawMessage(``),
+			expected:      json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`),
 		},
 		{
-			description: "Not Enforceable - Not Present",
-			policy:      Policy{Value: ""},
-			expected:    false,
+			description:   "Empty Ext Object",
+			noSaleBidders: []string{"a", "b"},
+			ext:           json.RawMessage(`{}`),
+			expected:      json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`),
 		},
 		{
-			description: "Not Enforceable - Opt-Out Unknown",
-			policy:      Policy{Value: "1---"},
-			expected:    false,
+			description:   "Empty Ext.Prebid",
+			noSaleBidders: []string{"a", "b"},
+			ext:           json.RawMessage(`{"prebid":{}}`),
+			expected:      json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`),
 		},
 		{
-			description: "Not Enforceable - Opt-Out Explicitly No",
-			policy:      Policy{Value: "1-N-"},
-			expected:    false,
+			description:   "Overwrites Existing",
+			noSaleBidders: []string{"a", "b"},
+			ext:           json.RawMessage(`{"prebid":{"nosale":["x","y"]}}`),
+			expected:      json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`),
 		},
 		{
-			description: "Invalid",
-			policy:      Policy{Value: "2---"},
-			expected:    false,
+			description:   "Leaves Other Ext Values",
+			noSaleBidders: []string{"a", "b"},
+			ext:           json.RawMessage(`{"other":"any"}`),
+			expected:      json.RawMessage(`{"other":"any","prebid":{"nosale":["a","b"]}}`),
+		},
+		{
+			description:   "Leaves Other Ext.Prebid Values",
+			noSaleBidders: []string{"a", "b"},
+			ext:           json.RawMessage(`{"prebid":{"other":"any"}}`),
+			expected:      json.RawMessage(`{"prebid":{"nosale":["a","b"],"other":"any"}}`),
+		},
+		{
+			description:   "Leaves All Other Values",
+			noSaleBidders: []string{"a", "b"},
+			ext:           json.RawMessage(`{"other":"ABC","prebid":{"other":"123"}}`),
+			expected:      json.RawMessage(`{"other":"ABC","prebid":{"nosale":["a","b"],"other":"123"}}`),
+		},
+		{
+			description:   "Invalid Ext.Prebid No Sale Type - Still Overrides",
+			noSaleBidders: []string{"a", "b"},
+			ext:           json.RawMessage(`{"prebid":{"nosale":123}}`),
+			expected:      json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`),
+		},
+		{
+			description:   "Invalid Ext.Prebid Type ",
+			noSaleBidders: []string{"a", "b"},
+			ext:           json.RawMessage(`{"prebid":"wrongtype"}`),
+			expectedError: true,
+		},
+		{
+			description:   "Malformed Ext",
+			noSaleBidders: []string{"a", "b"},
+			ext:           json.RawMessage(`{malformed`),
+			expectedError: true,
+		},
+		{
+			description:   "Malformed Ext.Prebid",
+			noSaleBidders: []string{"a", "b"},
+			ext:           json.RawMessage(`{"prebid":malformed}`),
+			expectedError: true,
 		},
 	}
 
 	for _, test := range testCases {
-		result := test.policy.ShouldEnforce()
+		result, err := buildExtWrite(test.noSaleBidders, test.ext)
+		assertError(t, test.expectedError, err, test.description)
 		assert.Equal(t, test.expected, result, test.description)
 	}
 }
+
+func assertError(t *testing.T, expectError bool, err error, description string) {
+	t.Helper()
+	if expectError {
+		assert.Error(t, err, description)
+	} else {
+		assert.NoError(t, err, description)
+	}
+}
diff --git a/privacy/enforcer_test.go b/privacy/enforcer_test.go
new file mode 100644
index 00000000..b0c4032c
--- /dev/null
+++ b/privacy/enforcer_test.go
@@ -0,0 +1,18 @@
+package privacy
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNilEnforcerCanEnforce(t *testing.T) {
+	nilEnforcer := &NilPolicyEnforcer{}
+	assert.False(t, nilEnforcer.CanEnforce())
+}
+
+func TestNilEnforcerShouldEnforce(t *testing.T) {
+	nilEnforcer := &NilPolicyEnforcer{}
+	assert.False(t, nilEnforcer.ShouldEnforce(""))
+	assert.False(t, nilEnforcer.ShouldEnforce("anyBidder"))
+}
diff --git a/privacy/gdpr/consentwriter_test.go b/privacy/gdpr/consentwriter_test.go
new file mode 100644
index 00000000..77fbdf88
--- /dev/null
+++ b/privacy/gdpr/consentwriter_test.go
@@ -0,0 +1,101 @@
+package gdpr
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/mxmCherry/openrtb"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestConsentWriter(t *testing.T) {
+	testCases := []struct {
+		description   string
+		consent       string
+		request       *openrtb.BidRequest
+		expected      *openrtb.BidRequest
+		expectedError bool
+	}{
+		{
+			description: "Empty",
+			consent:     "",
+			request:     &openrtb.BidRequest{},
+			expected:    &openrtb.BidRequest{},
+		},
+		{
+			description: "Enabled With Nil Request User Object",
+			consent:     "anyConsent",
+			request:     &openrtb.BidRequest{},
+			expected: &openrtb.BidRequest{User: &openrtb.User{
+				Ext: json.RawMessage(`{"consent":"anyConsent"}`)}},
+		},
+		{
+			description: "Enabled With Nil Request User Ext Object",
+			consent:     "anyConsent",
+			request:     &openrtb.BidRequest{User: &openrtb.User{}},
+			expected: &openrtb.BidRequest{User: &openrtb.User{
+				Ext: json.RawMessage(`{"consent":"anyConsent"}`)}},
+		},
+		{
+			description: "Enabled With Existing Request User Ext Object - Doesn't Overwrite",
+			consent:     "anyConsent",
+			request: &openrtb.BidRequest{User: &openrtb.User{
+				Ext: json.RawMessage(`{"existing":"any"}`)}},
+			expected: &openrtb.BidRequest{User: &openrtb.User{
+				Ext: json.RawMessage(`{"consent":"anyConsent","existing":"any"}`)}},
+		},
+		{
+			description: "Enabled With Existing Request User Ext Object - Overwrites",
+			consent:     "anyConsent",
+			request: &openrtb.BidRequest{User: &openrtb.User{
+				Ext: json.RawMessage(`{"existing":"any","consent":"toBeOverwritten"}`)}},
+			expected: &openrtb.BidRequest{User: &openrtb.User{
+				Ext: json.RawMessage(`{"consent":"anyConsent","existing":"any"}`)}},
+		},
+		{
+			description: "Enabled With Existing Malformed Request User Ext Object",
+			consent:     "anyConsent",
+			request: &openrtb.BidRequest{User: &openrtb.User{
+				Ext: json.RawMessage(`malformed`)}},
+			expectedError: true,
+		},
+		{
+			description: "Injection Attack With Nil Request User Object",
+			consent:     "BONV8oqONXwgmADACHENAO7pqzAAppY\"},\"oops\":\"malicious\",\"p\":{\"p\":\"",
+			request:     &openrtb.BidRequest{},
+			expected: &openrtb.BidRequest{User: &openrtb.User{
+				Ext: json.RawMessage(`{"consent":"BONV8oqONXwgmADACHENAO7pqzAAppY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}`),
+			}},
+		},
+		{
+			description: "Injection Attack With Nil Request User Ext Object",
+			consent:     "BONV8oqONXwgmADACHENAO7pqzAAppY\"},\"oops\":\"malicious\",\"p\":{\"p\":\"",
+			request:     &openrtb.BidRequest{User: &openrtb.User{}},
+			expected: &openrtb.BidRequest{User: &openrtb.User{
+				Ext: json.RawMessage(`{"consent":"BONV8oqONXwgmADACHENAO7pqzAAppY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}`),
+			}},
+		},
+		{
+			description: "Injection Attack With Existing Request User Ext Object",
+			consent:     "BONV8oqONXwgmADACHENAO7pqzAAppY\"},\"oops\":\"malicious\",\"p\":{\"p\":\"",
+			request: &openrtb.BidRequest{User: &openrtb.User{
+				Ext: json.RawMessage(`{"existing":"any"}`),
+			}},
+			expected: &openrtb.BidRequest{User: &openrtb.User{
+				Ext: json.RawMessage(`{"consent":"BONV8oqONXwgmADACHENAO7pqzAAppY\"},\"oops\":\"malicious\",\"p\":{\"p\":\"","existing":"any"}`),
+			}},
+		},
+	}
+
+	for _, test := range testCases {
+		writer := ConsentWriter{test.consent}
+		err := writer.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/gdpr/policy_test.go b/privacy/gdpr/policy_test.go
index c9bf10cd..dc8f5642 100644
--- a/privacy/gdpr/policy_test.go
+++ b/privacy/gdpr/policy_test.go
@@ -1,129 +1,36 @@
 package gdpr
 
 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{Consent: ""},
-			request:     &openrtb.BidRequest{},
-			expected:    &openrtb.BidRequest{},
-		},
-		{
-			description: "Enabled With Nil Request User Object",
-			policy:      Policy{Consent: "anyConsent"},
-			request:     &openrtb.BidRequest{},
-			expected: &openrtb.BidRequest{User: &openrtb.User{
-				Ext: json.RawMessage(`{"consent":"anyConsent"}`)}},
-		},
-		{
-			description: "Enabled With Nil Request User Ext Object",
-			policy:      Policy{Consent: "anyConsent"},
-			request:     &openrtb.BidRequest{User: &openrtb.User{}},
-			expected: &openrtb.BidRequest{User: &openrtb.User{
-				Ext: json.RawMessage(`{"consent":"anyConsent"}`)}},
-		},
-		{
-			description: "Enabled With Existing Request User Ext Object - Doesn't Overwrite",
-			policy:      Policy{Consent: "anyConsent"},
-			request: &openrtb.BidRequest{User: &openrtb.User{
-				Ext: json.RawMessage(`{"existing":"any"}`)}},
-			expected: &openrtb.BidRequest{User: &openrtb.User{
-				Ext: json.RawMessage(`{"consent":"anyConsent","existing":"any"}`)}},
-		},
-		{
-			description: "Enabled With Existing Request User Ext Object - Overwrites",
-			policy:      Policy{Consent: "anyConsent"},
-			request: &openrtb.BidRequest{User: &openrtb.User{
-				Ext: json.RawMessage(`{"existing":"any","consent":"toBeOverwritten"}`)}},
-			expected: &openrtb.BidRequest{User: &openrtb.User{
-				Ext: json.RawMessage(`{"consent":"anyConsent","existing":"any"}`)}},
-		},
-		{
-			description: "Enabled With Existing Malformed Request User Ext Object",
-			policy:      Policy{Consent: "anyConsent"},
-			request: &openrtb.BidRequest{User: &openrtb.User{
-				Ext: json.RawMessage(`malformed`)}},
-			expectedError: true,
-		},
-		{
-			description: "Injection Attack With Nil Request User Object",
-			policy:      Policy{Consent: "BONV8oqONXwgmADACHENAO7pqzAAppY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""},
-			request:     &openrtb.BidRequest{},
-			expected: &openrtb.BidRequest{User: &openrtb.User{
-				Ext: json.RawMessage(`{"consent":"BONV8oqONXwgmADACHENAO7pqzAAppY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}`),
-			}},
-		},
-		{
-			description: "Injection Attack With Nil Request User Ext Object",
-			policy:      Policy{Consent: "BONV8oqONXwgmADACHENAO7pqzAAppY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""},
-			request:     &openrtb.BidRequest{User: &openrtb.User{}},
-			expected: &openrtb.BidRequest{User: &openrtb.User{
-				Ext: json.RawMessage(`{"consent":"BONV8oqONXwgmADACHENAO7pqzAAppY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""}`),
-			}},
-		},
-		{
-			description: "Injection Attack With Existing Request User Ext Object",
-			policy:      Policy{Consent: "BONV8oqONXwgmADACHENAO7pqzAAppY\"},\"oops\":\"malicious\",\"p\":{\"p\":\""},
-			request: &openrtb.BidRequest{User: &openrtb.User{
-				Ext: json.RawMessage(`{"existing":"any"}`),
-			}},
-			expected: &openrtb.BidRequest{User: &openrtb.User{
-				Ext: json.RawMessage(`{"consent":"BONV8oqONXwgmADACHENAO7pqzAAppY\"},\"oops\":\"malicious\",\"p\":{\"p\":\"","existing":"any"}`),
-			}},
-		},
-	}
-
-	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 TestValidateConsent(t *testing.T) {
 	testCases := []struct {
 		description string
 		consent     string
-		expectError bool
+		expected    bool
 	}{
 		{
 			description: "Invalid",
 			consent:     "<any invalid>",
-			expectError: true,
+			expected:    false,
 		},
 		{
-			description: "Valid",
+			description: "TCF1 Valid",
 			consent:     "BONV8oqONXwgmADACHENAO7pqzAAppY",
-			expectError: false,
+			expected:    true,
+		},
+		{
+			description: "TCF2 Valid",
+			consent:     "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA",
+			expected:    true,
 		},
 	}
 
 	for _, test := range testCases {
 		result := ValidateConsent(test.consent)
-
-		if test.expectError {
-			assert.Error(t, result, test.description)
-		} else {
-			assert.NoError(t, result, test.description)
-		}
+		assert.Equal(t, test.expected, result, test.description)
 	}
 }
diff --git a/privacy/lmt/policy_test.go b/privacy/lmt/policy_test.go
index 45de219a..3027414f 100644
--- a/privacy/lmt/policy_test.go
+++ b/privacy/lmt/policy_test.go
@@ -7,7 +7,7 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func TestRead(t *testing.T) {
+func TestReadFromRequest(t *testing.T) {
 	var one int8 = 1
 
 	testCases := []struct {
@@ -60,11 +60,73 @@ func TestRead(t *testing.T) {
 	}
 
 	for _, test := range testCases {
-		p := ReadPolicy(test.request)
+		p := ReadFromRequest(test.request)
 		assert.Equal(t, test.expectedPolicy, p, test.description)
 	}
 }
 
+func TestCanEnforce(t *testing.T) {
+	testCases := []struct {
+		description string
+		policy      Policy
+		expected    bool
+	}{
+		{
+			description: "Signal Not Provided - Zero",
+			policy: Policy{
+				Signal:         0,
+				SignalProvided: false,
+			},
+			expected: false,
+		},
+		{
+			description: "Signal Not Provided - One",
+			policy: Policy{
+				Signal:         1,
+				SignalProvided: false,
+			},
+			expected: false,
+		},
+		{
+			description: "Signal Not Provided - Other",
+			policy: Policy{
+				Signal:         42,
+				SignalProvided: false,
+			},
+			expected: false,
+		},
+		{
+			description: "Signal Provided - Zero",
+			policy: Policy{
+				Signal:         0,
+				SignalProvided: true,
+			},
+			expected: true,
+		},
+		{
+			description: "Signal Provided - One",
+			policy: Policy{
+				Signal:         1,
+				SignalProvided: true,
+			},
+			expected: true,
+		},
+		{
+			description: "Signal Provided - Other",
+			policy: Policy{
+				Signal:         42,
+				SignalProvided: true,
+			},
+			expected: true,
+		},
+	}
+
+	for _, test := range testCases {
+		result := test.policy.CanEnforce()
+		assert.Equal(t, test.expected, result, test.description)
+	}
+}
+
 func TestShouldEnforce(t *testing.T) {
 	testCases := []struct {
 		description string
@@ -122,7 +184,7 @@ func TestShouldEnforce(t *testing.T) {
 	}
 
 	for _, test := range testCases {
-		result := test.policy.ShouldEnforce()
+		result := test.policy.ShouldEnforce("")
 		assert.Equal(t, test.expected, result, test.description)
 	}
 }
diff --git a/privacy/policies_test.go b/privacy/policies_test.go
deleted file mode 100644
index 34fbe52d..00000000
--- a/privacy/policies_test.go
+++ /dev/null
@@ -1,119 +0,0 @@
-package privacy
-
-import (
-	"errors"
-	"testing"
-
-	"github.com/mxmCherry/openrtb"
-	"github.com/prebid/prebid-server/privacy/ccpa"
-	"github.com/prebid/prebid-server/privacy/gdpr"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/mock"
-)
-
-func TestWritePoliciesNone(t *testing.T) {
-	request := &openrtb.BidRequest{}
-	policyWriters := []policyWriter{}
-
-	err := writePolicies(request, policyWriters)
-
-	assert.NoError(t, err)
-}
-
-func TestWritePoliciesOne(t *testing.T) {
-	request := &openrtb.BidRequest{}
-	mockWriter := new(mockPolicyWriter)
-	policyWriters := []policyWriter{
-		mockWriter,
-	}
-
-	mockWriter.On("Write", request).Return(nil).Once()
-
-	err := writePolicies(request, policyWriters)
-
-	assert.NoError(t, err)
-	mockWriter.AssertExpectations(t)
-}
-
-func TestWritePoliciesMany(t *testing.T) {
-	request := &openrtb.BidRequest{}
-	mockWriter1 := new(mockPolicyWriter)
-	mockWriter2 := new(mockPolicyWriter)
-	policyWriters := []policyWriter{
-		mockWriter1, mockWriter2,
-	}
-
-	mockWriter1.On("Write", request).Return(nil).Once()
-	mockWriter2.On("Write", request).Return(nil).Once()
-
-	err := writePolicies(request, policyWriters)
-
-	assert.NoError(t, err)
-	mockWriter1.AssertExpectations(t)
-	mockWriter2.AssertExpectations(t)
-}
-
-func TestWritePoliciesError(t *testing.T) {
-	request := &openrtb.BidRequest{}
-	mockWriter := new(mockPolicyWriter)
-	policyWriters := []policyWriter{
-		mockWriter,
-	}
-
-	expectedErr := errors.New("anyError")
-	mockWriter.On("Write", request).Return(expectedErr).Once()
-
-	err := writePolicies(request, policyWriters)
-
-	assert.Error(t, err, expectedErr)
-	mockWriter.AssertExpectations(t)
-}
-
-type mockPolicyWriter struct {
-	mock.Mock
-}
-
-func (m *mockPolicyWriter) Write(req *openrtb.BidRequest) error {
-	args := m.Called(req)
-	return args.Error(0)
-}
-
-func TestReadPoliciesFromConsent(t *testing.T) {
-	testCases := []struct {
-		description         string
-		consent             string
-		expectedResultValue Policies
-		expectedResultOK    bool
-	}{
-		{
-			description:         "Empty String",
-			consent:             "",
-			expectedResultValue: Policies{},
-			expectedResultOK:    false,
-		},
-		{
-			description:         "CCPA",
-			consent:             "1NYN",
-			expectedResultValue: Policies{CCPA: ccpa.Policy{Value: "1NYN"}},
-			expectedResultOK:    true,
-		},
-		{
-			description:         "GDPR TCF 1.0",
-			consent:             "BONV8oqONXwgmADACHENAO7pqzAAppY",
-			expectedResultValue: Policies{GDPR: gdpr.Policy{Consent: "BONV8oqONXwgmADACHENAO7pqzAAppY"}},
-			expectedResultOK:    true,
-		},
-		{
-			description:         "Invalid",
-			consent:             "any invalid",
-			expectedResultValue: Policies{},
-			expectedResultOK:    false,
-		},
-	}
-
-	for _, test := range testCases {
-		resultValue, resultOK := ReadPoliciesFromConsent(test.consent)
-		assert.Equal(t, test.expectedResultValue, resultValue, test.description+":value")
-		assert.Equal(t, test.expectedResultOK, resultOK, test.description+":ok")
-	}
-}
diff --git a/privacy/writer_test.go b/privacy/writer_test.go
new file mode 100644
index 00000000..79170cfc
--- /dev/null
+++ b/privacy/writer_test.go
@@ -0,0 +1,25 @@
+package privacy
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/mxmCherry/openrtb"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNilWriter(t *testing.T) {
+	request := &openrtb.BidRequest{
+		ID:  "anyID",
+		Ext: json.RawMessage(`{"anyJson":"anyValue"}`),
+	}
+	expectedRequest := &openrtb.BidRequest{
+		ID:  "anyID",
+		Ext: json.RawMessage(`{"anyJson":"anyValue"}`),
+	}
+
+	nilWriter := &NilPolicyWriter{}
+	nilWriter.Write(request)
+
+	assert.Equal(t, expectedRequest, request)
+}