Repository: prebid/prebid-server
Pull request #2567 (2023-02-06 20:17:17)

Refactor Targeting + Price Granularity JSON Unmarshal
Defaults for targeting and price granularity were embedded into the json unmarshal hooks which caused unpredictable behavior. You couldn't tell if where defaults were set just looking at code since it depended on when the objects are unmarshalled. The unmarshal order is changed by https://github.com/prebid/prebid-server/pull/2423 and tests broken.

Refactored to a deterministic order.

Co-Authored By @guscarreon  


Patch:
diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml
index b56ddad9..f256d875 100644
--- a/.github/workflows/security.yml
+++ b/.github/workflows/security.yml
@@ -5,6 +5,8 @@ on:
     branches: [master]
   pull_request:
     branches: [master]
+    paths:
+      - go.mod
 
 jobs:
   build:
diff --git a/adapters/logan/logan.go b/adapters/logan/logan.go
new file mode 100644
index 00000000..6aba0d4a
--- /dev/null
+++ b/adapters/logan/logan.go
@@ -0,0 +1,150 @@
+package logan
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+
+	"github.com/prebid/openrtb/v17/openrtb2"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+type adapter struct {
+	endpoint string
+}
+
+type reqBodyExt struct {
+	LoganBidderExt reqBodyExtBidder `json:"bidder"`
+}
+
+type reqBodyExtBidder struct {
+	Type        string `json:"type"`
+	PlacementID string `json:"placementId,omitempty"`
+}
+
+func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
+	bidder := &adapter{
+		endpoint: config.Endpoint,
+	}
+	return bidder, nil
+}
+
+func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	var errors []error
+	var adapterRequests []*adapters.RequestData
+
+	originalImpSlice := request.Imp
+
+	for i := range originalImpSlice {
+		currImp := originalImpSlice[i]
+		request.Imp = []openrtb2.Imp{currImp}
+
+		var bidderExt reqBodyExt
+		if err := json.Unmarshal(currImp.Ext, &bidderExt); err != nil {
+			errors = append(errors, &errortypes.BadInput{
+				Message: err.Error(),
+			})
+			continue // or return
+		}
+
+		bidderExt.LoganBidderExt.Type = "publisher" // constant
+
+		finalImpExt, err := json.Marshal(bidderExt)
+		if err != nil {
+			return nil, append(errors, err)
+		}
+
+		request.Imp[0].Ext = finalImpExt
+
+		adapterReq, err := a.makeRequest(request)
+		if err != nil {
+			return nil, append(errors, err)
+		}
+
+		if adapterReq != nil {
+			adapterRequests = append(adapterRequests, adapterReq)
+		}
+	}
+	request.Imp = originalImpSlice
+	return adapterRequests, nil
+}
+
+func (a *adapter) makeRequest(request *openrtb2.BidRequest) (*adapters.RequestData, error) {
+	reqJSON, err := json.Marshal(request)
+	if err != nil {
+		return nil, err
+	}
+
+	headers := http.Header{}
+	headers.Add("Content-Type", "application/json;charset=utf-8")
+	headers.Add("Accept", "application/json")
+	return &adapters.RequestData{
+		Method:  "POST",
+		Uri:     a.endpoint,
+		Body:    reqJSON,
+		Headers: headers,
+	}, nil
+}
+
+func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	if responseData.StatusCode == http.StatusNoContent {
+		return nil, nil
+	}
+
+	if responseData.StatusCode != http.StatusOK {
+		err := &errortypes.BadServerResponse{
+			Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info.", responseData.StatusCode),
+		}
+		return nil, []error{err}
+	}
+
+	var response openrtb2.BidResponse
+	if err := json.Unmarshal(responseData.Body, &response); err != nil {
+		return nil, []error{err}
+	}
+
+	bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
+	bidResponse.Currency = response.Cur
+
+	impsMappedByID := make(map[string]openrtb2.Imp, len(request.Imp))
+	for i, imp := range request.Imp {
+		impsMappedByID[request.Imp[i].ID] = imp
+	}
+
+	for _, seatBid := range response.SeatBid {
+		for i := range seatBid.Bid {
+			bidType, err := getMediaTypeForImp(seatBid.Bid[i].ImpID, impsMappedByID)
+			if err != nil {
+				return nil, []error{err}
+			}
+
+			b := &adapters.TypedBid{
+				Bid:     &seatBid.Bid[i],
+				BidType: bidType,
+			}
+			bidResponse.Bids = append(bidResponse.Bids, b)
+		}
+	}
+	return bidResponse, nil
+}
+
+func getMediaTypeForImp(impID string, impMap map[string]openrtb2.Imp) (openrtb_ext.BidType, error) {
+	if index, found := impMap[impID]; found {
+		if index.Banner != nil {
+			return openrtb_ext.BidTypeBanner, nil
+		}
+		if index.Video != nil {
+			return openrtb_ext.BidTypeVideo, nil
+		}
+		if index.Native != nil {
+			return openrtb_ext.BidTypeNative, nil
+		}
+	}
+
+	return "", &errortypes.BadInput{
+		Message: fmt.Sprintf("Failed to find impression \"%s\"", impID),
+	}
+}
diff --git a/adapters/taboola/taboola.go b/adapters/taboola/taboola.go
index ecd6fcbf..1c0cd484 100644
--- a/adapters/taboola/taboola.go
+++ b/adapters/taboola/taboola.go
@@ -3,45 +3,69 @@ package taboola
 import (
 	"encoding/json"
 	"fmt"
+	"github.com/prebid/openrtb/v17/native1"
+	nativeResponse "github.com/prebid/openrtb/v17/native1/response"
 	"github.com/prebid/openrtb/v17/openrtb2"
 	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/macros"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"net/http"
+	"net/url"
+	"text/template"
 )
 
 type adapter struct {
-	endpoint string
+	endpoint *template.Template
+	hostName string
 }
 
-// Builder builds a new instance of the Foo adapter for the given bidder with the given config.
+// Builder builds a new instance of Taboola adapter for the given bidder with the given config.
 func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
+	endpointTemplate, err := template.New("endpointTemplate").Parse(config.Endpoint)
+	if err != nil {
+		return nil, fmt.Errorf("unable to parse endpoint url template: %v", err)
+	}
+
+	hostName := ""
+	if server.ExternalUrl != "" {
+		parsedUrl, err := url.Parse(server.ExternalUrl)
+		if err == nil && parsedUrl != nil {
+			hostName = parsedUrl.Host
+		}
+	}
+
+	if err != nil {
+		return nil, fmt.Errorf("unable to parse endpoint url template: %v", err)
+	}
 	bidder := &adapter{
-		endpoint: config.Endpoint,
+		endpoint: endpointTemplate,
+		hostName: hostName,
 	}
 	return bidder, nil
 }
 
 func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
 
-	taboolaRequest, errs := createTaboolaRequest(request)
+	var requests []*adapters.RequestData
+
+	taboolaRequests, errs := createTaboolaRequests(request)
 	if len(errs) > 0 {
 		return nil, errs
 	}
 
-	requestJSON, err := json.Marshal(taboolaRequest)
-	if err != nil {
-		return nil, []error{err}
-	}
-
-	requestData := &adapters.RequestData{
-		Method: "POST",
-		Uri:    a.endpoint + "/" + taboolaRequest.Site.ID,
-		Body:   requestJSON,
+	for _, taboolaRequest := range taboolaRequests {
+		if len(taboolaRequest.Imp) > 0 {
+			request, err := a.buildRequest(taboolaRequest)
+			if err != nil {
+				return nil, []error{fmt.Errorf("unable to build request %v", err)}
+			}
+			requests = append(requests, request)
+		}
 	}
 
-	return []*adapters.RequestData{requestData}, errs
+	return requests, errs
 }
 
 func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) {
@@ -79,6 +103,20 @@ func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.R
 				errs = append(errs, err)
 				continue
 			}
+			if bidType == openrtb_ext.BidTypeNative {
+				var nativePayload nativeResponse.Response
+				if err := json.Unmarshal(json.RawMessage(seatBid.Bid[i].AdM), &nativePayload); err != nil {
+					errs = append(errs, err)
+					continue
+				}
+				overrideEventTrackers(&nativePayload)
+				nativePayloadJson, err := json.Marshal(nativePayload)
+				if err != nil {
+					errs = append(errs, err)
+					continue
+				}
+				seatBid.Bid[i].AdM = string(nativePayloadJson)
+			}
 			b := &adapters.TypedBid{
 				Bid:     &seatBid.Bid[i],
 				BidType: bidType,
@@ -89,8 +127,55 @@ func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.R
 	return bidResponse, errs
 }
 
-func createTaboolaRequest(request *openrtb2.BidRequest) (taboolaRequest *openrtb2.BidRequest, errors []error) {
+func (a *adapter) buildRequest(request *openrtb2.BidRequest) (*adapters.RequestData, error) {
+	requestJSON, err := json.Marshal(request)
+	if err != nil {
+		return nil, err
+	}
+
+	const (
+		NATIVE_ENDPOINT_PREFIX  = "native"
+		DISPLAY_ENDPOINT_PREFIX = "display"
+	)
+
+	//set MediaType based on first imp
+	var mediaType string
+	if request.Imp[0].Banner != nil {
+		mediaType = DISPLAY_ENDPOINT_PREFIX
+	} else if request.Imp[0].Native != nil {
+		mediaType = NATIVE_ENDPOINT_PREFIX
+	} else {
+		return nil, fmt.Errorf("unsupported media type for imp: %v", request.Imp[0])
+	}
+
+	url, err := a.buildEndpointURL(request.Site.ID, mediaType)
+	if err != nil {
+		return nil, err
+	}
+
+	requestData := &adapters.RequestData{
+		Method: "POST",
+		Uri:    url,
+		Body:   requestJSON,
+	}
+
+	return requestData, nil
+}
+
+// Builds endpoint url based on adapter-specific pub settings from imp.ext
+func (a *adapter) buildEndpointURL(publisherId string, mediaType string) (string, error) {
+	endpointParams := macros.EndpointTemplateParams{PublisherID: publisherId, MediaType: mediaType, Host: a.hostName}
+	resolvedUrl, err := macros.ResolveMacros(a.endpoint, endpointParams)
+	if err != nil {
+		return "", err
+	}
+	return resolvedUrl, nil
+}
+
+func createTaboolaRequests(request *openrtb2.BidRequest) (taboolaRequests []*openrtb2.BidRequest, errors []error) {
 	modifiedRequest := *request
+	var nativeImp []openrtb2.Imp
+	var bannerImp []openrtb2.Imp
 	var errs []error
 
 	var taboolaExt openrtb_ext.ImpExtTaboola
@@ -114,6 +199,12 @@ func createTaboolaRequest(request *openrtb2.BidRequest) (taboolaRequest *openrtb
 			imp.BidFloor = taboolaExt.BidFloor
 			modifiedRequest.Imp[i] = imp
 		}
+
+		if modifiedRequest.Imp[i].Banner != nil {
+			bannerImp = append(bannerImp, modifiedRequest.Imp[i])
+		} else if modifiedRequest.Imp[i].Native != nil {
+			nativeImp = append(nativeImp, modifiedRequest.Imp[i])
+		}
 	}
 
 	publisher := &openrtb2.Publisher{
@@ -145,7 +236,10 @@ func createTaboolaRequest(request *openrtb2.BidRequest) (taboolaRequest *openrtb
 		modifiedRequest.BAdv = taboolaExt.BAdv
 	}
 
-	return &modifiedRequest, errs
+	taboolaRequests = append(taboolaRequests, overrideBidRequestImp(&modifiedRequest, nativeImp))
+	taboolaRequests = append(taboolaRequests, overrideBidRequestImp(&modifiedRequest, bannerImp))
+
+	return taboolaRequests, errs
 }
 
 func getMediaType(impID string, imps []openrtb2.Imp) (openrtb_ext.BidType, error) {
@@ -153,12 +247,14 @@ func getMediaType(impID string, imps []openrtb2.Imp) (openrtb_ext.BidType, error
 		if imp.ID == impID {
 			if imp.Banner != nil {
 				return openrtb_ext.BidTypeBanner, nil
+			} else if imp.Native != nil {
+				return openrtb_ext.BidTypeNative, nil
 			}
 		}
 	}
 
 	return "", &errortypes.BadInput{
-		Message: fmt.Sprintf("Failed to find banner impression \"%s\" ", impID),
+		Message: fmt.Sprintf("Failed to find banner/native impression \"%s\" ", impID),
 	}
 }
 
@@ -171,3 +267,25 @@ func evaluateDomain(publisherDomain string, request *openrtb2.BidRequest) (resul
 	}
 	return ""
 }
+
+func overrideBidRequestImp(originBidRequest *openrtb2.BidRequest, imp []openrtb2.Imp) (bidRequest *openrtb2.BidRequest) {
+	bidRequestResult := *originBidRequest
+	bidRequestResult.Imp = imp
+	return &bidRequestResult
+}
+
+func overrideEventTrackers(nativePayload *nativeResponse.Response) {
+	// convert eventrackers to the deprecated imptrackers and jstracker because it's not supported in native 1.1v
+	for _, eventTracker := range nativePayload.EventTrackers {
+		if eventTracker.Event != native1.EventTypeImpression {
+			continue
+		}
+		switch eventTracker.Method {
+		case native1.EventTrackingMethodImage:
+			nativePayload.ImpTrackers = append(nativePayload.ImpTrackers, eventTracker.URL)
+		case native1.EventTrackingMethodJS:
+			nativePayload.JSTracker = fmt.Sprintf("<script src=\"%s\"></script>", eventTracker.URL)
+		}
+	}
+	nativePayload.EventTrackers = nil
+}
diff --git a/config/bidderinfo.go b/config/bidderinfo.go
index 65847151..6f62488c 100644
--- a/config/bidderinfo.go
+++ b/config/bidderinfo.go
@@ -133,7 +133,7 @@ type Syncer struct {
 // In most cases, bidders will specify a URL with a `{{.RedirectURL}}` macro for the call back to
 // Prebid Server and a UserMacro which the bidder server will replace with the user's id. Example:
 //
-//	url: "https://sync.bidderserver.com/usersync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}}"
+//	url: "https://sync.bidderserver.com/usersync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redirect={{.RedirectURL}}"
 //	userMacro: "$UID"
 //
 // Prebid Server is configured with a default RedirectURL template matching the /setuid call. This
@@ -152,6 +152,8 @@ type SyncerEndpoint struct {
 	//  {{.GDPR}}        - This will be replaced with the "gdpr" property sent to /cookie_sync.
 	//  {{.Consent}}     - This will be replaced with the "consent" property sent to /cookie_sync.
 	//  {{.USPrivacy}}   - This will be replaced with the "us_privacy" property sent to /cookie_sync.
+	//  {{.GPP}}		 - This will be replaced with the "gpp" property sent to /cookie_sync.
+	//  {{.GPPSID}}		 - This will be replaced with the "gpp_sid" property sent to /cookie_sync.
 	URL string `yaml:"url" mapstructure:"url"`
 
 	// RedirectURL is an endpoint on the host server the user will be redirected to when a user sync
@@ -165,7 +167,7 @@ type SyncerEndpoint struct {
 	//  {{.UserMacro}}   - This will be replaced with the bidder server's user id macro.
 	//
 	// The endpoint on the host server is usually Prebid Server's /setuid endpoint. The default value is:
-	// `{{.ExternalURL}}/setuid?bidder={{.SyncerKey}}&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&f={{.SyncType}}&uid={{.UserMacro}}`
+	// `{{.ExternalURL}}/setuid?bidder={{.SyncerKey}}&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&f={{.SyncType}}&uid={{.UserMacro}}`
 	RedirectURL string `yaml:"redirectUrl" mapstructure:"redirect_url"`
 
 	// ExternalURL is available as a macro to the RedirectURL template. If not specified, either the syncer configuration
@@ -285,6 +287,7 @@ var testEndpointTemplateParams = macros.EndpointTemplateParams{
 	ZoneID:      "anyZoneID",
 	SourceId:    "anySourceID",
 	AdUnit:      "anyAdUnit",
+	MediaType:   "MediaType",
 }
 
 // validateAdapterEndpoint makes sure that an adapter has a valid endpoint
diff --git a/config/config.go b/config/config.go
index b6bc69b7..d009b984 100644
--- a/config/config.go
+++ b/config/config.go
@@ -933,7 +933,7 @@ func SetupViper(v *viper.Viper, filename string, bidderInfos BidderInfos) {
 
 	// some adapters append the user id to the end of the redirect url instead of using
 	// macro substitution. it is important for the uid to be the last query parameter.
-	v.SetDefault("user_sync.redirect_url", "{{.ExternalURL}}/setuid?bidder={{.SyncerKey}}&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&f={{.SyncType}}&uid={{.UserMacro}}")
+	v.SetDefault("user_sync.redirect_url", "{{.ExternalURL}}/setuid?bidder={{.SyncerKey}}&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&f={{.SyncType}}&uid={{.UserMacro}}")
 
 	v.SetDefault("max_request_size", 1024*256)
 	v.SetDefault("analytics.file.filename", "")
diff --git a/endpoints/cookie_sync.go b/endpoints/cookie_sync.go
index c61cedbb..0877ba7d 100644
--- a/endpoints/cookie_sync.go
+++ b/endpoints/cookie_sync.go
@@ -22,6 +22,7 @@ import (
 	"github.com/prebid/prebid-server/privacy"
 	"github.com/prebid/prebid-server/privacy/ccpa"
 	gdprPrivacy "github.com/prebid/prebid-server/privacy/gdpr"
+	gppPrivacy "github.com/prebid/prebid-server/privacy/gpp"
 	"github.com/prebid/prebid-server/stored_requests"
 	"github.com/prebid/prebid-server/usersync"
 )
@@ -154,6 +155,10 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, pr
 		CCPA: ccpa.Policy{
 			Consent: request.USPrivacy,
 		},
+		GPP: gppPrivacy.Policy{
+			Consent: request.GPP,
+			RawSID:  request.GPPSid,
+		},
 	}
 
 	ccpaParsedPolicy := ccpa.ParsedPolicy{}
@@ -398,6 +403,8 @@ type cookieSyncRequest struct {
 	GDPRConsent     string                           `json:"gdpr_consent"`
 	USPrivacy       string                           `json:"us_privacy"`
 	Limit           int                              `json:"limit"`
+	GPP             string                           `json:"gpp"`
+	GPPSid          string                           `json:"gpp_sid"`
 	CooperativeSync *bool                            `json:"coopSync"`
 	FilterSettings  *cookieSyncRequestFilterSettings `json:"filterSettings"`
 	Account         string                           `json:"account"`
diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go
index 6e38f8f8..96637a64 100644
--- a/endpoints/openrtb2/amp_auction.go
+++ b/endpoints/openrtb2/amp_auction.go
@@ -17,6 +17,7 @@ import (
 	"github.com/prebid/openrtb/v17/openrtb2"
 	"github.com/prebid/openrtb/v17/openrtb3"
 	"github.com/prebid/prebid-server/hooks/hookexecution"
+	"github.com/prebid/prebid-server/ortb"
 	"github.com/prebid/prebid-server/util/uuidutil"
 	jsonpatch "gopkg.in/evanphx/json-patch.v4"
 
@@ -444,16 +445,20 @@ func (deps *endpointDeps) parseAmpRequest(httpRequest *http.Request) (req *openr
 	deps.setFieldsImplicitly(httpRequest, req)
 
 	// Need to ensure cache and targeting are turned on
-	e = defaultRequestExt(req)
+	e = initAmpTargetingAndCache(req)
 	if errs = append(errs, e...); errortypes.ContainsFatalError(errs) {
 		return
 	}
 
-	// At this point, we should have a valid request that definitely has Targeting and Cache turned on
-	hasStoredResponses := len(storedAuctionResponses) > 0
+	if err := ortb.SetDefaults(req); err != nil {
+		errs = append(errs, err)
+		return
+	}
 
+	hasStoredResponses := len(storedAuctionResponses) > 0
 	e = deps.validateRequest(req, true, hasStoredResponses, storedBidResponses, false)
 	errs = append(errs, e...)
+
 	return
 }
 
@@ -697,7 +702,7 @@ func setHeights(formats []openrtb2.Format, height int64) {
 
 // AMP won't function unless ext.prebid.targeting and ext.prebid.cache.bids are defined.
 // If the user didn't include them, default those here.
-func defaultRequestExt(req *openrtb_ext.RequestWrapper) []error {
+func initAmpTargetingAndCache(req *openrtb_ext.RequestWrapper) []error {
 	extRequest, err := req.GetRequestExt()
 	if err != nil {
 		return []error{err}
@@ -706,27 +711,23 @@ func defaultRequestExt(req *openrtb_ext.RequestWrapper) []error {
 	prebid := extRequest.GetPrebid()
 	prebidModified := false
 
-	// create prebid object if missing from request
+	// create prebid object if missing
 	if prebid == nil {
 		prebid = &openrtb_ext.ExtRequestPrebid{}
 	}
 
-	// Ensure Targeting and caching is on
+	// create targeting object if missing
 	if prebid.Targeting == nil {
-		prebid.Targeting = &openrtb_ext.ExtRequestTargeting{
-			IncludeWinners:    true,
-			IncludeBidderKeys: true,
-			PriceGranularity:  openrtb_ext.PriceGranularityFromString("med"),
-		}
+		prebid.Targeting = &openrtb_ext.ExtRequestTargeting{}
 		prebidModified = true
 	}
 
+	// create cache object if missing
 	if prebid.Cache == nil {
-		prebid.Cache = &openrtb_ext.ExtRequestPrebidCache{
-			Bids: &openrtb_ext.ExtRequestPrebidCacheBids{},
-		}
+		prebid.Cache = &openrtb_ext.ExtRequestPrebidCache{}
 		prebidModified = true
-	} else if prebid.Cache.Bids == nil {
+	}
+	if prebid.Cache.Bids == nil {
 		prebid.Cache.Bids = &openrtb_ext.ExtRequestPrebidCacheBids{}
 		prebidModified = true
 	}
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index f8c4aecd..967bdf04 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -24,6 +24,7 @@ import (
 	"github.com/prebid/openrtb/v17/openrtb2"
 	"github.com/prebid/openrtb/v17/openrtb3"
 	"github.com/prebid/prebid-server/hooks"
+	"github.com/prebid/prebid-server/ortb"
 	"golang.org/x/net/publicsuffix"
 	jsonpatch "gopkg.in/evanphx/json-patch.v4"
 
@@ -439,6 +440,11 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request, labels *metric
 	// Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers).
 	deps.setFieldsImplicitly(httpRequest, req)
 
+	if err := ortb.SetDefaults(req); err != nil {
+		errs = []error{err}
+		return
+	}
+
 	if err := processInterstitials(req); err != nil {
 		errs = []error{err}
 		return
@@ -446,10 +452,7 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request, labels *metric
 
 	lmt.ModifyForIOS(req.BidRequest)
 
-	var hasStoredResponses bool
-	if len(storedAuctionResponses) > 0 {
-		hasStoredResponses = true
-	}
+	hasStoredResponses := len(storedAuctionResponses) > 0
 	errL := deps.validateRequest(req, false, hasStoredResponses, storedBidResponses, hasStoredBidRequest)
 	if len(errL) > 0 {
 		errs = append(errs, errL...)
@@ -671,7 +674,9 @@ func (deps *endpointDeps) validateRequest(req *openrtb_ext.RequestWrapper, isAmp
 	reqPrebid := reqExt.GetPrebid()
 	if err := deps.parseBidExt(req); err != nil {
 		return []error{err}
-	} else if reqPrebid != nil {
+	}
+
+	if reqPrebid != nil {
 		aliases = reqPrebid.Aliases
 
 		if err := deps.validateAliases(aliases); err != nil {
@@ -1499,8 +1504,56 @@ func validateRequestExt(req *openrtb_ext.RequestWrapper) error {
 	}
 
 	prebid := reqExt.GetPrebid()
-	if prebid != nil && prebid.Cache != nil && (prebid.Cache.Bids == nil && prebid.Cache.VastXML == nil) {
-		return errors.New(`request.ext is invalid: request.ext.prebid.cache requires one of the "bids" or "vastxml" properties`)
+
+	// exit early if there is no request.ext.prebid to validate
+	if prebid == nil {
+		return nil
+	}
+
+	if prebid.Cache != nil {
+		if prebid.Cache.Bids == nil && prebid.Cache.VastXML == nil {
+			return errors.New(`request.ext is invalid: request.ext.prebid.cache requires one of the "bids" or "vastxml" properties`)
+		}
+	}
+
+	if err := validateTargeting(prebid.Targeting); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func validateTargeting(t *openrtb_ext.ExtRequestTargeting) error {
+	if t == nil {
+		return nil
+	}
+
+	if (t.IncludeWinners == nil || !*t.IncludeWinners) && (t.IncludeBidderKeys == nil || !*t.IncludeBidderKeys) {
+		return errors.New("ext.prebid.targeting: At least one of includewinners or includebidderkeys must be enabled to enable targeting support")
+	}
+
+	if t.PriceGranularity != nil {
+		pg := t.PriceGranularity
+
+		if pg.Precision == nil {
+			return errors.New("Price granularity error: precision is required")
+		} else if *pg.Precision < 0 {
+			return errors.New("Price granularity error: precision must be non-negative")
+		} else if *pg.Precision > openrtb_ext.MaxDecimalFigures {
+			return fmt.Errorf("Price granularity error: precision of more than %d significant figures is not supported", openrtb_ext.MaxDecimalFigures)
+		}
+
+		var prevMax float64 = 0
+		for _, gr := range pg.Ranges {
+			if gr.Max <= prevMax {
+				return errors.New(`Price granularity error: range list must be ordered with increasing "max"`)
+			}
+
+			if gr.Increment <= 0.0 {
+				return errors.New("Price granularity error: increment must be a nonzero positive number")
+			}
+			prevMax = gr.Max
+		}
 	}
 
 	return nil
diff --git a/endpoints/openrtb2/sample-requests/amp/consent-through-query/addtl-consent-through-query.json b/endpoints/openrtb2/sample-requests/amp/consent-through-query/addtl-consent-through-query.json
index d67df50e..c775d332 100644
--- a/endpoints/openrtb2/sample-requests/amp/consent-through-query/addtl-consent-through-query.json
+++ b/endpoints/openrtb2/sample-requests/amp/consent-through-query/addtl-consent-through-query.json
@@ -3,12 +3,18 @@
   "query": "tag_id=101&addtl_consent=1~1.35.41.101",
   "config": {
     "mockBidders": [
-      {"bidderName": "appnexus", "currency": "USD", "price": 15}
+      {
+        "bidderName": "appnexus",
+        "currency": "USD",
+        "price": 15
+      }
     ]
   },
   "mockBidRequest": {
     "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5",
-    "site": {"page": "prebid.org"},
+    "site": {
+      "page": "prebid.org"
+    },
     "imp": [
       {
         "id": "/19968336/header-bid-tag-0",
@@ -32,10 +38,12 @@
     "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5",
     "site": {
       "page": "prebid.org",
-      "ext": {"amp":1}
+      "ext": {
+        "amp": 1
+      }
     },
     "device": {
-      "ip":"192.0.2.1"
+      "ip": "192.0.2.1"
     },
     "at": 1,
     "imp": [
@@ -63,11 +71,44 @@
     ],
     "user": {
       "ext": {
-        "ConsentedProvidersSettings":{"consented_providers":"1~1.35.41.101"},
-        "consented_providers_settings":{"consented_providers":[1,35,41,101]}
+        "ConsentedProvidersSettings": {
+          "consented_providers": "1~1.35.41.101"
+        },
+        "consented_providers_settings": {
+          "consented_providers": [
+            1,
+            35,
+            41,
+            101
+          ]
+        }
       }
     },
-    "ext": {"prebid":{"cache":{"bids":{"returnCreative":null},"vastxml":null},"channel":{"name":"amp","version":""},"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":20,"increment":0.1}]},"includewinners":true,"includebidderkeys":true,"includebrandcategory":null,"includeformat":false,"durationrangesec":null,"preferdeals":false}}}
+    "ext": {
+      "prebid": {
+        "cache": {
+          "bids": {}
+        },
+        "channel": {
+          "name": "amp",
+          "version": ""
+        },
+        "targeting": {
+          "pricegranularity": {
+            "precision": 2,
+            "ranges": [
+              {
+                "min": 0,
+                "max": 20,
+                "increment": 0.1
+              }
+            ]
+          },
+          "includewinners": true,
+          "includebidderkeys": true
+        }
+      }
+    }
   },
   "expectedAmpResponse": {
     "targeting": {
@@ -96,4 +137,4 @@
     }
   },
   "expectedReturnCode": 200
-}
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-ccpa-through-query.json b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-ccpa-through-query.json
index cbcd5ed7..f2e2a217 100644
--- a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-ccpa-through-query.json
+++ b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-ccpa-through-query.json
@@ -81,10 +81,7 @@
     "ext": {
       "prebid": {
         "cache": {
-          "bids": {
-            "returnCreative": null
-          },
-          "vastxml": null
+          "bids": {}
         },
         "channel": {
           "name": "amp",
@@ -102,11 +99,7 @@
             ]
           },
           "includewinners": true,
-          "includebidderkeys": true,
-          "includebrandcategory": null,
-          "includeformat": false,
-          "durationrangesec": null,
-          "preferdeals": false
+          "includebidderkeys": true
         }
       }
     }
diff --git a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-legacy-tcf2-consent-through-query.json b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-legacy-tcf2-consent-through-query.json
index 3a3baca9..1cf81ea2 100644
--- a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-legacy-tcf2-consent-through-query.json
+++ b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-legacy-tcf2-consent-through-query.json
@@ -86,10 +86,7 @@
     "ext": {
       "prebid": {
         "cache": {
-          "bids": {
-            "returnCreative": null
-          },
-          "vastxml": null
+          "bids": {}
         },
         "channel": {
           "name": "amp",
@@ -107,11 +104,7 @@
             ]
           },
           "includewinners": true,
-          "includebidderkeys": true,
-          "includebrandcategory": null,
-          "includeformat": false,
-          "durationrangesec": null,
-          "preferdeals": false
+          "includebidderkeys": true
         }
       }
     }
diff --git a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf1-consent-through-query.json b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf1-consent-through-query.json
index c81dff7a..c596a832 100644
--- a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf1-consent-through-query.json
+++ b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf1-consent-through-query.json
@@ -86,10 +86,7 @@
     "ext": {
       "prebid": {
         "cache": {
-          "bids": {
-            "returnCreative": null
-          },
-          "vastxml": null
+          "bids": {}
         },
         "channel": {
           "name": "amp",
@@ -107,11 +104,7 @@
             ]
           },
           "includewinners": true,
-          "includebidderkeys": true,
-          "includebrandcategory": null,
-          "includeformat": false,
-          "durationrangesec": null,
-          "preferdeals": false
+          "includebidderkeys": true
         }
       }
     }
diff --git a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf2-consent-through-query.json b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf2-consent-through-query.json
index 7d3d9931..fdc25ed9 100644
--- a/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf2-consent-through-query.json
+++ b/endpoints/openrtb2/sample-requests/amp/consent-through-query/gdpr-tcf2-consent-through-query.json
@@ -86,10 +86,7 @@
     "ext": {
       "prebid": {
         "cache": {
-          "bids": {
-            "returnCreative": null
-          },
-          "vastxml": null
+          "bids": {}
         },
         "channel": {
           "name": "amp",
@@ -107,11 +104,7 @@
             ]
           },
           "includewinners": true,
-          "includebidderkeys": true,
-          "includebrandcategory": null,
-          "includeformat": false,
-          "durationrangesec": null,
-          "preferdeals": false
+          "includebidderkeys": true
         }
       }
     }
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params-backward-compatible-merge.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params-backward-compatible-merge.json
index 425070d1..9e9fd538 100644
--- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params-backward-compatible-merge.json
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params-backward-compatible-merge.json
@@ -64,7 +64,18 @@
   "expectedReqExt": {
     "prebid": {
       "targeting": {
-        "pricegranularity": "low"
+        "pricegranularity": {
+          "precision": 2,
+          "ranges": [
+            {
+              "min": 0,
+              "max": 5,
+              "increment": 0.5
+            }
+          ]
+        },
+        "includewinners": true,
+        "includebidderkeys": true
       },
       "cache": {
         "bids": {}
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params-merge.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params-merge.json
index 22026f56..9f0e78be 100644
--- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params-merge.json
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params-merge.json
@@ -66,7 +66,18 @@
   "expectedReqExt": {
     "prebid": {
       "targeting": {
-        "pricegranularity": "low"
+        "pricegranularity": {
+          "precision": 2,
+          "ranges": [
+            {
+              "min": 0,
+              "max": 5,
+              "increment": 0.5
+            }
+          ]
+        },
+        "includewinners": true,
+        "includebidderkeys": true
       },
       "cache": {
         "bids": {}
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params.json
index 9d4bcd0d..27ee9da4 100644
--- a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params.json
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params.json
@@ -59,7 +59,18 @@
   "expectedReqExt": {
     "prebid": {
       "targeting": {
-        "pricegranularity": "low"
+        "pricegranularity": {
+          "precision": 2,
+          "ranges": [
+            {
+              "min": 0,
+              "max": 5,
+              "increment": 0.5
+            }
+          ]
+        },
+        "includewinners": true,
+        "includebidderkeys": true
       },
       "cache": {
         "bids": {}
diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go
index eddc5173..8fbb7f42 100644
--- a/endpoints/openrtb2/video_auction.go
+++ b/endpoints/openrtb2/video_auction.go
@@ -19,6 +19,7 @@ import (
 	"github.com/julienschmidt/httprouter"
 	"github.com/prebid/openrtb/v17/openrtb2"
 	"github.com/prebid/prebid-server/hooks/hookexecution"
+	"github.com/prebid/prebid-server/ortb"
 	jsonpatch "gopkg.in/evanphx/json-patch.v4"
 
 	accountService "github.com/prebid/prebid-server/account"
@@ -33,6 +34,7 @@ import (
 	"github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher"
 	"github.com/prebid/prebid-server/usersync"
 	"github.com/prebid/prebid-server/util/iputil"
+	"github.com/prebid/prebid-server/util/ptrutil"
 	"github.com/prebid/prebid-server/util/uuidutil"
 	"github.com/prebid/prebid-server/version"
 )
@@ -59,7 +61,7 @@ func NewVideoEndpoint(
 		return nil, errors.New("NewVideoEndpoint requires non-nil arguments.")
 	}
 
-	defRequest := defReqJSON != nil && len(defReqJSON) > 0
+	defRequest := len(defReqJSON) > 0
 
 	ipValidator := iputil.PublicNetworkIPValidator{
 		IPv4PrivateNetworks: cfg.RequestValidation.IPv4PrivateNetworksParsed,
@@ -253,6 +255,11 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 	// Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers).
 	deps.setFieldsImplicitly(r, bidReqWrapper)
 
+	if err := ortb.SetDefaults(bidReqWrapper); err != nil {
+		handleError(&labels, w, errL, &vo, &debugLog)
+		return
+	}
+
 	errL = deps.validateRequest(bidReqWrapper, false, false, nil, false)
 	if errortypes.ContainsFatalError(errL) {
 		handleError(&labels, w, errL, &vo, &debugLog)
@@ -586,7 +593,6 @@ func getVideoStoredRequestId(request []byte) (string, error) {
 }
 
 func mergeData(videoRequest *openrtb_ext.BidRequestVideo, bidRequest *openrtb2.BidRequest) error {
-
 	if videoRequest.Site != nil {
 		bidRequest.Site = videoRequest.Site
 		if &videoRequest.Content != nil {
@@ -639,7 +645,6 @@ func mergeData(videoRequest *openrtb_ext.BidRequestVideo, bidRequest *openrtb2.B
 }
 
 func createBidExtension(videoRequest *openrtb_ext.BidRequestVideo) ([]byte, error) {
-
 	var inclBrandCat *openrtb_ext.ExtIncludeBrandCategory
 	if videoRequest.IncludeBrandCategory != nil {
 		inclBrandCat = &openrtb_ext.ExtIncludeBrandCategory{
@@ -659,16 +664,11 @@ func createBidExtension(videoRequest *openrtb_ext.BidRequestVideo) ([]byte, erro
 		durationRangeSec = videoRequest.PodConfig.DurationRangeSec
 	}
 
-	priceGranularity := openrtb_ext.PriceGranularityFromString("med")
-	if videoRequest.PriceGranularity.Precision != 0 {
-		priceGranularity = videoRequest.PriceGranularity
-	}
-
 	targeting := openrtb_ext.ExtRequestTargeting{
-		PriceGranularity:     priceGranularity,
+		PriceGranularity:     videoRequest.PriceGranularity,
 		IncludeBrandCategory: inclBrandCat,
 		DurationRangeSec:     durationRangeSec,
-		IncludeBidderKeys:    true,
+		IncludeBidderKeys:    ptrutil.ToPtr(true),
 		AppendBidderNames:    videoRequest.AppendBidderNames,
 	}
 
@@ -684,11 +684,7 @@ func createBidExtension(videoRequest *openrtb_ext.BidRequestVideo) ([]byte, erro
 	}
 	extReq := openrtb_ext.ExtRequest{Prebid: prebid}
 
-	reqJSON, err := json.Marshal(extReq)
-	if err != nil {
-		return nil, err
-	}
-	return reqJSON, nil
+	return json.Marshal(extReq)
 }
 
 func (deps *endpointDeps) parseVideoRequest(request []byte, headers http.Header) (req *openrtb_ext.BidRequestVideo, errs []error, podErrors []PodError) {
diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go
index bdd00819..f79009b6 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -98,6 +98,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/krushmedia"
 	"github.com/prebid/prebid-server/adapters/kubient"
 	"github.com/prebid/prebid-server/adapters/lockerdome"
+	"github.com/prebid/prebid-server/adapters/logan"
 	"github.com/prebid/prebid-server/adapters/logicad"
 	"github.com/prebid/prebid-server/adapters/lunamedia"
 	"github.com/prebid/prebid-server/adapters/madvertise"
@@ -268,6 +269,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderKrushmedia:        krushmedia.Builder,
 		openrtb_ext.BidderKubient:           kubient.Builder,
 		openrtb_ext.BidderLockerDome:        lockerdome.Builder,
+		openrtb_ext.BidderLogan:             logan.Builder,
 		openrtb_ext.BidderLogicad:           logicad.Builder,
 		openrtb_ext.BidderLunaMedia:         lunamedia.Builder,
 		openrtb_ext.BidderMadvertise:        madvertise.Builder,
diff --git a/exchange/price_granularity.go b/exchange/price_granularity.go
index af3300b2..1af56ea2 100644
--- a/exchange/price_granularity.go
+++ b/exchange/price_granularity.go
@@ -11,9 +11,10 @@ import (
 func GetPriceBucket(cpm float64, config openrtb_ext.PriceGranularity) string {
 	cpmStr := ""
 	bucketMax := 0.0
-	increment := 0.0
-	precision := config.Precision
 	bucketMin := 0.0
+	increment := 0.0
+	precision := *config.Precision
+
 	for i := 0; i < len(config.Ranges); i++ {
 		if config.Ranges[i].Max > bucketMax {
 			bucketMax = config.Ranges[i].Max
diff --git a/exchange/utils.go b/exchange/utils.go
index 5450707d..d9c1c022 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -731,12 +731,12 @@ func getExtTargetData(requestExt *openrtb_ext.ExtRequest, cacheInstructions *ext
 
 	if requestExt != nil && requestExt.Prebid.Targeting != nil {
 		targData = &targetData{
-			priceGranularity:  requestExt.Prebid.Targeting.PriceGranularity,
-			includeWinners:    requestExt.Prebid.Targeting.IncludeWinners,
-			includeBidderKeys: requestExt.Prebid.Targeting.IncludeBidderKeys,
+			includeWinners:    *requestExt.Prebid.Targeting.IncludeWinners,
+			includeBidderKeys: *requestExt.Prebid.Targeting.IncludeBidderKeys,
 			includeCacheBids:  cacheInstructions.cacheBids,
 			includeCacheVast:  cacheInstructions.cacheVAST,
 			includeFormat:     requestExt.Prebid.Targeting.IncludeFormat,
+			priceGranularity:  *requestExt.Prebid.Targeting.PriceGranularity,
 			preferDeals:       requestExt.Prebid.Targeting.PreferDeals,
 		}
 	}
diff --git a/macros/macros.go b/macros/macros.go
index 4440f2e2..b265e4cd 100644
--- a/macros/macros.go
+++ b/macros/macros.go
@@ -13,6 +13,7 @@ type EndpointTemplateParams struct {
 	SourceId    string
 	AccountID   string
 	AdUnit      string
+	MediaType   string
 }
 
 // UserSyncTemplateParams specifies params for an user sync URL template
@@ -20,6 +21,8 @@ type UserSyncTemplateParams struct {
 	GDPR        string
 	GDPRConsent string
 	USPrivacy   string
+	GPP         string
+	GPPSID      string
 }
 
 // ResolveMacros resolves macros in the given template with the provided params
diff --git a/openrtb_ext/bid_request_video.go b/openrtb_ext/bid_request_video.go
index 171bd6dc..fa0a31d0 100644
--- a/openrtb_ext/bid_request_video.go
+++ b/openrtb_ext/bid_request_video.go
@@ -98,7 +98,7 @@ type BidRequestVideo struct {
 	//   object; optional
 	// Description:
 	//    Object to tell ad server how much money the “bidder” demand is worth to you
-	PriceGranularity PriceGranularity `json:"pricegranularity,omitempty"`
+	PriceGranularity *PriceGranularity `json:"pricegranularity,omitempty"`
 
 	// Attribute:
 	//   tmax
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 675beafc..ac2e0e24 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -186,6 +186,7 @@ const (
 	BidderKrushmedia        BidderName = "krushmedia"
 	BidderKubient           BidderName = "kubient"
 	BidderLockerDome        BidderName = "lockerdome"
+	BidderLogan             BidderName = "logan"
 	BidderLogicad           BidderName = "logicad"
 	BidderLunaMedia         BidderName = "lunamedia"
 	BidderMadvertise        BidderName = "madvertise"
@@ -363,6 +364,7 @@ func CoreBidderNames() []BidderName {
 		BidderKrushmedia,
 		BidderKubient,
 		BidderLockerDome,
+		BidderLogan,
 		BidderLogicad,
 		BidderLunaMedia,
 		BidderMadvertise,
diff --git a/openrtb_ext/imp_logan.go b/openrtb_ext/imp_logan.go
new file mode 100644
index 00000000..2143f032
--- /dev/null
+++ b/openrtb_ext/imp_logan.go
@@ -0,0 +1,5 @@
+package openrtb_ext
+
+type ImpExtLogan struct {
+	PlacementID string `json:"placementId"`
+}
diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go
index 6559e632..bc42769e 100644
--- a/openrtb_ext/request.go
+++ b/openrtb_ext/request.go
@@ -2,7 +2,6 @@ package openrtb_ext
 
 import (
 	"encoding/json"
-	"errors"
 
 	"github.com/prebid/openrtb/v17/openrtb2"
 )
@@ -118,8 +117,8 @@ type ExtRequestPrebidChannel struct {
 
 // ExtRequestPrebidCache defines the contract for bidrequest.ext.prebid.cache
 type ExtRequestPrebidCache struct {
-	Bids    *ExtRequestPrebidCacheBids `json:"bids"`
-	VastXML *ExtRequestPrebidCacheVAST `json:"vastxml"`
+	Bids    *ExtRequestPrebidCacheBids `json:"bids,omitempty"`
+	VastXML *ExtRequestPrebidCacheVAST `json:"vastxml,omitempty"`
 }
 
 type ExtRequestPrebidServer struct {
@@ -130,23 +129,23 @@ type ExtRequestPrebidServer struct {
 
 // ExtRequestPrebidCacheBids defines the contract for bidrequest.ext.prebid.cache.bids
 type ExtRequestPrebidCacheBids struct {
-	ReturnCreative *bool `json:"returnCreative"`
+	ReturnCreative *bool `json:"returnCreative,omitempty"`
 }
 
 // ExtRequestPrebidCacheVAST defines the contract for bidrequest.ext.prebid.cache.vastxml
 type ExtRequestPrebidCacheVAST struct {
-	ReturnCreative *bool `json:"returnCreative"`
+	ReturnCreative *bool `json:"returnCreative,omitempty"`
 }
 
 // ExtRequestTargeting defines the contract for bidrequest.ext.prebid.targeting
 type ExtRequestTargeting struct {
-	PriceGranularity     PriceGranularity         `json:"pricegranularity"`
-	IncludeWinners       bool                     `json:"includewinners"`
-	IncludeBidderKeys    bool                     `json:"includebidderkeys"`
-	IncludeBrandCategory *ExtIncludeBrandCategory `json:"includebrandcategory"`
-	IncludeFormat        bool                     `json:"includeformat"`
-	DurationRangeSec     []int                    `json:"durationrangesec"`
-	PreferDeals          bool                     `json:"preferdeals"`
+	PriceGranularity     *PriceGranularity        `json:"pricegranularity,omitempty"`
+	IncludeWinners       *bool                    `json:"includewinners,omitempty"`
+	IncludeBidderKeys    *bool                    `json:"includebidderkeys,omitempty"`
+	IncludeBrandCategory *ExtIncludeBrandCategory `json:"includebrandcategory,omitempty"`
+	IncludeFormat        bool                     `json:"includeformat,omitempty"`
+	DurationRangeSec     []int                    `json:"durationrangesec,omitempty"`
+	PreferDeals          bool                     `json:"preferdeals,omitempty"`
 	AppendBidderNames    bool                     `json:"appendbiddernames,omitempty"`
 }
 
@@ -157,34 +156,9 @@ type ExtIncludeBrandCategory struct {
 	TranslateCategories *bool  `json:"translatecategories,omitempty"`
 }
 
-// Make an unmarshaller that will set a default PriceGranularity
-func (ert *ExtRequestTargeting) UnmarshalJSON(b []byte) error {
-	if string(b) == "null" {
-		return nil
-	}
-
-	// define separate type to prevent infinite recursive calls to UnmarshalJSON
-	type extRequestTargetingDefaults ExtRequestTargeting
-	defaults := &extRequestTargetingDefaults{
-		PriceGranularity:  priceGranularityMed,
-		IncludeWinners:    true,
-		IncludeBidderKeys: true,
-	}
-
-	err := json.Unmarshal(b, defaults)
-	if err == nil {
-		if !defaults.IncludeWinners && !defaults.IncludeBidderKeys {
-			return errors.New("ext.prebid.targeting: At least one of includewinners or includebidderkeys must be enabled to enable targeting support")
-		}
-		*ert = ExtRequestTargeting(*defaults)
-	}
-
-	return err
-}
-
 // PriceGranularity defines the allowed values for bidrequest.ext.prebid.targeting.pricegranularity
 type PriceGranularity struct {
-	Precision int                `json:"precision,omitempty"`
+	Precision *int               `json:"precision,omitempty"`
 	Ranges    []GranularityRange `json:"ranges,omitempty"`
 }
 
@@ -197,141 +171,109 @@ type GranularityRange struct {
 	Increment float64 `json:"increment"`
 }
 
-// UnmarshalJSON : custom unmarshaller to handle legacy string granularites.
 func (pg *PriceGranularity) UnmarshalJSON(b []byte) error {
-	// We default to medium
-	if len(b) == 0 {
-		*pg = priceGranularityMed
-		return nil
-	}
-	// First check for legacy strings
-	var pgString string
-	err := json.Unmarshal(b, &pgString)
-	if err == nil {
-		*pg = PriceGranularityFromString(pgString)
-		if len(pg.Ranges) > 0 {
-			// Only exit if we matched something, else we try processing as custom granularity
-			// This way we error as expecting the new custom granularity standard.
+	// price granularity used to be a string referencing a predefined value, try to parse
+	// and map the legacy string before falling back to the modern custom model.
+	legacyID := ""
+	if err := json.Unmarshal(b, &legacyID); err == nil {
+		if legacyValue, ok := NewPriceGranularityFromLegacyID(legacyID); ok {
+			*pg = legacyValue
 			return nil
 		}
 	}
-	// Not legacy, so we do a normal Unmarshal
-	pgraw := PriceGranularityRaw{}
-	pgraw.Precision = 2
-	err = json.Unmarshal(b, &pgraw)
-	if err != nil {
-		return err
-	}
-	if pgraw.Precision < 0 {
-		return errors.New("Price granularity error: precision must be non-negative")
-	}
-	if pgraw.Precision > MaxDecimalFigures {
-		return errors.New("Price granularity error: precision of more than 15 significant figures is not supported")
-	}
-	if len(pgraw.Ranges) > 0 {
-		var prevMax float64 = 0
-		for i, gr := range pgraw.Ranges {
-			if gr.Max <= prevMax {
-				return errors.New("Price granularity error: range list must be ordered with increasing \"max\"")
-			}
-			if gr.Increment <= 0.0 {
-				return errors.New("Price granularity error: increment must be a nonzero positive number")
-			}
-			// Enforce that we don't read "min" from the request
-			pgraw.Ranges[i].Min = prevMax
-			prevMax = gr.Max
-		}
-		*pg = PriceGranularity(pgraw)
-		return nil
+
+	// use a type-alias to avoid calling back into this UnmarshalJSON implementation
+	modernValue := PriceGranularityRaw{}
+	err := json.Unmarshal(b, &modernValue)
+	if err == nil {
+		*pg = (PriceGranularity)(modernValue)
 	}
-	// Default to medium if no ranges are specified
-	*pg = priceGranularityMed
-	return nil
+	return err
 }
 
-// PriceGranularityFromString converts a legacy string into the new PriceGranularity
-func PriceGranularityFromString(gran string) PriceGranularity {
-	switch gran {
+func NewPriceGranularityDefault() PriceGranularity {
+	pg, _ := NewPriceGranularityFromLegacyID("medium")
+	return pg
+}
+
+// NewPriceGranularityFromLegacyID converts a legacy string into the new PriceGranularity structure.
+func NewPriceGranularityFromLegacyID(v string) (PriceGranularity, bool) {
+	precision2 := 2
+
+	switch v {
 	case "low":
-		return priceGranularityLow
+		return PriceGranularity{
+			Precision: &precision2,
+			Ranges: []GranularityRange{{
+				Min:       0,
+				Max:       5,
+				Increment: 0.5}},
+		}, true
+
 	case "med", "medium":
-		// Seems that PBS was written with medium = "med", so hacking that in
-		return priceGranularityMed
+		return PriceGranularity{
+			Precision: &precision2,
+			Ranges: []GranularityRange{{
+				Min:       0,
+				Max:       20,
+				Increment: 0.1}},
+		}, true
+
 	case "high":
-		return priceGranularityHigh
+		return PriceGranularity{
+			Precision: &precision2,
+			Ranges: []GranularityRange{{
+				Min:       0,
+				Max:       20,
+				Increment: 0.01}},
+		}, true
+
 	case "auto":
-		return priceGranularityAuto
+		return PriceGranularity{
+			Precision: &precision2,
+			Ranges: []GranularityRange{
+				{
+					Min:       0,
+					Max:       5,
+					Increment: 0.05,
+				},
+				{
+					Min:       5,
+					Max:       10,
+					Increment: 0.1,
+				},
+				{
+					Min:       10,
+					Max:       20,
+					Increment: 0.5,
+				},
+			},
+		}, true
+
 	case "dense":
-		return priceGranularityDense
+		return PriceGranularity{
+			Precision: &precision2,
+			Ranges: []GranularityRange{
+				{
+					Min:       0,
+					Max:       3,
+					Increment: 0.01,
+				},
+				{
+					Min:       3,
+					Max:       8,
+					Increment: 0.05,
+				},
+				{
+					Min:       8,
+					Max:       20,
+					Increment: 0.5,
+				},
+			},
+		}, true
 	}
-	// Return empty if not matched
-	return PriceGranularity{}
-}
-
-var priceGranularityLow = PriceGranularity{
-	Precision: 2,
-	Ranges: []GranularityRange{{
-		Min:       0,
-		Max:       5,
-		Increment: 0.5}},
-}
-
-var priceGranularityMed = PriceGranularity{
-	Precision: 2,
-	Ranges: []GranularityRange{{
-		Min:       0,
-		Max:       20,
-		Increment: 0.1}},
-}
-
-var priceGranularityHigh = PriceGranularity{
-	Precision: 2,
-	Ranges: []GranularityRange{{
-		Min:       0,
-		Max:       20,
-		Increment: 0.01}},
-}
-
-var priceGranularityDense = PriceGranularity{
-	Precision: 2,
-	Ranges: []GranularityRange{
-		{
-			Min:       0,
-			Max:       3,
-			Increment: 0.01,
-		},
-		{
-			Min:       3,
-			Max:       8,
-			Increment: 0.05,
-		},
-		{
-			Min:       8,
-			Max:       20,
-			Increment: 0.5,
-		},
-	},
-}
 
-var priceGranularityAuto = PriceGranularity{
-	Precision: 2,
-	Ranges: []GranularityRange{
-		{
-			Min:       0,
-			Max:       5,
-			Increment: 0.05,
-		},
-		{
-			Min:       5,
-			Max:       10,
-			Increment: 0.1,
-		},
-		{
-			Min:       10,
-			Max:       20,
-			Increment: 0.5,
-		},
-	},
+	return PriceGranularity{}, false
 }
 
 // ExtRequestPrebidData defines Prebid's First Party Data (FPD) and related bid request options.
diff --git a/openrtb_ext/request_wrapper.go b/openrtb_ext/request_wrapper.go
index 526d86f0..aa301803 100644
--- a/openrtb_ext/request_wrapper.go
+++ b/openrtb_ext/request_wrapper.go
@@ -712,7 +712,7 @@ func (re *RequestExt) SetExt(ext map[string]json.RawMessage) {
 }
 
 func (re *RequestExt) GetPrebid() *ExtRequestPrebid {
-	if re.prebid == nil {
+	if re == nil || re.prebid == nil {
 		return nil
 	}
 	prebid := *re.prebid
diff --git a/ortb/default.go b/ortb/default.go
new file mode 100644
index 00000000..2f4255d3
--- /dev/null
+++ b/ortb/default.go
@@ -0,0 +1,87 @@
+package ortb
+
+import (
+	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/prebid/prebid-server/util/ptrutil"
+)
+
+const (
+	DefaultPriceGranularityPrecision  = 2
+	DefaultTargetingIncludeWinners    = true
+	DefaultTargetingIncludeBidderKeys = true
+)
+
+func SetDefaults(r *openrtb_ext.RequestWrapper) error {
+	requestExt, err := r.GetRequestExt()
+	if err != nil {
+		return err
+	}
+
+	requestExtPrebid := requestExt.GetPrebid()
+	if requestExtPrebid != nil {
+		modified := setDefaultsTargeting(requestExtPrebid.Targeting)
+
+		if modified {
+			requestExt.SetPrebid(requestExtPrebid)
+		}
+	}
+
+	return nil
+}
+
+func setDefaultsTargeting(targeting *openrtb_ext.ExtRequestTargeting) bool {
+	if targeting == nil {
+		return false
+	}
+
+	modified := false
+
+	if targeting.PriceGranularity == nil || len(targeting.PriceGranularity.Ranges) == 0 {
+		targeting.PriceGranularity = ptrutil.ToPtr(openrtb_ext.NewPriceGranularityDefault())
+		modified = true
+	} else if setDefaultsPriceGranularity(targeting.PriceGranularity) {
+		modified = true
+	}
+
+	if targeting.IncludeWinners == nil {
+		targeting.IncludeWinners = ptrutil.ToPtr(DefaultTargetingIncludeWinners)
+		modified = true
+	}
+
+	if targeting.IncludeBidderKeys == nil {
+		targeting.IncludeBidderKeys = ptrutil.ToPtr(DefaultTargetingIncludeBidderKeys)
+		modified = true
+	}
+
+	return modified
+}
+
+func setDefaultsPriceGranularity(pg *openrtb_ext.PriceGranularity) bool {
+	modified := false
+
+	if pg.Precision == nil {
+		pg.Precision = ptrutil.ToPtr(DefaultPriceGranularityPrecision)
+		modified = true
+	}
+
+	if setDefaultsPriceGranularityRange(pg.Ranges) {
+		modified = true
+	}
+
+	return modified
+}
+
+func setDefaultsPriceGranularityRange(ranges []openrtb_ext.GranularityRange) bool {
+	modified := false
+
+	var prevMax float64 = 0
+	for i, r := range ranges {
+		if ranges[i].Min != prevMax {
+			ranges[i].Min = prevMax
+			modified = true
+		}
+		prevMax = r.Max
+	}
+
+	return modified
+}
diff --git a/privacy/gpp/gpp.go b/privacy/gpp/gpp.go
new file mode 100644
index 00000000..fe3507b6
--- /dev/null
+++ b/privacy/gpp/gpp.go
@@ -0,0 +1,8 @@
+package gpp
+
+// Policy represents the GPP privacy string container.
+// Currently just a placeholder until more expansive support is made.
+type Policy struct {
+	Consent string
+	RawSID  string // This is the CSV format ("2,6") that the IAB recommends for passing the SID(s) on a query string.
+}
diff --git a/privacy/policies.go b/privacy/policies.go
index bc844a4e..214696e1 100644
--- a/privacy/policies.go
+++ b/privacy/policies.go
@@ -3,6 +3,7 @@ package privacy
 import (
 	"github.com/prebid/prebid-server/privacy/ccpa"
 	"github.com/prebid/prebid-server/privacy/gdpr"
+	"github.com/prebid/prebid-server/privacy/gpp"
 	"github.com/prebid/prebid-server/privacy/lmt"
 )
 
@@ -11,4 +12,5 @@ type Policies struct {
 	CCPA ccpa.Policy
 	GDPR gdpr.Policy
 	LMT  lmt.Policy
+	GPP  gpp.Policy
 }
diff --git a/static/bidder-info/beyondmedia.yaml b/static/bidder-info/beyondmedia.yaml
index 4fb782b7..eb0d239f 100644
--- a/static/bidder-info/beyondmedia.yaml
+++ b/static/bidder-info/beyondmedia.yaml
@@ -13,3 +13,7 @@ capabilities:
     - banner
     - video
     - native
+userSync:
+  redirect:
+    url: "https://cookies.andbeyond.media/pbserver?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&ccpa={{.USPrivacy}}&redir={{.RedirectURL}}"
+    userMacro: "[UID]"
diff --git a/static/bidder-info/logan.yaml b/static/bidder-info/logan.yaml
new file mode 100644
index 00000000..e62458c5
--- /dev/null
+++ b/static/bidder-info/logan.yaml
@@ -0,0 +1,19 @@
+endpoint: "http://USeast2.logan.ai/pserver"
+maintainer:
+  email: "support@logan.ai"
+capabilities:
+  site:
+    mediaTypes:
+    - banner
+    - video
+    - native
+    
+  app:
+    mediaTypes:
+    - banner
+    - video
+    - native
+userSync:
+  redirect:
+    url: "https://ssp-cookie.logan.ai/pserver?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir={{.RedirectURL}}"
+    userMacro: "[UID]"
diff --git a/static/bidder-info/taboola.yaml b/static/bidder-info/taboola.yaml
index ef05d311..693ef048 100644
--- a/static/bidder-info/taboola.yaml
+++ b/static/bidder-info/taboola.yaml
@@ -1,4 +1,4 @@
-endpoint: "https://display.bidder.taboola.com/OpenRTB/TaboolaHB/auction"
+endpoint: "https://{{.MediaType}}.bidder.taboola.com/OpenRTB/PS/auction/{{.Host}}/{{.PublisherID}}"
 maintainer:
   email:  ps-team@taboola.com
 gvlVendorID: 42
@@ -6,10 +6,15 @@ capabilities:
   app:
     mediaTypes:
       - banner
+      - native
   site:
     mediaTypes:
       - banner
+      - native
 userSync:
   redirect:
-    url: https://trc.taboola.com/sg/ps/1/cm?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}}
+    url: https://trc.taboola.com/sg/ps/1/cm?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}}
+    userMacro: "<TUID>"
+  iframe:
+    url: https://cdn.taboola.com/scripts/ps-sync.html?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}}
     userMacro: "<TUID>"
diff --git a/static/bidder-params/logan.json b/static/bidder-params/logan.json
new file mode 100644
index 00000000..5f611918
--- /dev/null
+++ b/static/bidder-params/logan.json
@@ -0,0 +1,17 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "Logan Adapter Params",
+  "description": "A schema which validates params accepted by the Logan adapter",
+  "type": "object",
+
+  "properties": {
+    "placementId": {
+      "type": "string",
+      "minLength": 1,
+      "description": "Placement ID"
+    }
+  },
+  "required": [
+    "placementId"
+  ]
+}
\ No newline at end of file
diff --git a/usersync/syncer.go b/usersync/syncer.go
index c9ca39c2..2e0e4102 100644
--- a/usersync/syncer.go
+++ b/usersync/syncer.go
@@ -229,6 +229,8 @@ func (s standardSyncer) GetSync(syncTypes []SyncType, privacyPolicies privacy.Po
 		GDPR:        privacyPolicies.GDPR.Signal,
 		GDPRConsent: privacyPolicies.GDPR.Consent,
 		USPrivacy:   privacyPolicies.CCPA.Consent,
+		GPP:         privacyPolicies.GPP.Consent,
+		GPPSID:      privacyPolicies.GPP.RawSID,
 	})
 	if err != nil {
 		return Sync{}, err
diff --git a/util/ptrutil/ptrutil.go b/util/ptrutil/ptrutil.go
new file mode 100644
index 00000000..999ac34a
--- /dev/null
+++ b/util/ptrutil/ptrutil.go
@@ -0,0 +1,5 @@
+package ptrutil
+
+func ToPtr[T any](v T) *T {
+	return &v
+}


Test patch:
diff --git a/adapters/logan/logan_test.go b/adapters/logan/logan_test.go
new file mode 100644
index 00000000..da3a8741
--- /dev/null
+++ b/adapters/logan/logan_test.go
@@ -0,0 +1,20 @@
+package logan
+
+import (
+	"testing"
+
+	"github.com/prebid/prebid-server/adapters/adapterstest"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+func TestJsonSamples(t *testing.T) {
+	bidder, buildErr := Builder(openrtb_ext.BidderLogan, config.Adapter{
+		Endpoint: "http://endpoint1.logan.com/pserver"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})
+
+	if buildErr != nil {
+		t.Fatalf("Builder returned unexpected error %v", buildErr)
+	}
+
+	adapterstest.RunJSONBidderTest(t, "logantest", bidder)
+}
diff --git a/adapters/logan/logantest/exemplary/simple-banner.json b/adapters/logan/logantest/exemplary/simple-banner.json
new file mode 100644
index 00000000..5b7f0feb
--- /dev/null
+++ b/adapters/logan/logantest/exemplary/simple-banner.json
@@ -0,0 +1,123 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "device": {
+            "ip": "123.123.123.123",
+            "ua": "iPad"
+        },
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "placementId": "test"
+                    }
+                }
+            }
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://endpoint1.logan.com/pserver",
+                "body": {
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "tagid": "test",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 300,
+                                        "h": 250
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "placementId": "test",
+                                    "type": "publisher"
+                                }
+                            }
+                        }
+                    ],
+                    "app": {
+                        "id": "1",
+                        "bundle": "com.wls.testwlsapplication"
+                    },
+                    "device": {
+                        "ip": "123.123.123.123",
+                        "ua": "iPad"
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "bid": [
+                                {
+                                    "id": "test_bid_id",
+                                    "impid": "test-imp-id",
+                                    "price": 0.27543,
+                                    "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://endpoint1.logan.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                                    "cid": "test_cid",
+                                    "crid": "test_crid",
+                                    "dealid": "test_dealid",
+                                    "w": 300,
+                                    "h": 250
+                                }
+                            ],
+                            "seat": "logan"
+                        }
+                    ],
+                    "cur": "USD"
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "bids": [
+                {
+                    "bid": {
+                        "id": "test_bid_id",
+                        "impid": "test-imp-id",
+                        "price": 0.27543,
+                        "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://endpoint1.logan.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                        "cid": "test_cid",
+                        "crid": "test_crid",
+                        "dealid": "test_dealid",
+                        "w": 300,
+                        "h": 250
+                    },
+                    "type": "banner"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/logan/logantest/exemplary/simple-native.json b/adapters/logan/logantest/exemplary/simple-native.json
new file mode 100644
index 00000000..2c814f2e
--- /dev/null
+++ b/adapters/logan/logantest/exemplary/simple-native.json
@@ -0,0 +1,107 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "device": {
+            "ip": "123.123.123.123",
+            "ua": "iPad"
+        },
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "native": {
+                    "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}",
+                    "ver": "1.1"
+                },
+                "ext": {
+                    "bidder": {
+                        "placementId": "test"
+                    }
+                }
+            }
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://endpoint1.logan.com/pserver",
+                "body": {
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "tagid": "test",
+                            "native": {
+                                "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}",
+                                "ver": "1.1"
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "placementId": "test",
+                                    "type": "publisher"
+                                }
+                            }
+                        }
+                    ],
+                    "app": {
+                        "id": "1",
+                        "bundle": "com.wls.testwlsapplication"
+                    },
+                    "device": {
+                        "ip": "123.123.123.123",
+                        "ua": "iPad"
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "bid": [
+                                {
+                                    "id": "test_bid_id",
+                                    "impid": "test-imp-id",
+                                    "price": 0.27543,
+                                    "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://endpoint1.logan.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                                    "cid": "test_cid",
+                                    "crid": "test_crid",
+                                    "dealid": "test_dealid",
+                                    "w": 300,
+                                    "h": 250
+                                }
+                            ],
+                            "seat": "logan"
+                        }
+                    ],
+                    "cur": "USD"
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "bids": [
+                {
+                    "bid": {
+                        "id": "test_bid_id",
+                        "impid": "test-imp-id",
+                        "price": 0.27543,
+                        "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://endpoint1.logan.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                        "cid": "test_cid",
+                        "crid": "test_crid",
+                        "dealid": "test_dealid",
+                        "w": 300,
+                        "h": 250
+                    },
+                    "type": "native"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/logan/logantest/exemplary/simple-video.json b/adapters/logan/logantest/exemplary/simple-video.json
new file mode 100644
index 00000000..6d2b4b54
--- /dev/null
+++ b/adapters/logan/logantest/exemplary/simple-video.json
@@ -0,0 +1,118 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "device": {
+            "ip": "123.123.123.123",
+            "ua": "iPad"
+        },
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "video": {
+                    "mimes": [
+                        "video/mp4"
+                    ],
+                    "protocols": [
+                        2,
+                        5
+                    ],
+                    "w": 1024,
+                    "h": 576
+                },
+                "ext": {
+                    "bidder": {
+                        "placementId": "test"
+                    }
+                }
+            }
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://endpoint1.logan.com/pserver",
+                "body": {
+                    "id": "test-request-id",
+                    "device": {
+                        "ip": "123.123.123.123",
+                        "ua": "iPad"
+                    },
+                    "app": {
+                        "id": "1",
+                        "bundle": "com.wls.testwlsapplication"
+                    },
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "tagid": "test",
+                            "video": {
+                                "mimes": [
+                                    "video/mp4"
+                                ],
+                                "protocols": [
+                                    2,
+                                    5
+                                ],
+                                "w": 1024,
+                                "h": 576
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "placementId": "test",
+                                    "type": "publisher"
+                                }
+                            }
+                        }
+                    ]
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "bid": [
+                                {
+                                    "id": "test_bid_id",
+                                    "impid": "test-imp-id",
+                                    "price": 0.27543,
+                                    "adm": "<VAST version=\"3.0\"><Ad id=\"20001\" sequence=\"1\"><InLine><AdSystem version=\"4.0\"><![CDATA[iabtechlab]]></AdSystem><AdTitle><![CDATA[Inline Simple Ad]]></AdTitle><Impression><![CDATA[]]></Impression><Creatives><Creative id=\"5480\" sequence=\"1\"><Linear><Duration>00:01:00</Duration><MediaFiles><MediaFile id=\"5246\" delivery=\"progressive\" type=\"video/mp4\" bitrate=\"600\" width=\"640\" height=\"360\" minBitrate=\"500\" maxBitrate=\"700\" scalable=\"1\" maintainAspectRatio=\"1\" codec=\"0\"><![CDATA[https://s0.2mdn.net/4253510/google_ddm_animation_480P.mp4]]></MediaFile></MediaFiles><VideoClicks><ClickThrough id=\"blog\"><![CDATA[https://example.com]]></ClickThrough></VideoClicks></Linear></Creative></Creatives></InLine></Ad></VAST>",
+                                    "cid": "test_cid",
+                                    "crid": "test_crid",
+                                    "dealid": "test_dealid"
+                                }
+                            ],
+                            "seat": "logan"
+                        }
+                    ],
+                    "cur": "USD"
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "currency": "USD",
+            "bids": [
+                {
+                    "bid": {
+                        "id": "test_bid_id",
+                        "impid": "test-imp-id",
+                        "price": 0.27543,
+                        "adm": "<VAST version=\"3.0\"><Ad id=\"20001\" sequence=\"1\"><InLine><AdSystem version=\"4.0\"><![CDATA[iabtechlab]]></AdSystem><AdTitle><![CDATA[Inline Simple Ad]]></AdTitle><Impression><![CDATA[]]></Impression><Creatives><Creative id=\"5480\" sequence=\"1\"><Linear><Duration>00:01:00</Duration><MediaFiles><MediaFile id=\"5246\" delivery=\"progressive\" type=\"video/mp4\" bitrate=\"600\" width=\"640\" height=\"360\" minBitrate=\"500\" maxBitrate=\"700\" scalable=\"1\" maintainAspectRatio=\"1\" codec=\"0\"><![CDATA[https://s0.2mdn.net/4253510/google_ddm_animation_480P.mp4]]></MediaFile></MediaFiles><VideoClicks><ClickThrough id=\"blog\"><![CDATA[https://example.com]]></ClickThrough></VideoClicks></Linear></Creative></Creatives></InLine></Ad></VAST>",
+                        "cid": "test_cid",
+                        "crid": "test_crid",
+                        "dealid": "test_dealid"
+                    },
+                    "type": "video"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/logan/logantest/exemplary/simple-web-banner.json b/adapters/logan/logantest/exemplary/simple-web-banner.json
new file mode 100644
index 00000000..af45a0af
--- /dev/null
+++ b/adapters/logan/logantest/exemplary/simple-web-banner.json
@@ -0,0 +1,123 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "placementId": "test"
+                    }
+                }
+            }
+        ],
+        "site": {
+            "id": "1",
+            "domain": "test.com"
+        },
+        "device": {
+            "ip": "123.123.123.123",
+            "ua": "Ubuntu"
+        }
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://endpoint1.logan.com/pserver",
+                "body": {
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "tagid": "test",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 300,
+                                        "h": 250
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "placementId": "test",
+                                    "type": "publisher"
+                                }
+                            }
+                        }
+                    ],
+                    "site": {
+                        "id": "1",
+                        "domain": "test.com"
+                    },
+                    "device": {
+                        "ip": "123.123.123.123",
+                        "ua": "Ubuntu"
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "bid": [
+                                {
+                                    "id": "test_bid_id",
+                                    "impid": "test-imp-id",
+                                    "price": 0.27543,
+                                    "adm": "<iframe id=\"adm-banner-1\" width=\"468\" height=\"60\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://endpoint1.logan.com/pserver&k=bc2d316f39931a07d9a8dd249bf85fc0\"></iframe>",
+                                    "cid": "test_cid",
+                                    "crid": "test_crid",
+                                    "dealid": "test_dealid",
+                                    "w": 468,
+                                    "h": 60
+                                }
+                            ],
+                            "seat": "logan"
+                        }
+                    ],
+                    "cur": "USD"
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "bids": [
+                {
+                    "bid": {
+                        "id": "test_bid_id",
+                        "impid": "test-imp-id",
+                        "price": 0.27543,
+                        "adm": "<iframe id=\"adm-banner-1\" width=\"468\" height=\"60\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://endpoint1.logan.com/pserver&k=bc2d316f39931a07d9a8dd249bf85fc0\"></iframe>",
+                        "cid": "test_cid",
+                        "crid": "test_crid",
+                        "dealid": "test_dealid",
+                        "w": 468,
+                        "h": 60
+                    },
+                    "type": "banner"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/logan/logantest/supplemental/bad_media_type.json b/adapters/logan/logantest/supplemental/bad_media_type.json
new file mode 100644
index 00000000..de038a20
--- /dev/null
+++ b/adapters/logan/logantest/supplemental/bad_media_type.json
@@ -0,0 +1,81 @@
+{
+	"mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "ext": {
+                    "bidder": {
+                        "placementId": "test"
+                    }
+                }
+            }
+        ],
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "device": {
+            "ip": "123.123.123.123",
+            "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+        }
+    },
+	"httpCalls": [{
+		"expectedRequest": {
+			"uri": "http://endpoint1.logan.com/pserver",
+			"body": {
+                "id": "test-request-id",
+                "imp": [
+                    {
+                        "id": "test-imp-id",
+                        "ext": {
+                            "bidder": {
+                                "placementId": "test",
+                                "type": "publisher"
+                            }
+                        }
+                    }
+                ],
+                "app": {
+                    "id": "1",
+                    "bundle": "com.wls.testwlsapplication"
+                },
+                "device": {
+                    "ip": "123.123.123.123",
+                    "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+                }
+            }
+		},
+		"mockResponse": {
+			"status": 200,
+			"body": {
+                "id": "test-request-id",
+                "seatbid": [
+                    {
+                        "bid": [
+                            {
+                                "id": "test_bid_id",
+                                "impid": "test-imp-id",
+                                "price": 0.27543,
+                                "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://endpoint1.logan.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                                "cid": "test_cid",
+                                "crid": "test_crid",
+                                "dealid": "test_dealid",
+                                "w": 300,
+                                "h": 250
+                            }
+                        ],
+                        "seat": "logan"
+                    }
+                ],
+                "cur": "USD"
+            }
+        }
+    }],
+    "expectedMakeBidsErrors": [
+        {
+          "value": "Failed to find impression \"test-imp-id\"",
+          "comparison": "literal"
+        }
+    ]
+}
diff --git a/adapters/logan/logantest/supplemental/bad_response.json b/adapters/logan/logantest/supplemental/bad_response.json
new file mode 100644
index 00000000..d09947c6
--- /dev/null
+++ b/adapters/logan/logantest/supplemental/bad_response.json
@@ -0,0 +1,84 @@
+{
+	"mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "placementId": "test"
+                    }
+                }
+            }
+        ],
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "device": {
+            "ip": "123.123.123.123",
+            "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+        }
+    },
+	"httpCalls": [{
+		"expectedRequest": {
+			"uri": "http://endpoint1.logan.com/pserver",
+			"body": {
+                "id": "test-request-id",
+                "imp": [
+                    {
+                        "id": "test-imp-id",
+                        "banner": {
+                            "format": [
+                                {
+                                    "w": 300,
+                                    "h": 250
+                                },
+                                {
+                                    "w": 300,
+                                    "h": 600
+                                }
+                            ]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "placementId": "test",
+                                "type": "publisher"
+                            }
+                        }
+                    }
+                ],
+                "app": {
+                    "id": "1",
+                    "bundle": "com.wls.testwlsapplication"
+                },
+                "device": {
+                    "ip": "123.123.123.123",
+                    "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+                }
+            }
+		},
+		"mockResponse": {
+			"status": 200,
+			"body": ""
+        }
+    }],
+    "expectedMakeBidsErrors": [
+        {
+          "value": "json: cannot unmarshal string into Go value of type openrtb2.BidResponse",
+          "comparison": "literal"
+        }
+    ]
+}
diff --git a/adapters/logan/logantest/supplemental/status-204.json b/adapters/logan/logantest/supplemental/status-204.json
new file mode 100644
index 00000000..6f5c7a5e
--- /dev/null
+++ b/adapters/logan/logantest/supplemental/status-204.json
@@ -0,0 +1,79 @@
+{
+	"mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "placementId": "test"
+                    }
+                }
+            }
+        ],
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "device": {
+            "ip": "123.123.123.123",
+            "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+        }
+    },
+	"httpCalls": [{
+		"expectedRequest": {
+			"uri": "http://endpoint1.logan.com/pserver",
+			"body": {
+                "id": "test-request-id",
+                "imp": [
+                    {
+                        "id": "test-imp-id",
+                        "banner": {
+                            "format": [
+                                {
+                                    "w": 300,
+                                    "h": 250
+                                },
+                                {
+                                    "w": 300,
+                                    "h": 600
+                                }
+                            ]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "placementId": "test",
+                                "type": "publisher"
+                            }
+                        }
+                    }
+                ],
+                "app": {
+                    "id": "1",
+                    "bundle": "com.wls.testwlsapplication"
+                },
+                "device": {
+                    "ip": "123.123.123.123",
+                    "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+                }
+            }
+		},
+		"mockResponse": {
+			"status": 204,
+			"body": {}
+		}
+    }],
+    "expectedBidResponses": []
+}
diff --git a/adapters/logan/logantest/supplemental/status-not-200.json b/adapters/logan/logantest/supplemental/status-not-200.json
new file mode 100644
index 00000000..57f210da
--- /dev/null
+++ b/adapters/logan/logantest/supplemental/status-not-200.json
@@ -0,0 +1,84 @@
+{
+	"mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "placementId": "test"
+                    }
+                }
+            }
+        ],
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "device": {
+            "ip": "123.123.123.123",
+            "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+        }
+    },
+	"httpCalls": [{
+		"expectedRequest": {
+			"uri": "http://endpoint1.logan.com/pserver",
+			"body": {
+                "id": "test-request-id",
+                "imp": [
+                    {
+                        "id": "test-imp-id",
+                        "banner": {
+                            "format": [
+                                {
+                                    "w": 300,
+                                    "h": 250
+                                },
+                                {
+                                    "w": 300,
+                                    "h": 600
+                                }
+                            ]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "placementId": "test",
+                                "type": "publisher"
+                            }
+                        }
+                    }
+                ],
+                "app": {
+                    "id": "1",
+                    "bundle": "com.wls.testwlsapplication"
+                },
+                "device": {
+                    "ip": "123.123.123.123",
+                    "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+                }
+            }
+		},
+		"mockResponse": {
+			"status": 404,
+			"body": {}
+		}
+    }],
+    "expectedMakeBidsErrors": [
+        {
+          "value": "Unexpected status code: 404. Run with request.debug = 1 for more info.",
+          "comparison": "literal"
+        }
+    ]
+}
diff --git a/adapters/logan/params_test.go b/adapters/logan/params_test.go
new file mode 100644
index 00000000..48da1912
--- /dev/null
+++ b/adapters/logan/params_test.go
@@ -0,0 +1,45 @@
+package logan
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+func TestValidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json schema. %v", err)
+	}
+
+	for _, p := range validParams {
+		if err := validator.Validate(openrtb_ext.BidderLogan, json.RawMessage(p)); err != nil {
+			t.Errorf("Schema rejected valid params: %s", p)
+		}
+	}
+}
+
+func TestInvalidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json schema. %v", err)
+	}
+
+	for _, p := range invalidParams {
+		if err := validator.Validate(openrtb_ext.BidderLogan, json.RawMessage(p)); err == nil {
+			t.Errorf("Schema allowed invalid params: %s", p)
+		}
+	}
+}
+
+var validParams = []string{
+	`{"placementId": "test"}`,
+	`{"placementId": "1"}`,
+}
+
+var invalidParams = []string{
+	`{"placementId": 42}`,
+	`{"placementId": null}`,
+	`{"placementId": ""}`,
+}
diff --git a/adapters/smaato/smaatotest/exemplary/native.json b/adapters/smaato/smaatotest/exemplary/native.json
index 2f3f4823..76534dd4 100644
--- a/adapters/smaato/smaatotest/exemplary/native.json
+++ b/adapters/smaato/smaatotest/exemplary/native.json
@@ -35,6 +35,15 @@
                 "data": {}
             }
         },
+        "regs": {
+            "coppa": 1,
+            "ext": {
+                "gdpr": 1,
+                "us_privacy": "uspConsentString",
+                "gpp": "gppString",
+                "gpp_sid": [7]
+            }
+        },
         "ext": {
             "prebid": {
                 "auctiontimestamp": 1598262728811,
@@ -83,6 +92,15 @@
                             "id": "1100042525"
                         }
                     },
+                    "regs": {
+                        "coppa": 1,
+                        "ext": {
+                            "gdpr": 1,
+                            "us_privacy": "uspConsentString",
+                            "gpp": "gppString",
+                            "gpp_sid": [7]
+                        }
+                    },
                     "ext": {
                         "client": "prebid_server_0.5"
                     }
diff --git a/adapters/smaato/smaatotest/exemplary/simple-banner.json b/adapters/smaato/smaatotest/exemplary/simple-banner.json
index 102af452..f6f1f9d7 100644
--- a/adapters/smaato/smaatotest/exemplary/simple-banner.json
+++ b/adapters/smaato/smaatotest/exemplary/simple-banner.json
@@ -64,7 +64,9 @@
       "coppa": 1,
       "ext": {
         "gdpr": 1,
-        "us_privacy": "uspConsentString"
+        "us_privacy": "uspConsentString",
+        "gpp": "gppString",
+        "gpp_sid": [7]
       }
     }
   },
@@ -118,7 +120,9 @@
             "coppa": 1,
             "ext": {
               "gdpr": 1,
-              "us_privacy": "uspConsentString"
+              "us_privacy": "uspConsentString",
+              "gpp": "gppString",
+              "gpp_sid": [7]
             }
           },
           "site": {
diff --git a/adapters/smaato/smaatotest/exemplary/video.json b/adapters/smaato/smaatotest/exemplary/video.json
index 03654bfb..17af4b36 100644
--- a/adapters/smaato/smaatotest/exemplary/video.json
+++ b/adapters/smaato/smaatotest/exemplary/video.json
@@ -56,6 +56,15 @@
                 "data": {}
             }
         },
+        "regs": {
+            "coppa": 1,
+            "ext": {
+                "gdpr": 1,
+                "us_privacy": "uspConsentString",
+                "gpp": "gppString",
+                "gpp_sid": [7]
+            }
+        },
         "ext": {
             "prebid": {
                 "auctiontimestamp": 1598262728811,
@@ -117,6 +126,15 @@
                         "ext": {
                         }
                     },
+                    "regs": {
+                        "coppa": 1,
+                        "ext": {
+                            "gdpr": 1,
+                            "us_privacy": "uspConsentString",
+                            "gpp": "gppString",
+                            "gpp_sid": [7]
+                        }
+                    },
                     "device": {
                         "ua": "test-user-agent"
                     },
diff --git a/adapters/taboola/taboola_test.go b/adapters/taboola/taboola_test.go
index 9233a2cf..b5e87f08 100644
--- a/adapters/taboola/taboola_test.go
+++ b/adapters/taboola/taboola_test.go
@@ -3,6 +3,7 @@ package taboola
 import (
 	"github.com/prebid/prebid-server/adapters/adapterstest"
 	"github.com/prebid/prebid-server/config"
+	"github.com/stretchr/testify/assert"
 	"testing"
 )
 
@@ -11,8 +12,8 @@ import (
 )
 
 func TestJsonSamples(t *testing.T) {
-	bidder, buildErr := Builder(openrtb_ext.BidderOutbrain, config.Adapter{
-		Endpoint: "http://whatever.com"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})
+	bidder, buildErr := Builder(openrtb_ext.BidderTaboola, config.Adapter{
+		Endpoint: "http://{{.MediaType}}.whatever.com/{{.Host}}/{{.PublisherID}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})
 
 	if buildErr != nil {
 		t.Fatalf("Builder returned unexpected error %v", buildErr)
@@ -20,3 +21,16 @@ func TestJsonSamples(t *testing.T) {
 
 	adapterstest.RunJSONBidderTest(t, "taboolatest", bidder)
 }
+
+func TestEmptyExternalUrl(t *testing.T) {
+	bidder, buildErr := Builder(openrtb_ext.BidderTaboola, config.Adapter{
+		Endpoint: "http://whatever.com"}, config.Server{})
+
+	if buildErr != nil {
+		t.Fatalf("Builder returned unexpected error %v", buildErr)
+	}
+
+	bidderTaboola := bidder.(*adapter)
+
+	assert.Equal(t, "", bidderTaboola.hostName)
+}
diff --git a/adapters/taboola/taboolatest/exemplary/banner.json b/adapters/taboola/taboolatest/exemplary/banner.json
index 13309f7a..a3f819b8 100644
--- a/adapters/taboola/taboolatest/exemplary/banner.json
+++ b/adapters/taboola/taboolatest/exemplary/banner.json
@@ -42,7 +42,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://whatever.com/publisher-id",
+        "uri": "http://display.whatever.com/hosturl.com/publisher-id",
         "body": {
           "id": "request-id",
           "imp": [
diff --git a/adapters/taboola/taboolatest/exemplary/multiFormatImpressionsRequest.json b/adapters/taboola/taboolatest/exemplary/multiFormatImpressionsRequest.json
new file mode 100644
index 00000000..f717d77f
--- /dev/null
+++ b/adapters/taboola/taboolatest/exemplary/multiFormatImpressionsRequest.json
@@ -0,0 +1,345 @@
+{
+  "mockBidRequest": {
+    "id": "request-id",
+    "imp": [
+      {
+        "id": "native-impression-id",
+        "native": {
+          "request": "\"{\\\"ver\\\":\\\"1.1\\\",\\\"plcmttype\\\": 4,\\\"plcmtcnt\\\": 1,\\\"assets\\\":[{\\\"id\\\": 1,\\\"required\\\": 1,\\\"title\\\":{\\\"len\\\": 140}},{\\\"id\\\": 4,\\\"required\\\": 1,\\\"img\\\":{\\\"type\\\": 3,\\\"wmin\\\": 600,\\\"hmin\\\": 315}},{\\\"id\\\": 2,\\\"required\\\": 0,\\\"img\\\":{\\\"type\\\": 1,\\\"wmin\\\": 320,\\\"hmin\\\": 180}},{\\\"id\\\": 6,\\\"required\\\": 1,\\\"data\\\":{\\\"type\\\": 2,\\\"len\\\": 205}},{\\\"id\\\": 5,\\\"required\\\": 1,\\\"data\\\":{\\\"type\\\": 1,\\\"len\\\": 140}}]}}\"",
+          "ver": "1.1"
+        },
+        "ext": {
+          "bidder": {
+            "publisherId": "publisher-id",
+            "tagid": "tag-id"
+          }
+        }
+      },
+      {
+        "id": "banner-impression-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            },
+            {
+              "w": 160,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "publisherId": "publisher-id",
+            "tagid": "tag-id"
+          }
+        }
+      },
+      {
+        "id": "banner-impression-id-2",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            },
+            {
+              "w": 160,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "publisherId": "publisher-id",
+            "tagid": "tag-id-2"
+          }
+        }
+      }
+    ],
+    "site": {
+      "domain": "http://domain.com",
+      "page": "http://page-domain.com",
+      "ref": "http://page-domain.com"
+    },
+    "device": {
+      "ua": "Mozilla/5.0 (Linux; Android 12; Pixel 6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.62 Mobile Safari/537.36",
+      "h": 300,
+      "w": 300
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://native.whatever.com/hosturl.com/publisher-id",
+        "body": {
+          "id": "request-id",
+          "imp": [
+            {
+              "id": "native-impression-id",
+              "native": {
+                "request": "\"{\\\"ver\\\":\\\"1.1\\\",\\\"plcmttype\\\": 4,\\\"plcmtcnt\\\": 1,\\\"assets\\\":[{\\\"id\\\": 1,\\\"required\\\": 1,\\\"title\\\":{\\\"len\\\": 140}},{\\\"id\\\": 4,\\\"required\\\": 1,\\\"img\\\":{\\\"type\\\": 3,\\\"wmin\\\": 600,\\\"hmin\\\": 315}},{\\\"id\\\": 2,\\\"required\\\": 0,\\\"img\\\":{\\\"type\\\": 1,\\\"wmin\\\": 320,\\\"hmin\\\": 180}},{\\\"id\\\": 6,\\\"required\\\": 1,\\\"data\\\":{\\\"type\\\": 2,\\\"len\\\": 205}},{\\\"id\\\": 5,\\\"required\\\": 1,\\\"data\\\":{\\\"type\\\": 1,\\\"len\\\": 140}}]}}\"",
+                "ver": "1.1"
+              },
+              "tagid": "tag-id",
+              "ext": {
+                "bidder": {
+                  "publisherId": "publisher-id",
+                  "tagid": "tag-id"
+                }
+              }
+            }
+          ],
+          "site": {
+            "id": "publisher-id",
+            "name": "publisher-id",
+            "domain": "http://domain.com",
+            "page": "http://page-domain.com",
+            "ref": "http://page-domain.com",
+            "publisher": {
+              "id": "publisher-id"
+            }
+          },
+          "device": {
+            "ua": "Mozilla/5.0 (Linux; Android 12; Pixel 6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.62 Mobile Safari/537.36",
+            "h": 300,
+            "w": 300
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "123",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "native-request-id",
+                  "impid": "native-impression-id",
+                  "price": 2.1,
+                  "adid": "1",
+                  "adm": "{}",
+                  "adomain": [
+                    "adomain.com"
+                  ],
+                  "cid": "cid",
+                  "crid": "crid",
+                  "w": 300,
+                  "h": 250,
+                  "exp": 60,
+                  "lurl": "https://lurl.domain.com/"
+                }
+              ],
+              "seat": "1111"
+            }
+          ],
+          "bidid": "123",
+          "cur": "USD"
+        }
+      }
+    },
+    {
+      "expectedRequest": {
+        "uri": "http://display.whatever.com/hosturl.com/publisher-id",
+        "body": {
+          "id": "request-id",
+          "imp": [
+            {
+              "id": "banner-impression-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  },
+                  {
+                    "w": 300,
+                    "h": 600
+                  },
+                  {
+                    "w": 160,
+                    "h": 600
+                  }
+                ]
+              },
+              "tagid" : "tag-id",
+              "ext": {
+                "bidder": {
+                  "publisherId": "publisher-id",
+                  "tagid": "tag-id"
+                }
+              }
+            },
+            {
+              "id": "banner-impression-id-2",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  },
+                  {
+                    "w": 300,
+                    "h": 600
+                  },
+                  {
+                    "w": 160,
+                    "h": 600
+                  }
+                ]
+              },
+              "tagid" : "tag-id-2",
+              "ext": {
+                "bidder": {
+                  "publisherId": "publisher-id",
+                  "tagid": "tag-id-2"
+                }
+              }
+            }
+          ],
+          "site": {
+            "id": "publisher-id",
+            "name": "publisher-id",
+            "domain": "http://domain.com",
+            "page": "http://page-domain.com",
+            "ref": "http://page-domain.com",
+            "publisher": {
+              "id": "publisher-id"
+            }
+          },
+          "device": {
+            "ua": "Mozilla/5.0 (Linux; Android 12; Pixel 6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.62 Mobile Safari/537.36",
+            "h": 300,
+            "w": 300
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "1234",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "banner-request-id",
+                  "impid": "banner-impression-id",
+                  "price": 3.5,
+                  "adid": "1",
+                  "adm": "<hrml></html>",
+                  "adomain": [
+                    "adomain.com"
+                  ],
+                  "cid": "cid",
+                  "crid": "crid",
+                  "w": 300,
+                  "h": 250,
+                  "exp": 60,
+                  "lurl": "https://lurl.domain.com/"
+                },
+                {
+                  "id": "banner-request-id-2",
+                  "impid": "banner-impression-id-2",
+                  "price": 4.2,
+                  "adid": "1",
+                  "adm": "<hrml></html>",
+                  "adomain": [
+                    "adomain.com"
+                  ],
+                  "cid": "cid",
+                  "crid": "crid",
+                  "w": 300,
+                  "h": 250,
+                  "exp": 60,
+                  "lurl": "https://lurl.domain.com/"
+                }
+              ],
+              "seat": "1111"
+            }
+          ],
+          "bidid": "123",
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "native-request-id",
+            "impid": "native-impression-id",
+            "price": 2.1,
+            "adid": "1",
+            "adm": "{\"link\":{\"url\":\"\"}}",
+            "adomain": [
+              "adomain.com"
+            ],
+            "cid": "cid",
+            "crid": "crid",
+            "w": 300,
+            "h": 250,
+            "exp": 60,
+            "lurl": "https://lurl.domain.com/"
+          },
+          "type": "native"
+        }
+      ]
+    },
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "banner-request-id",
+            "impid": "banner-impression-id",
+            "price": 3.5,
+            "adid": "1",
+            "adm": "<hrml></html>",
+            "adomain": [
+              "adomain.com"
+            ],
+            "cid": "cid",
+            "crid": "crid",
+            "w": 300,
+            "h": 250,
+            "exp": 60,
+            "lurl": "https://lurl.domain.com/"
+          },
+          "type": "banner"
+        },
+        {
+          "bid": {
+            "id": "banner-request-id-2",
+            "impid": "banner-impression-id-2",
+            "price": 4.2,
+            "adid": "1",
+            "adm": "<hrml></html>",
+            "adomain": [
+              "adomain.com"
+            ],
+            "cid": "cid",
+            "crid": "crid",
+            "w": 300,
+            "h": 250,
+            "exp": 60,
+            "lurl": "https://lurl.domain.com/"
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/taboola/taboolatest/exemplary/native.json b/adapters/taboola/taboolatest/exemplary/native.json
new file mode 100644
index 00000000..6aa7a13b
--- /dev/null
+++ b/adapters/taboola/taboolatest/exemplary/native.json
@@ -0,0 +1,123 @@
+{
+  "mockBidRequest": {
+    "id": "request-id",
+    "imp": [
+      {
+        "id": "impression-id",
+        "native": {
+          "request": "\"{\\\"ver\\\":\\\"1.1\\\",\\\"plcmttype\\\": 4,\\\"plcmtcnt\\\": 1,\\\"assets\\\":[{\\\"id\\\": 1,\\\"required\\\": 1,\\\"title\\\":{\\\"len\\\": 140}},{\\\"id\\\": 4,\\\"required\\\": 1,\\\"img\\\":{\\\"type\\\": 3,\\\"wmin\\\": 600,\\\"hmin\\\": 315}},{\\\"id\\\": 6,\\\"required\\\": 1,\\\"data\\\":{\\\"type\\\": 2,\\\"len\\\": 205}},{\\\"id\\\": 5,\\\"required\\\": 1,\\\"data\\\":{\\\"type\\\": 1,\\\"len\\\": 140}}]}}\"",
+          "ver": "1.1"
+        },
+        "ext": {
+          "bidder": {
+            "publisherId": "publisher-id",
+            "tagid": "tag-id"
+          }
+        }
+      }
+    ],
+    "site": {
+      "domain": "http://domain.com",
+      "page": "http://page-domain.com",
+      "ref": "http://page-domain.com"
+    },
+    "device": {
+      "ua": "Mozilla/5.0 (Linux; Android 12; Pixel 6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.62 Mobile Safari/537.36",
+      "h": 300,
+      "w": 300
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://native.whatever.com/hosturl.com/publisher-id",
+        "body": {
+          "id": "request-id",
+          "imp": [
+            {
+              "id": "impression-id",
+              "native": {
+                "request": "\"{\\\"ver\\\":\\\"1.1\\\",\\\"plcmttype\\\": 4,\\\"plcmtcnt\\\": 1,\\\"assets\\\":[{\\\"id\\\": 1,\\\"required\\\": 1,\\\"title\\\":{\\\"len\\\": 140}},{\\\"id\\\": 4,\\\"required\\\": 1,\\\"img\\\":{\\\"type\\\": 3,\\\"wmin\\\": 600,\\\"hmin\\\": 315}},{\\\"id\\\": 6,\\\"required\\\": 1,\\\"data\\\":{\\\"type\\\": 2,\\\"len\\\": 205}},{\\\"id\\\": 5,\\\"required\\\": 1,\\\"data\\\":{\\\"type\\\": 1,\\\"len\\\": 140}}]}}\"",
+                "ver": "1.1"
+              },
+              "tagid": "tag-id",
+              "ext": {
+                "bidder": {
+                  "publisherId": "publisher-id",
+                  "tagid": "tag-id"
+                }
+              }
+            }
+          ],
+          "site": {
+            "id": "publisher-id",
+            "name": "publisher-id",
+            "domain": "http://domain.com",
+            "page": "http://page-domain.com",
+            "ref": "http://page-domain.com",
+            "publisher": {
+              "id": "publisher-id"
+            }
+          },
+          "device": {
+            "ua": "Mozilla/5.0 (Linux; Android 12; Pixel 6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.62 Mobile Safari/537.36",
+            "h": 300,
+            "w": 300
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "123",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "request-id",
+                  "impid": "impression-id",
+                  "price": 2.1,
+                  "adid": "1",
+                  "adm": "{\"ver\":\"1.2\",\"assets\":[{\"id\":7,\"required\":1,\"img\":{\"url\":\"http://image.taboola.com\",\"w\":600,\"h\":315}},{\"id\":0,\"required\":1,\"title\":{\"text\":\"Title For Example\"}},{\"id\":5,\"data\":{\"value\":\"Test sponsor\"}}],\"link\":{\"url\":\"http://clickUrl.com\"},\"eventtrackers\":[{\"event\":1,\"method\":1,\"url\":\"http://tracker.com\"},{\"event\":1,\"method\":2,\"url\":\"http://tracker-2.com\"}]}",
+                  "adomain": [
+                    "adomain.com"
+                  ],
+                  "cid": "cid",
+                  "crid": "crid",
+                  "lurl": "https://lurl.domain.com/"
+                }
+              ],
+              "seat": "1111"
+            }
+          ],
+          "bidid": "123",
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "request-id",
+            "impid": "impression-id",
+            "price": 2.1,
+            "adid": "1",
+            "adm": "{\"ver\":\"1.2\",\"assets\":[{\"id\":7,\"required\":1,\"img\":{\"url\":\"http://image.taboola.com\",\"w\":600,\"h\":315}},{\"id\":0,\"required\":1,\"title\":{\"text\":\"Title For Example\"}},{\"id\":5,\"data\":{\"value\":\"Test sponsor\"}}],\"link\":{\"url\":\"http://clickUrl.com\"},\"imptrackers\":[\"http://tracker.com\"],\"jstracker\":\"\\u003cscript src=\\\"http://tracker-2.com\\\"\\u003e\\u003c/script\\u003e\"}",
+
+          "adomain": [
+              "adomain.com"
+            ],
+            "cid": "cid",
+            "crid": "crid",
+            "lurl": "https://lurl.domain.com/"
+          },
+          "type": "native"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/taboola/taboolatest/supplemental/bidParamsOverrideRequestFields.json b/adapters/taboola/taboolatest/supplemental/bidParamsOverrideRequestFields.json
index 267b5173..56d790db 100644
--- a/adapters/taboola/taboolatest/supplemental/bidParamsOverrideRequestFields.json
+++ b/adapters/taboola/taboolatest/supplemental/bidParamsOverrideRequestFields.json
@@ -45,7 +45,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://whatever.com/publisher-id",
+        "uri": "http://display.whatever.com/hosturl.com/publisher-id",
         "body": {
           "id": "request-id",
           "imp": [
diff --git a/adapters/taboola/taboolatest/supplemental/bidderServerError.json b/adapters/taboola/taboolatest/supplemental/bidderServerError.json
index 1e27aed7..5c1670fb 100644
--- a/adapters/taboola/taboolatest/supplemental/bidderServerError.json
+++ b/adapters/taboola/taboolatest/supplemental/bidderServerError.json
@@ -42,7 +42,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://whatever.com/publisher-id",
+        "uri": "http://display.whatever.com/hosturl.com/publisher-id",
         "body": {
           "id": "request-id",
           "imp": [
diff --git a/adapters/taboola/taboolatest/supplemental/emptyReponseFromBidder.json b/adapters/taboola/taboolatest/supplemental/emptyReponseFromBidder.json
index 31241873..435f5d4d 100644
--- a/adapters/taboola/taboolatest/supplemental/emptyReponseFromBidder.json
+++ b/adapters/taboola/taboolatest/supplemental/emptyReponseFromBidder.json
@@ -42,7 +42,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://whatever.com/publisher-id",
+        "uri": "http://display.whatever.com/hosturl.com/publisher-id",
         "body": {
           "id": "request-id",
           "imp": [
diff --git a/adapters/taboola/taboolatest/supplemental/emptySiteInRequest.json b/adapters/taboola/taboolatest/supplemental/emptySiteInRequest.json
index 76d663bf..fafdd8a7 100644
--- a/adapters/taboola/taboolatest/supplemental/emptySiteInRequest.json
+++ b/adapters/taboola/taboolatest/supplemental/emptySiteInRequest.json
@@ -40,7 +40,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://whatever.com/publisher-id",
+        "uri": "http://display.whatever.com/hosturl.com/publisher-id",
         "body": {
           "id": "request-id",
           "imp": [
diff --git a/adapters/taboola/taboolatest/supplemental/incorrectResponseImpMapping.json b/adapters/taboola/taboolatest/supplemental/incorrectResponseImpMapping.json
index e32ae471..18500f3e 100644
--- a/adapters/taboola/taboolatest/supplemental/incorrectResponseImpMapping.json
+++ b/adapters/taboola/taboolatest/supplemental/incorrectResponseImpMapping.json
@@ -42,7 +42,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://whatever.com/publisher-id",
+        "uri": "http://display.whatever.com/hosturl.com/publisher-id",
         "body": {
           "id": "request-id",
           "imp": [
@@ -125,7 +125,7 @@
   ],
   "expectedMakeBidsErrors": [
     {
-      "value": "Failed to find banner impression \"incorrect-impression-id\" ",
+      "value": "Failed to find banner/native impression \"incorrect-impression-id\" ",
       "comparison": "literal"
     }
   ]
diff --git a/adapters/taboola/taboolatest/supplemental/multiImpressionsRequest.json b/adapters/taboola/taboolatest/supplemental/multiImpressionsRequest.json
index 03c3532e..720398f9 100644
--- a/adapters/taboola/taboolatest/supplemental/multiImpressionsRequest.json
+++ b/adapters/taboola/taboolatest/supplemental/multiImpressionsRequest.json
@@ -51,7 +51,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://whatever.com/publisher-id",
+        "uri": "http://display.whatever.com/hosturl.com/publisher-id",
         "body": {
           "id": "request-id",
           "imp": [
diff --git a/adapters/taboola/taboolatest/supplemental/optionalParamsProvided.json b/adapters/taboola/taboolatest/supplemental/optionalParamsProvided.json
index 17103882..580c7d28 100644
--- a/adapters/taboola/taboolatest/supplemental/optionalParamsProvided.json
+++ b/adapters/taboola/taboolatest/supplemental/optionalParamsProvided.json
@@ -46,7 +46,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://whatever.com/publisher-id",
+        "uri": "http://display.whatever.com/hosturl.com/publisher-id",
         "body": {
           "id": "request-id",
           "imp": [
diff --git a/adapters/taboola/taboolatest/supplemental/unexpectedStatusCode.json b/adapters/taboola/taboolatest/supplemental/unexpectedStatusCode.json
index 3e4cd70f..706a0ac2 100644
--- a/adapters/taboola/taboolatest/supplemental/unexpectedStatusCode.json
+++ b/adapters/taboola/taboolatest/supplemental/unexpectedStatusCode.json
@@ -42,7 +42,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://whatever.com/publisher-id",
+        "uri": "http://display.whatever.com/hosturl.com/publisher-id",
         "body": {
           "id": "request-id",
           "imp": [
diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go
index e4237b52..94bed36e 100644
--- a/endpoints/cookie_sync_test.go
+++ b/endpoints/cookie_sync_test.go
@@ -21,6 +21,7 @@ import (
 	"github.com/prebid/prebid-server/privacy"
 	"github.com/prebid/prebid-server/privacy/ccpa"
 	gdprPrivacy "github.com/prebid/prebid-server/privacy/gdpr"
+	gppPrivacy "github.com/prebid/prebid-server/privacy/gpp"
 	"github.com/prebid/prebid-server/usersync"
 
 	"github.com/stretchr/testify/assert"
@@ -324,6 +325,8 @@ func TestCookieSyncParseRequest(t *testing.T) {
 				`"gdpr":1,` +
 				`"gdpr_consent":"anyGDPRConsent",` +
 				`"us_privacy":"1NYN",` +
+				`"gpp":"anyGPPString",` +
+				`"gpp_sid":"2",` +
 				`"limit":42,` +
 				`"coopSync":true,` +
 				`"filterSettings":{"iframe":{"bidders":"*","filter":"include"}, "image":{"bidders":["b"],"filter":"exclude"}}` +
@@ -344,6 +347,10 @@ func TestCookieSyncParseRequest(t *testing.T) {
 				CCPA: ccpa.Policy{
 					Consent: "1NYN",
 				},
+				GPP: gppPrivacy.Policy{
+					Consent: "anyGPPString",
+					RawSID:  "2",
+				},
 			},
 			expectedRequest: usersync.Request{
 				Bidders: []string{"a", "b"},
diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go
index 80544874..5b125fc5 100644
--- a/endpoints/openrtb2/amp_auction_test.go
+++ b/endpoints/openrtb2/amp_auction_test.go
@@ -9,13 +9,15 @@ import (
 	"net/http/httptest"
 	"net/url"
 	"os"
-	"reflect"
 	"strconv"
 	"testing"
 	"time"
 
 	"github.com/julienschmidt/httprouter"
 	"github.com/prebid/openrtb/v17/openrtb2"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
 	"github.com/prebid/prebid-server/amp"
 	"github.com/prebid/prebid-server/analytics"
 	analyticsConf "github.com/prebid/prebid-server/analytics/config"
@@ -28,8 +30,6 @@ import (
 	metricsConfig "github.com/prebid/prebid-server/metrics/config"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher"
-
-	"github.com/stretchr/testify/assert"
 )
 
 // TestGoodRequests makes sure that the auction runs properly-formatted stored bids correctly.
@@ -1108,30 +1108,88 @@ func TestAmpDebug(t *testing.T) {
 	}
 }
 
-// Prevents #452
-func TestAmpTargetingDefaults(t *testing.T) {
-	req := &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}
-	if err := defaultRequestExt(req); err != nil {
-		t.Fatalf("Unexpected error defaulting request.ext for AMP: %v", err)
+func TestInitAmpTargetingAndCache(t *testing.T) {
+	trueVal := true
+	emptyTargetingAndCache := &openrtb_ext.ExtRequestPrebid{
+		Targeting: &openrtb_ext.ExtRequestTargeting{},
+		Cache: &openrtb_ext.ExtRequestPrebidCache{
+			Bids: &openrtb_ext.ExtRequestPrebidCacheBids{},
+		},
 	}
 
-	assert.NoError(t, req.RebuildRequest())
-
-	var extRequest openrtb_ext.ExtRequest
-	if err := json.Unmarshal(req.Ext, &extRequest); err != nil {
-		t.Fatalf("Unexpected error unmarshalling defaulted request.ext for AMP: %v", err)
-	}
-	if extRequest.Prebid.Targeting == nil {
-		t.Fatal("AMP defaults should set request.ext.targeting")
-	}
-	if !extRequest.Prebid.Targeting.IncludeWinners {
-		t.Error("AMP defaults should set request.ext.targeting.includewinners to true")
-	}
-	if !extRequest.Prebid.Targeting.IncludeBidderKeys {
-		t.Error("AMP defaults should set request.ext.targeting.includebidderkeys to true")
+	testCases := []struct {
+		name           string
+		request        *openrtb2.BidRequest
+		expectedPrebid *openrtb_ext.ExtRequestPrebid
+		expectedErrs   []string
+	}{
+		{
+			name:         "malformed",
+			request:      &openrtb2.BidRequest{Ext: json.RawMessage("malformed")},
+			expectedErrs: []string{"invalid character 'm' looking for beginning of value"},
+		},
+		{
+			name:           "nil",
+			request:        &openrtb2.BidRequest{},
+			expectedPrebid: emptyTargetingAndCache,
+		},
+		{
+			name:           "empty",
+			request:        &openrtb2.BidRequest{Ext: json.RawMessage(`{"ext":{}}`)},
+			expectedPrebid: emptyTargetingAndCache,
+		},
+		{
+			name:           "missing targeting + cache",
+			request:        &openrtb2.BidRequest{Ext: json.RawMessage(`{"ext":{"prebid":{}}}`)},
+			expectedPrebid: emptyTargetingAndCache,
+		},
+		{
+			name:    "missing targeting",
+			request: &openrtb2.BidRequest{Ext: json.RawMessage(`{"prebid":{"cache":{"bids":{"returnCreative":true}}}}`)},
+			expectedPrebid: &openrtb_ext.ExtRequestPrebid{
+				Targeting: &openrtb_ext.ExtRequestTargeting{},
+				Cache: &openrtb_ext.ExtRequestPrebidCache{
+					Bids: &openrtb_ext.ExtRequestPrebidCacheBids{
+						ReturnCreative: &trueVal,
+					},
+				},
+			},
+		},
+		{
+			name:    "missing cache",
+			request: &openrtb2.BidRequest{Ext: json.RawMessage(`{"prebid":{"targeting":{"includewinners":true}}}`)},
+			expectedPrebid: &openrtb_ext.ExtRequestPrebid{
+				Targeting: &openrtb_ext.ExtRequestTargeting{
+					IncludeWinners: &trueVal,
+				},
+				Cache: &openrtb_ext.ExtRequestPrebidCache{
+					Bids: &openrtb_ext.ExtRequestPrebidCacheBids{},
+				},
+			},
+		},
 	}
-	if !reflect.DeepEqual(extRequest.Prebid.Targeting.PriceGranularity, openrtb_ext.PriceGranularityFromString("med")) {
-		t.Error("AMP defaults should set request.ext.targeting.pricegranularity to medium")
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			// setup
+			req := &openrtb_ext.RequestWrapper{BidRequest: tc.request}
+
+			// run
+			actualErrs := initAmpTargetingAndCache(req)
+
+			// assertions
+			require.NoError(t, req.RebuildRequest(), "rebuild request")
+
+			actualErrsMsgs := make([]string, len(actualErrs))
+			for i, v := range actualErrs {
+				actualErrsMsgs[i] = v.Error()
+			}
+			assert.ElementsMatch(t, tc.expectedErrs, actualErrsMsgs, "errors")
+
+			actualReqExt, _ := req.GetRequestExt()
+			actualPrebid := actualReqExt.GetPrebid()
+			assert.Equal(t, tc.expectedPrebid, actualPrebid, "prebid ext")
+		})
 	}
 }
 
@@ -1657,7 +1715,7 @@ func TestBuildAmpObject(t *testing.T) {
 					},
 					AT:   1,
 					TMax: 500,
-					Ext:  json.RawMessage(`{"prebid":{"cache":{"bids":{"returnCreative":null},"vastxml":null},"channel":{"name":"amp","version":""},"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":20,"increment":0.1}]},"includewinners":true,"includebidderkeys":true,"includebrandcategory":null,"includeformat":false,"durationrangesec":null,"preferdeals":false}}}`),
+					Ext:  json.RawMessage(`{"prebid":{"cache":{"bids":{}},"channel":{"name":"amp","version":""},"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":20,"increment":0.1}]},"includewinners":true,"includebidderkeys":true}}}`),
 				},
 				AuctionResponse: &openrtb2.BidResponse{
 					SeatBid: []openrtb2.SeatBid{{
@@ -1711,7 +1769,7 @@ func TestBuildAmpObject(t *testing.T) {
 					},
 					AT:   1,
 					TMax: 500,
-					Ext:  json.RawMessage(`{"prebid":{"cache":{"bids":{"returnCreative":null},"vastxml":null},"channel":{"name":"amp","version":""},"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":20,"increment":0.1}]},"includewinners":true,"includebidderkeys":true,"includebrandcategory":null,"includeformat":false,"durationrangesec":null,"preferdeals":false}}}`),
+					Ext:  json.RawMessage(`{"prebid":{"cache":{"bids":{}},"channel":{"name":"amp","version":""},"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":20,"increment":0.1}]},"includewinners":true,"includebidderkeys":true}}}`),
 				},
 				AuctionResponse: &openrtb2.BidResponse{
 					SeatBid: []openrtb2.SeatBid{{
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index 16ae06bb..6449d770 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -36,6 +36,7 @@ import (
 	"github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher"
 	"github.com/prebid/prebid-server/stored_responses"
 	"github.com/prebid/prebid-server/util/iputil"
+	"github.com/prebid/prebid-server/util/ptrutil"
 )
 
 func TestJsonSampleRequests(t *testing.T) {
@@ -135,7 +136,7 @@ func TestJsonSampleRequests(t *testing.T) {
 
 					auctionEndpointHandler, _, mockBidServers, mockCurrencyRatesServer, err := buildTestEndpoint(test, cfg)
 					if assert.NoError(t, err) {
-						runTestCase(t, auctionEndpointHandler, test, fileData, testFile)
+						assert.NotPanics(t, func() { runTestCase(t, auctionEndpointHandler, test, fileData, testFile) }, testFile)
 					}
 
 					// Close servers regardless if the test case was run or not
@@ -1655,49 +1656,54 @@ func TestValidateRequestExt(t *testing.T) {
 			givenRequestExt: nil,
 		},
 		{
-			description:     "empty",
+			description:     "prebid - nil",
 			givenRequestExt: json.RawMessage(`{}`),
 		},
 		{
 			description:     "prebid - empty",
-			givenRequestExt: json.RawMessage(`{"prebid": {}}`),
+			givenRequestExt: json.RawMessage(`{"prebid":{}}`),
 		},
 		{
 			description:     "prebid cache - empty",
-			givenRequestExt: json.RawMessage(`{"prebid": {"cache": {}}}`),
+			givenRequestExt: json.RawMessage(`{"prebid":{"cache":{}}}`),
 			expectedError:   `request.ext is invalid: request.ext.prebid.cache requires one of the "bids" or "vastxml" properties`,
 		},
 		{
 			description:     "prebid cache - bids - null",
-			givenRequestExt: json.RawMessage(`{"prebid": {"cache": {"bids": null}}}`),
+			givenRequestExt: json.RawMessage(`{"prebid":{"cache":{"bids":null}}}`),
 			expectedError:   `request.ext is invalid: request.ext.prebid.cache requires one of the "bids" or "vastxml" properties`,
 		},
 		{
 			description:     "prebid cache - bids - wrong type",
-			givenRequestExt: json.RawMessage(`{"prebid": {"cache": {"bids": true}}}`),
+			givenRequestExt: json.RawMessage(`{"prebid":{"cache":{"bids":true}}}`),
 			expectedError:   `json: cannot unmarshal bool into Go struct field ExtRequestPrebidCache.cache.bids of type openrtb_ext.ExtRequestPrebidCacheBids`,
 		},
 		{
 			description:     "prebid cache - bids - provided",
-			givenRequestExt: json.RawMessage(`{"prebid": {"cache": {"bids": {}}}}`),
+			givenRequestExt: json.RawMessage(`{"prebid":{"cache":{"bids":{}}}}`),
 		},
 		{
 			description:     "prebid cache - vastxml - null",
-			givenRequestExt: json.RawMessage(`{"prebid": {"cache": {"vastxml": null}}}`),
+			givenRequestExt: json.RawMessage(`{"prebid":{"cache":{"vastxml":null}}}`),
 			expectedError:   `request.ext is invalid: request.ext.prebid.cache requires one of the "bids" or "vastxml" properties`,
 		},
 		{
 			description:     "prebid cache - vastxml - wrong type",
-			givenRequestExt: json.RawMessage(`{"prebid": {"cache": {"vastxml": true}}}`),
+			givenRequestExt: json.RawMessage(`{"prebid":{"cache":{"vastxml":true}}}`),
 			expectedError:   `json: cannot unmarshal bool into Go struct field ExtRequestPrebidCache.cache.vastxml of type openrtb_ext.ExtRequestPrebidCacheVAST`,
 		},
 		{
 			description:     "prebid cache - vastxml - provided",
-			givenRequestExt: json.RawMessage(`{"prebid": {"cache": {"vastxml": {}}}}`),
+			givenRequestExt: json.RawMessage(`{"prebid":{"cache":{"vastxml":{}}}}`),
 		},
 		{
 			description:     "prebid cache - bids + vastxml - provided",
-			givenRequestExt: json.RawMessage(`{"prebid": {"cache": {"bids": {}, "vastxml": {}}}}`),
+			givenRequestExt: json.RawMessage(`{"prebid":{"cache":{"bids":{},"vastxml":{}}}}`),
+		},
+		{
+			description:     "prebid targeting", // test integration with validateTargeting
+			givenRequestExt: json.RawMessage(`{"prebid":{"targeting":{}}}`),
+			expectedError:   "ext.prebid.targeting: At least one of includewinners or includebidderkeys must be enabled to enable targeting support",
 		},
 	}
 
@@ -1713,6 +1719,146 @@ func TestValidateRequestExt(t *testing.T) {
 	}
 }
 
+func TestValidateTargeting(t *testing.T) {
+	testCases := []struct {
+		name           string
+		givenTargeting *openrtb_ext.ExtRequestTargeting
+		expectedError  error
+	}{
+		{
+			name:           "nil",
+			givenTargeting: nil,
+			expectedError:  nil,
+		},
+		{
+			name:           "empty",
+			givenTargeting: &openrtb_ext.ExtRequestTargeting{},
+			expectedError:  errors.New("ext.prebid.targeting: At least one of includewinners or includebidderkeys must be enabled to enable targeting support"),
+		},
+		{
+			name: "includewinners nil, includebidderkeys false",
+			givenTargeting: &openrtb_ext.ExtRequestTargeting{
+				IncludeBidderKeys: ptrutil.ToPtr(false),
+			},
+			expectedError: errors.New("ext.prebid.targeting: At least one of includewinners or includebidderkeys must be enabled to enable targeting support"),
+		},
+		{
+			name: "includewinners nil, includebidderkeys true",
+			givenTargeting: &openrtb_ext.ExtRequestTargeting{
+				IncludeBidderKeys: ptrutil.ToPtr(true),
+			},
+			expectedError: nil,
+		},
+		{
+			name: "includewinners false, includebidderkeys nil",
+			givenTargeting: &openrtb_ext.ExtRequestTargeting{
+				IncludeWinners: ptrutil.ToPtr(false),
+			},
+			expectedError: errors.New("ext.prebid.targeting: At least one of includewinners or includebidderkeys must be enabled to enable targeting support"),
+		},
+		{
+			name: "includewinners true, includebidderkeys nil",
+			givenTargeting: &openrtb_ext.ExtRequestTargeting{
+				IncludeWinners: ptrutil.ToPtr(true),
+			},
+			expectedError: nil,
+		},
+		{
+			name: "all false",
+			givenTargeting: &openrtb_ext.ExtRequestTargeting{
+				IncludeWinners:    ptrutil.ToPtr(false),
+				IncludeBidderKeys: ptrutil.ToPtr(false),
+			},
+			expectedError: errors.New("ext.prebid.targeting: At least one of includewinners or includebidderkeys must be enabled to enable targeting support"),
+		},
+		{
+			name: "includewinners false, includebidderkeys true",
+			givenTargeting: &openrtb_ext.ExtRequestTargeting{
+				IncludeWinners:    ptrutil.ToPtr(false),
+				IncludeBidderKeys: ptrutil.ToPtr(true),
+			},
+			expectedError: nil,
+		},
+		{
+			name: "includewinners false, includebidderkeys true",
+			givenTargeting: &openrtb_ext.ExtRequestTargeting{
+				IncludeWinners:    ptrutil.ToPtr(true),
+				IncludeBidderKeys: ptrutil.ToPtr(false),
+			},
+			expectedError: nil,
+		},
+		{
+			name: "includewinners true, includebidderkeys true",
+			givenTargeting: &openrtb_ext.ExtRequestTargeting{
+				IncludeWinners:    ptrutil.ToPtr(true),
+				IncludeBidderKeys: ptrutil.ToPtr(true),
+			},
+			expectedError: nil,
+		},
+		{
+			name: "price granularity empty",
+			givenTargeting: &openrtb_ext.ExtRequestTargeting{
+				IncludeWinners:   ptrutil.ToPtr(true),
+				PriceGranularity: &openrtb_ext.PriceGranularity{},
+			},
+			expectedError: errors.New("Price granularity error: precision is required"),
+		},
+		{
+			name: "price granularity negative",
+			givenTargeting: &openrtb_ext.ExtRequestTargeting{
+				IncludeWinners: ptrutil.ToPtr(true),
+				PriceGranularity: &openrtb_ext.PriceGranularity{
+					Precision: ptrutil.ToPtr(-1),
+				},
+			},
+			expectedError: errors.New("Price granularity error: precision must be non-negative"),
+		},
+		{
+			name: "price granularity greater than max",
+			givenTargeting: &openrtb_ext.ExtRequestTargeting{
+				IncludeWinners: ptrutil.ToPtr(true),
+				PriceGranularity: &openrtb_ext.PriceGranularity{
+					Precision: ptrutil.ToPtr(openrtb_ext.MaxDecimalFigures + 1),
+				},
+			},
+			expectedError: errors.New("Price granularity error: precision of more than 15 significant figures is not supported"),
+		},
+		{
+			name: "price granularity ranges out of order",
+			givenTargeting: &openrtb_ext.ExtRequestTargeting{
+				IncludeWinners: ptrutil.ToPtr(true),
+				PriceGranularity: &openrtb_ext.PriceGranularity{
+					Precision: ptrutil.ToPtr(2),
+					Ranges: []openrtb_ext.GranularityRange{
+						{Min: 1.0, Max: 2.0, Increment: 0.2},
+						{Min: 0.0, Max: 1.0, Increment: 0.5},
+					},
+				},
+			},
+			expectedError: errors.New(`Price granularity error: range list must be ordered with increasing "max"`),
+		},
+		{
+			name: "price granularity negative increment",
+			givenTargeting: &openrtb_ext.ExtRequestTargeting{
+				IncludeWinners: ptrutil.ToPtr(true),
+				PriceGranularity: &openrtb_ext.PriceGranularity{
+					Precision: ptrutil.ToPtr(2),
+					Ranges: []openrtb_ext.GranularityRange{
+						{Min: 0.0, Max: 1.0, Increment: -0.1},
+					},
+				},
+			},
+			expectedError: errors.New("Price granularity error: increment must be a nonzero positive number"),
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			assert.Equal(t, tc.expectedError, validateTargeting(tc.givenTargeting), "Targeting")
+		})
+	}
+}
+
 func TestValidateOrFillChannel(t *testing.T) {
 	testCases := []struct {
 		description           string
diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go
index 4e8fe2f7..2eb3fcd3 100644
--- a/endpoints/openrtb2/video_auction_test.go
+++ b/endpoints/openrtb2/video_auction_test.go
@@ -22,11 +22,13 @@ import (
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/prebid_cache_client"
 	"github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher"
+	"github.com/prebid/prebid-server/util/ptrutil"
 
 	"github.com/prebid/openrtb/v17/adcom1"
 	"github.com/prebid/openrtb/v17/openrtb2"
 	gometrics "github.com/rcrowley/go-metrics"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestVideoEndpointImpressionsNumber(t *testing.T) {
@@ -78,7 +80,8 @@ func TestVideoEndpointImpressionsDuration(t *testing.T) {
 
 	var extData openrtb_ext.ExtRequest
 	json.Unmarshal(ex.lastRequest.Ext, &extData)
-	assert.True(t, extData.Prebid.Targeting.IncludeBidderKeys, "Request ext incorrect: IncludeBidderKeys should be true ")
+	assert.NotNil(t, extData.Prebid.Targeting.IncludeBidderKeys, "Request ext incorrect: IncludeBidderKeys should be true ")
+	assert.True(t, *extData.Prebid.Targeting.IncludeBidderKeys, "Request ext incorrect: IncludeBidderKeys should be true ")
 
 	assert.Len(t, ex.lastRequest.Imp, 22, "Incorrect number of impressions in request")
 	assert.Equal(t, ex.lastRequest.Imp[0].ID, "1_0", "Incorrect impression id in request")
@@ -121,8 +124,8 @@ func TestCreateBidExtension(t *testing.T) {
 			DurationRangeSec:     durationRange,
 			RequireExactDuration: false,
 		},
-		PriceGranularity: openrtb_ext.PriceGranularity{
-			Precision: 2,
+		PriceGranularity: &openrtb_ext.PriceGranularity{
+			Precision: ptrutil.ToPtr(2),
 			Ranges:    priceGranRanges,
 		},
 	}
@@ -138,37 +141,20 @@ func TestCreateBidExtension(t *testing.T) {
 	assert.Equal(t, resExt.Prebid.Targeting.PriceGranularity.Ranges, priceGranRanges, "Price granularity is incorrect")
 }
 
-func TestCreateBidExtensionExactDurTrueNoPriceRange(t *testing.T) {
-	durationRange := make([]int, 0)
-	durationRange = append(durationRange, 15)
-	durationRange = append(durationRange, 30)
+func TestCreateBidExtensionTargeting(t *testing.T) {
+	ex := &mockExchangeVideo{}
+	reqBody := readVideoTestFile(t, "sample-requests/video/video_valid_sample.json")
+	req := httptest.NewRequest("POST", "/openrtb2/video", strings.NewReader(reqBody))
+	recorder := httptest.NewRecorder()
 
-	translateCategories := false
-	videoRequest := openrtb_ext.BidRequestVideo{
-		IncludeBrandCategory: &openrtb_ext.IncludeBrandCategory{
-			PrimaryAdserver:     1,
-			Publisher:           "",
-			TranslateCategories: &translateCategories,
-		},
-		PodConfig: openrtb_ext.PodConfig{
-			DurationRangeSec:     durationRange,
-			RequireExactDuration: true,
-		},
-		PriceGranularity: openrtb_ext.PriceGranularity{
-			Precision: 0,
-			Ranges:    nil,
-		},
-	}
-	res, err := createBidExtension(&videoRequest)
-	assert.NoError(t, err, "Error should be nil")
+	deps := mockDeps(t, ex)
+	deps.VideoAuctionEndpoint(recorder, req, nil)
 
-	resExt := &openrtb_ext.ExtRequest{}
+	require.NotNil(t, ex.lastRequest, "The request never made it into the Exchange.")
 
-	if err := json.Unmarshal(res, &resExt); err != nil {
-		assert.Fail(t, "Unable to unmarshal bid extension")
-	}
-	assert.Equal(t, resExt.Prebid.Targeting.DurationRangeSec, []int(nil), "Duration range seconds is incorrect")
-	assert.Equal(t, resExt.Prebid.Targeting.PriceGranularity, openrtb_ext.PriceGranularityFromString("med"), "Price granularity is incorrect")
+	// assert targeting set to default
+	expectedRequestExt := `{"prebid":{"cache":{"vastxml":{}},"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":20,"increment":0.1}]},"includebidderkeys":true,"includewinners":true,"includebrandcategory":{"primaryadserver":1,"publisher":"","withcategory":true}}}}`
+	assert.JSONEq(t, expectedRequestExt, string(ex.lastRequest.Ext))
 }
 
 func TestVideoEndpointDebugQueryTrue(t *testing.T) {
@@ -1256,13 +1242,13 @@ func (m *mockAnalyticsModule) LogVideoObject(vo *analytics.VideoObject) {
 	m.videoObjects = append(m.videoObjects, vo)
 }
 
-func (m *mockAnalyticsModule) LogCookieSyncObject(cso *analytics.CookieSyncObject) { return }
+func (m *mockAnalyticsModule) LogCookieSyncObject(cso *analytics.CookieSyncObject) {}
 
-func (m *mockAnalyticsModule) LogSetUIDObject(so *analytics.SetUIDObject) { return }
+func (m *mockAnalyticsModule) LogSetUIDObject(so *analytics.SetUIDObject) {}
 
-func (m *mockAnalyticsModule) LogAmpObject(ao *analytics.AmpObject) { return }
+func (m *mockAnalyticsModule) LogAmpObject(ao *analytics.AmpObject) {}
 
-func (m *mockAnalyticsModule) LogNotificationEventObject(ne *analytics.NotificationEvent) { return }
+func (m *mockAnalyticsModule) LogNotificationEventObject(ne *analytics.NotificationEvent) {}
 
 func mockDeps(t *testing.T, ex *mockExchangeVideo) *endpointDeps {
 	return &endpointDeps{
@@ -1369,6 +1355,10 @@ type mockExchangeVideo struct {
 }
 
 func (m *mockExchangeVideo) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
+	if err := r.BidRequestWrapper.RebuildRequest(); err != nil {
+		return nil, err
+	}
+
 	m.lastRequest = r.BidRequestWrapper.BidRequest
 	if debugLog != nil && debugLog.Enabled {
 		m.cache.called = true
diff --git a/exchange/auction_test.go b/exchange/auction_test.go
index 9f335aa8..6518eda7 100644
--- a/exchange/auction_test.go
+++ b/exchange/auction_test.go
@@ -16,6 +16,7 @@ import (
 	"github.com/prebid/prebid-server/exchange/entities"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/prebid_cache_client"
+	"github.com/prebid/prebid-server/util/ptrutil"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -229,7 +230,7 @@ func runCacheSpec(t *testing.T, fileDisplayName string, specData *cacheSpec) {
 
 	targData := &targetData{
 		priceGranularity: openrtb_ext.PriceGranularity{
-			Precision: 2,
+			Precision: ptrutil.ToPtr(2),
 			Ranges: []openrtb_ext.GranularityRange{
 				{
 					Min:       0,
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index 34749ba6..3d374417 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -24,6 +24,7 @@ import (
 	"github.com/prebid/prebid-server/hooks"
 	"github.com/prebid/prebid-server/hooks/hookexecution"
 	"github.com/prebid/prebid-server/hooks/hookstage"
+	"github.com/prebid/prebid-server/util/ptrutil"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
 	jsonpatch "gopkg.in/evanphx/json-patch.v4"
@@ -2142,7 +2143,7 @@ func TestExchangeJSON(t *testing.T) {
 			t.Run(fileDisplayName, func(t *testing.T) {
 				specData, err := loadFile(fileName)
 				if assert.NoError(t, err, "Failed to load contents of file %s: %v", fileDisplayName, err) {
-					runSpec(t, fileDisplayName, specData)
+					assert.NotPanics(t, func() { runSpec(t, fileDisplayName, specData) }, fileDisplayName)
 				}
 			})
 		}
@@ -2508,7 +2509,7 @@ func (m *fakeRandomDeduplicateBidBooleanGenerator) Generate() bool {
 
 func newExtRequest() openrtb_ext.ExtRequest {
 	priceGran := openrtb_ext.PriceGranularity{
-		Precision: 2,
+		Precision: ptrutil.ToPtr(2),
 		Ranges: []openrtb_ext.GranularityRange{
 			{
 				Min:       0.0,
@@ -2522,8 +2523,8 @@ func newExtRequest() openrtb_ext.ExtRequest {
 	brandCat := openrtb_ext.ExtIncludeBrandCategory{PrimaryAdServer: 1, WithCategory: true, TranslateCategories: &translateCategories}
 
 	reqExt := openrtb_ext.ExtRequestTargeting{
-		PriceGranularity:     priceGran,
-		IncludeWinners:       true,
+		PriceGranularity:     &priceGran,
+		IncludeWinners:       ptrutil.ToPtr(true),
 		IncludeBrandCategory: &brandCat,
 	}
 
@@ -2536,7 +2537,7 @@ func newExtRequest() openrtb_ext.ExtRequest {
 
 func newExtRequestNoBrandCat() openrtb_ext.ExtRequest {
 	priceGran := openrtb_ext.PriceGranularity{
-		Precision: 2,
+		Precision: ptrutil.ToPtr(2),
 		Ranges: []openrtb_ext.GranularityRange{
 			{
 				Min:       0.0,
@@ -2549,8 +2550,8 @@ func newExtRequestNoBrandCat() openrtb_ext.ExtRequest {
 	brandCat := openrtb_ext.ExtIncludeBrandCategory{WithCategory: false}
 
 	reqExt := openrtb_ext.ExtRequestTargeting{
-		PriceGranularity:     priceGran,
-		IncludeWinners:       true,
+		PriceGranularity:     &priceGran,
+		IncludeWinners:       ptrutil.ToPtr(true),
 		IncludeBrandCategory: &brandCat,
 	}
 
@@ -2571,7 +2572,7 @@ func TestCategoryMapping(t *testing.T) {
 	requestExt := newExtRequest()
 
 	targData := &targetData{
-		priceGranularity: requestExt.Prebid.Targeting.PriceGranularity,
+		priceGranularity: *requestExt.Prebid.Targeting.PriceGranularity,
 		includeWinners:   true,
 	}
 
@@ -2627,7 +2628,7 @@ func TestCategoryMappingNoIncludeBrandCategory(t *testing.T) {
 	requestExt := newExtRequestNoBrandCat()
 
 	targData := &targetData{
-		priceGranularity: requestExt.Prebid.Targeting.PriceGranularity,
+		priceGranularity: *requestExt.Prebid.Targeting.PriceGranularity,
 		includeWinners:   true,
 	}
 	requestExt.Prebid.Targeting.DurationRangeSec = []int{15, 30, 40, 50}
@@ -2682,7 +2683,7 @@ func TestCategoryMappingTranslateCategoriesNil(t *testing.T) {
 	requestExt := newExtRequestTranslateCategories(nil)
 
 	targData := &targetData{
-		priceGranularity: requestExt.Prebid.Targeting.PriceGranularity,
+		priceGranularity: *requestExt.Prebid.Targeting.PriceGranularity,
 		includeWinners:   true,
 	}
 
@@ -2725,7 +2726,7 @@ func TestCategoryMappingTranslateCategoriesNil(t *testing.T) {
 
 func newExtRequestTranslateCategories(translateCategories *bool) openrtb_ext.ExtRequest {
 	priceGran := openrtb_ext.PriceGranularity{
-		Precision: 2,
+		Precision: ptrutil.ToPtr(2),
 		Ranges: []openrtb_ext.GranularityRange{
 			{
 				Min:       0.0,
@@ -2741,8 +2742,8 @@ func newExtRequestTranslateCategories(translateCategories *bool) openrtb_ext.Ext
 	}
 
 	reqExt := openrtb_ext.ExtRequestTargeting{
-		PriceGranularity:     priceGran,
-		IncludeWinners:       true,
+		PriceGranularity:     &priceGran,
+		IncludeWinners:       ptrutil.ToPtr(true),
 		IncludeBrandCategory: &brandCat,
 	}
 
@@ -2764,7 +2765,7 @@ func TestCategoryMappingTranslateCategoriesFalse(t *testing.T) {
 	requestExt := newExtRequestTranslateCategories(&translateCategories)
 
 	targData := &targetData{
-		priceGranularity: requestExt.Prebid.Targeting.PriceGranularity,
+		priceGranularity: *requestExt.Prebid.Targeting.PriceGranularity,
 		includeWinners:   true,
 	}
 
@@ -2815,7 +2816,7 @@ func TestCategoryDedupe(t *testing.T) {
 	requestExt := newExtRequest()
 
 	targData := &targetData{
-		priceGranularity: requestExt.Prebid.Targeting.PriceGranularity,
+		priceGranularity: *requestExt.Prebid.Targeting.PriceGranularity,
 		includeWinners:   true,
 	}
 
@@ -2895,7 +2896,7 @@ func TestNoCategoryDedupe(t *testing.T) {
 	requestExt := newExtRequestNoBrandCat()
 
 	targData := &targetData{
-		priceGranularity: requestExt.Prebid.Targeting.PriceGranularity,
+		priceGranularity: *requestExt.Prebid.Targeting.PriceGranularity,
 		includeWinners:   true,
 	}
 
@@ -2977,7 +2978,7 @@ func TestCategoryMappingBidderName(t *testing.T) {
 	requestExt.Prebid.Targeting.AppendBidderNames = true
 
 	targData := &targetData{
-		priceGranularity: requestExt.Prebid.Targeting.PriceGranularity,
+		priceGranularity: *requestExt.Prebid.Targeting.PriceGranularity,
 		includeWinners:   true,
 	}
 
@@ -3031,7 +3032,7 @@ func TestCategoryMappingBidderNameNoCategories(t *testing.T) {
 	requestExt.Prebid.Targeting.AppendBidderNames = true
 
 	targData := &targetData{
-		priceGranularity: requestExt.Prebid.Targeting.PriceGranularity,
+		priceGranularity: *requestExt.Prebid.Targeting.PriceGranularity,
 		includeWinners:   true,
 	}
 
@@ -3084,7 +3085,7 @@ func TestBidRejectionErrors(t *testing.T) {
 	requestExt.Prebid.Targeting.DurationRangeSec = []int{15, 30, 50}
 
 	targData := &targetData{
-		priceGranularity: requestExt.Prebid.Targeting.PriceGranularity,
+		priceGranularity: *requestExt.Prebid.Targeting.PriceGranularity,
 		includeWinners:   true,
 	}
 
@@ -3191,7 +3192,7 @@ func TestCategoryMappingTwoBiddersOneBidEachNoCategorySamePrice(t *testing.T) {
 	requestExt := newExtRequestTranslateCategories(nil)
 
 	targData := &targetData{
-		priceGranularity: requestExt.Prebid.Targeting.PriceGranularity,
+		priceGranularity: *requestExt.Prebid.Targeting.PriceGranularity,
 		includeWinners:   true,
 	}
 
@@ -3268,7 +3269,7 @@ func TestCategoryMappingTwoBiddersManyBidsEachNoCategorySamePrice(t *testing.T)
 	requestExt := newExtRequestTranslateCategories(nil)
 
 	targData := &targetData{
-		priceGranularity: requestExt.Prebid.Targeting.PriceGranularity,
+		priceGranularity: *requestExt.Prebid.Targeting.PriceGranularity,
 		includeWinners:   true,
 	}
 
diff --git a/exchange/exchangetest/append-bidder-names.json b/exchange/exchangetest/append-bidder-names.json
index 86688ed1..400992ce 100644
--- a/exchange/exchangetest/append-bidder-names.json
+++ b/exchange/exchangetest/append-bidder-names.json
@@ -50,6 +50,18 @@
               "publisher": "",
               "withcategory": true
             },
+            "pricegranularity": {
+              "precision": 2,
+              "ranges": [
+                {
+                  "min": 0,
+                  "max": 20,
+                  "increment": 0.1
+                }
+              ]
+            },
+            "includewinners": true,
+            "includebidderkeys": true,
             "appendbiddernames": true
           }
         }
@@ -229,6 +241,18 @@
                   "publisher": "",
                   "withcategory": true
                 },
+                "pricegranularity": {
+                  "precision": 2,
+                  "ranges": [
+                    {
+                      "min": 0,
+                      "max": 20,
+                      "increment": 0.1
+                    }
+                  ]
+                },
+                "includewinners": true,
+                "includebidderkeys": true,
                 "appendbiddernames": true
               }
             }
@@ -237,4 +261,4 @@
       }
     }
   }
-}
\ No newline at end of file
+}
diff --git a/exchange/exchangetest/bid-id-invalid.json b/exchange/exchangetest/bid-id-invalid.json
index e71bb0a5..d7d12619 100644
--- a/exchange/exchangetest/bid-id-invalid.json
+++ b/exchange/exchangetest/bid-id-invalid.json
@@ -37,6 +37,18 @@
               "publisher": "",
               "withcategory": true
             },
+            "pricegranularity": {
+              "precision": 2,
+              "ranges": [
+                {
+                  "min": 0,
+                  "max": 20,
+                  "increment": 0.1
+                }
+              ]
+            },
+            "includewinners": true,
+            "includebidderkeys": true,
             "appendbiddernames": true
           }
         }
@@ -154,6 +166,18 @@
                   "publisher": "",
                   "withcategory": true
                 },
+                "pricegranularity": {
+                  "precision": 2,
+                  "ranges": [
+                    {
+                      "min": 0,
+                      "max": 20,
+                      "increment": 0.1
+                    }
+                  ]
+                },
+                "includewinners": true,
+                "includebidderkeys": true,
                 "appendbiddernames": true
               }
             }
@@ -170,4 +194,4 @@
       }
     }
   }
-}
\ No newline at end of file
+}
diff --git a/exchange/exchangetest/bid-id-valid.json b/exchange/exchangetest/bid-id-valid.json
index 85ceb883..397d232a 100644
--- a/exchange/exchangetest/bid-id-valid.json
+++ b/exchange/exchangetest/bid-id-valid.json
@@ -37,6 +37,18 @@
               "publisher": "",
               "withcategory": true
             },
+            "pricegranularity": {
+              "precision": 2,
+              "ranges": [
+                {
+                  "min": 0,
+                  "max": 20,
+                  "increment": 0.1
+                }
+              ]
+            },
+            "includewinners": true,
+            "includebidderkeys": true,
             "appendbiddernames": true
           }
         }
@@ -150,6 +162,18 @@
           "ext": {
             "prebid": {
               "targeting": {
+                "pricegranularity": {
+                  "precision": 2,
+                  "ranges": [
+                    {
+                      "min": 0,
+                      "max": 20,
+                      "increment": 0.1
+                    }
+                  ]
+                },
+                "includewinners": true,
+                "includebidderkeys": true,
                 "includebrandcategory": {
                   "primaryadserver": 1,
                   "publisher": "",
@@ -163,4 +187,4 @@
       }
     }
   }
-}
\ No newline at end of file
+}
diff --git a/exchange/exchangetest/debuglog_disabled.json b/exchange/exchangetest/debuglog_disabled.json
index fc3226e3..d803759d 100644
--- a/exchange/exchangetest/debuglog_disabled.json
+++ b/exchange/exchangetest/debuglog_disabled.json
@@ -55,6 +55,18 @@
       "ext": {
         "prebid": {
           "targeting": {
+            "pricegranularity": {
+              "precision": 2,
+              "ranges": [
+                {
+                  "min": 0,
+                  "max": 20,
+                  "increment": 0.1
+                }
+              ]
+            },
+            "includewinners": true,
+            "includebidderkeys": true,
             "includebrandcategory": {
               "primaryadserver": 1,
               "publisher": "",
@@ -233,6 +245,18 @@
           "ext": {
             "prebid": {
               "targeting": {
+                "pricegranularity": {
+                  "precision": 2,
+                  "ranges": [
+                    {
+                      "min": 0,
+                      "max": 20,
+                      "increment": 0.1
+                    }
+                  ]
+                },
+                "includewinners": true,
+                "includebidderkeys": true,
                 "includebrandcategory": {
                   "primaryadserver": 1,
                   "publisher": "",
@@ -245,4 +269,4 @@
       }
     }
   }
-}
\ No newline at end of file
+}
diff --git a/exchange/exchangetest/debuglog_enabled.json b/exchange/exchangetest/debuglog_enabled.json
index af77a640..054737e6 100644
--- a/exchange/exchangetest/debuglog_enabled.json
+++ b/exchange/exchangetest/debuglog_enabled.json
@@ -57,6 +57,18 @@
       "ext": {
         "prebid": {
           "targeting": {
+            "pricegranularity": {
+              "precision": 2,
+              "ranges": [
+                {
+                  "min": 0,
+                  "max": 20,
+                  "increment": 0.1
+                }
+              ]
+            },
+            "includewinners": true,
+            "includebidderkeys": true,
             "includebrandcategory": {
               "primaryadserver": 1,
               "publisher": "",
@@ -235,6 +247,18 @@
           "ext": {
             "prebid": {
               "targeting": {
+                "pricegranularity": {
+                  "precision": 2,
+                  "ranges": [
+                    {
+                      "min": 0,
+                      "max": 20,
+                      "increment": 0.1
+                    }
+                  ]
+                },
+                "includewinners": true,
+                "includebidderkeys": true,
                 "includebrandcategory": {
                   "primaryadserver": 1,
                   "publisher": "",
@@ -247,4 +271,4 @@
       }
     }
   }
-}
\ No newline at end of file
+}
diff --git a/exchange/exchangetest/debuglog_enabled_no_bids.json b/exchange/exchangetest/debuglog_enabled_no_bids.json
index f0ecffae..cf2a821d 100644
--- a/exchange/exchangetest/debuglog_enabled_no_bids.json
+++ b/exchange/exchangetest/debuglog_enabled_no_bids.json
@@ -56,6 +56,18 @@
       "ext": {
         "prebid": {
           "targeting": {
+            "pricegranularity": {
+              "precision": 2,
+              "ranges": [
+                {
+                  "min": 0,
+                  "max": 20,
+                  "increment": 0.1
+                }
+              ]
+            },
+            "includewinners": false,
+            "includebidderkeys": true,
             "includebrandcategory": {
               "primaryadserver": 1,
               "publisher": "",
diff --git a/exchange/exchangetest/events-vast-account-off-request-off.json b/exchange/exchangetest/events-vast-account-off-request-off.json
index ac9b0c9d..a94546d2 100644
--- a/exchange/exchangetest/events-vast-account-off-request-off.json
+++ b/exchange/exchangetest/events-vast-account-off-request-off.json
@@ -29,7 +29,17 @@
         "prebid": {
           "targeting": {
             "includewinners": true,
-            "includebidderkeys": false
+            "includebidderkeys": false,
+            "pricegranularity": {
+              "precision": 2,
+              "ranges": [
+                {
+                  "min": 0,
+                  "max": 20,
+                  "increment": 0.1
+                }
+              ]
+            }
           }
         }
       }
@@ -167,4 +177,4 @@
       ]
     }
   }
-}
\ No newline at end of file
+}
diff --git a/exchange/exchangetest/events-vast-account-off-request-on.json b/exchange/exchangetest/events-vast-account-off-request-on.json
index 23d1924a..275b8aef 100644
--- a/exchange/exchangetest/events-vast-account-off-request-on.json
+++ b/exchange/exchangetest/events-vast-account-off-request-on.json
@@ -32,6 +32,16 @@
       "ext": {
         "prebid": {
           "targeting": {
+            "pricegranularity": {
+              "precision": 2,
+              "ranges": [
+                {
+                  "min": 0,
+                  "max": 20,
+                  "increment": 0.1
+                }
+              ]
+            },
             "includewinners": true,
             "includebidderkeys": false
           },
@@ -178,4 +188,4 @@
       ]
     }
   }
-}
\ No newline at end of file
+}
diff --git a/exchange/exchangetest/events-vast-account-on-request-off.json b/exchange/exchangetest/events-vast-account-on-request-off.json
index e75e1ec6..e271ecf5 100644
--- a/exchange/exchangetest/events-vast-account-on-request-off.json
+++ b/exchange/exchangetest/events-vast-account-on-request-off.json
@@ -28,6 +28,16 @@
       "ext": {
         "prebid": {
           "targeting": {
+            "pricegranularity": {
+              "precision": 2,
+              "ranges": [
+                {
+                  "min": 0,
+                  "max": 20,
+                  "increment": 0.1
+                }
+              ]
+            },
             "includewinners": true,
             "includebidderkeys": false
           },
@@ -170,4 +180,4 @@
       ]
     }
   }
-}
\ No newline at end of file
+}
diff --git a/exchange/exchangetest/extra-bids-with-aliases-adaptercode.json b/exchange/exchangetest/extra-bids-with-aliases-adaptercode.json
index 7b49cb80..b46f54c6 100644
--- a/exchange/exchangetest/extra-bids-with-aliases-adaptercode.json
+++ b/exchange/exchangetest/extra-bids-with-aliases-adaptercode.json
@@ -44,6 +44,16 @@
       "ext": {
         "prebid": {
           "targeting": {
+            "pricegranularity": {
+              "precision": 2,
+              "ranges": [
+                {
+                  "min": 0,
+                  "max": 20,
+                  "increment": 0.1
+                }
+              ]
+            },
             "includewinners": true,
             "includebidderkeys": true
           },
@@ -244,4 +254,4 @@
       ]
     }
   }
-}
\ No newline at end of file
+}
diff --git a/exchange/exchangetest/extra-bids.json b/exchange/exchangetest/extra-bids.json
index 67a9b085..096a26d2 100644
--- a/exchange/exchangetest/extra-bids.json
+++ b/exchange/exchangetest/extra-bids.json
@@ -50,6 +50,16 @@
       "ext": {
         "prebid": {
           "targeting": {
+            "pricegranularity": {
+              "precision": 2,
+              "ranges": [
+                {
+                  "min": 0,
+                  "max": 20,
+                  "increment": 0.1
+                }
+              ]
+            },
             "includewinners": true,
             "includebidderkeys": true
           },
@@ -458,4 +468,4 @@
       ]
     }
   }
-}
\ No newline at end of file
+}
diff --git a/exchange/exchangetest/include-brand-category.json b/exchange/exchangetest/include-brand-category.json
index 81b44b4c..9d537f74 100644
--- a/exchange/exchangetest/include-brand-category.json
+++ b/exchange/exchangetest/include-brand-category.json
@@ -44,6 +44,18 @@
       "ext": {
         "prebid": {
           "targeting": {
+            "pricegranularity": {
+              "precision": 2,
+              "ranges": [
+                {
+                  "min": 0,
+                  "max": 20,
+                  "increment": 0.1
+                }
+              ]
+            },
+            "includewinners": true,
+            "includebidderkeys": true,
             "includebrandcategory": {
               "primaryadserver": 1,
               "publisher": "",
@@ -176,4 +188,4 @@
       ]
     }
   }
-}
\ No newline at end of file
+}
diff --git a/exchange/exchangetest/request-multi-bidders-debug-info.json b/exchange/exchangetest/request-multi-bidders-debug-info.json
index cb3c3456..368c6e45 100644
--- a/exchange/exchangetest/request-multi-bidders-debug-info.json
+++ b/exchange/exchangetest/request-multi-bidders-debug-info.json
@@ -55,6 +55,18 @@
               15,
               30
             ],
+            "pricegranularity": {
+              "precision": 2,
+              "ranges": [
+                {
+                  "min": 0,
+                  "max": 20,
+                  "increment": 0.1
+                }
+              ]
+            },
+            "includewinners": true,
+            "includebidderkeys": true,
             "includebrandcategory": {
               "primaryadserver": 1,
               "publisher": "",
@@ -249,6 +261,18 @@
                   15,
                   30
                 ],
+                "pricegranularity": {
+                  "precision": 2,
+                  "ranges": [
+                    {
+                      "min": 0,
+                      "max": 20,
+                      "increment": 0.1
+                    }
+                  ]
+                },
+                "includewinners": true,
+                "includebidderkeys": true,
                 "includebrandcategory": {
                   "primaryadserver": 1,
                   "publisher": "",
@@ -262,4 +286,4 @@
       }
     }
   }
-}
\ No newline at end of file
+}
diff --git a/exchange/exchangetest/request-multi-bidders-one-no-resp.json b/exchange/exchangetest/request-multi-bidders-one-no-resp.json
index b5a1da82..8ec9034a 100644
--- a/exchange/exchangetest/request-multi-bidders-one-no-resp.json
+++ b/exchange/exchangetest/request-multi-bidders-one-no-resp.json
@@ -54,6 +54,18 @@
               15,
               30
             ],
+            "pricegranularity": {
+              "precision": 2,
+              "ranges": [
+                {
+                  "min": 0,
+                  "max": 20,
+                  "increment": 0.1
+                }
+              ]
+            },
+            "includewinners": true,
+            "includebidderkeys": true,
             "includebrandcategory": {
               "primaryadserver": 1,
               "publisher": "",
@@ -137,4 +149,4 @@
       ]
     }
   }
-}
\ No newline at end of file
+}
diff --git a/exchange/exchangetest/targeting-cache-vast-banner.json b/exchange/exchangetest/targeting-cache-vast-banner.json
index 39f9a005..43b65e59 100644
--- a/exchange/exchangetest/targeting-cache-vast-banner.json
+++ b/exchange/exchangetest/targeting-cache-vast-banner.json
@@ -28,7 +28,20 @@
           "cache": {
             "vastxml": {}
           },
-          "targeting": {}
+          "targeting": {
+            "pricegranularity": {
+              "precision": 2,
+              "ranges": [
+                {
+                  "min": 0,
+                  "max": 20,
+                  "increment": 0.1
+                }
+              ]
+            },
+            "includewinners": true,
+            "includebidderkeys": true
+          }
         }
       }
     }
@@ -95,4 +108,4 @@
       ]
     }
   }
-}
\ No newline at end of file
+}
diff --git a/exchange/exchangetest/targeting-cache-vast.json b/exchange/exchangetest/targeting-cache-vast.json
index 53742086..2f59ba30 100644
--- a/exchange/exchangetest/targeting-cache-vast.json
+++ b/exchange/exchangetest/targeting-cache-vast.json
@@ -29,7 +29,20 @@
           "cache": {
             "vastxml": {}
           },
-          "targeting": {}
+          "targeting": {
+            "pricegranularity": {
+              "precision": 2,
+              "ranges": [
+                {
+                  "min": 0,
+                  "max": 20,
+                  "increment": 0.1
+                }
+              ]
+            },
+            "includebidderkeys": true,
+            "includewinners": true
+          }
         }
       }
     }
@@ -106,4 +119,4 @@
       ]
     }
   }
-}
\ No newline at end of file
+}
diff --git a/exchange/exchangetest/targeting-cache-zero.json b/exchange/exchangetest/targeting-cache-zero.json
index 2d422d6d..ea062fbe 100644
--- a/exchange/exchangetest/targeting-cache-zero.json
+++ b/exchange/exchangetest/targeting-cache-zero.json
@@ -31,7 +31,20 @@
             "bids": {},
             "vastxml": {}
           },
-          "targeting": {}
+          "targeting": {
+            "pricegranularity": {
+              "precision": 2,
+              "ranges": [
+                {
+                  "min": 0,
+                  "max": 20,
+                  "increment": 0.1
+                }
+              ]
+            },
+            "includewinners": true,
+            "includebidderkeys": true
+          }
         }
       }
     }
@@ -110,4 +123,4 @@
       ]
     }
   }
-}
\ No newline at end of file
+}
diff --git a/exchange/exchangetest/targeting-mobile.json b/exchange/exchangetest/targeting-mobile.json
index deea901a..914a557b 100644
--- a/exchange/exchangetest/targeting-mobile.json
+++ b/exchange/exchangetest/targeting-mobile.json
@@ -49,7 +49,20 @@
       ],
       "ext": {
         "prebid": {
-          "targeting": {}
+          "targeting": {
+            "pricegranularity": {
+              "precision": 2,
+              "ranges": [
+                {
+                  "min": 0,
+                  "max": 20,
+                  "increment": 0.1
+                }
+              ]
+            },
+            "includewinners": true,
+            "includebidderkeys": true
+          }
         }
       }
     }
@@ -231,4 +244,4 @@
       ]
     }
   }
-}
\ No newline at end of file
+}
diff --git a/exchange/exchangetest/targeting-no-winners.json b/exchange/exchangetest/targeting-no-winners.json
index 729575ee..6ab69fb4 100644
--- a/exchange/exchangetest/targeting-no-winners.json
+++ b/exchange/exchangetest/targeting-no-winners.json
@@ -50,7 +50,18 @@
       "ext": {
         "prebid": {
           "targeting": {
-            "includewinners": false
+            "pricegranularity": {
+              "precision": 2,
+              "ranges": [
+                {
+                  "min": 0,
+                  "max": 20,
+                  "increment": 0.1
+                }
+              ]
+            },
+            "includewinners": false,
+            "includebidderkeys": true
           }
         }
       }
@@ -218,4 +229,4 @@
       ]
     }
   }
-}
\ No newline at end of file
+}
diff --git a/exchange/exchangetest/targeting-only-winners.json b/exchange/exchangetest/targeting-only-winners.json
index 900ea1fd..f9f96515 100644
--- a/exchange/exchangetest/targeting-only-winners.json
+++ b/exchange/exchangetest/targeting-only-winners.json
@@ -50,6 +50,17 @@
       "ext": {
         "prebid": {
           "targeting": {
+            "pricegranularity": {
+              "precision": 2,
+              "ranges": [
+                {
+                  "min": 0,
+                  "max": 20,
+                  "increment": 0.1
+                }
+              ]
+            },
+            "includewinners": true,
             "includebidderkeys": false
           }
         }
@@ -211,4 +222,4 @@
       ]
     }
   }
-}
\ No newline at end of file
+}
diff --git a/exchange/exchangetest/targeting-with-winners.json b/exchange/exchangetest/targeting-with-winners.json
index ca1b1afe..d1a0f49f 100644
--- a/exchange/exchangetest/targeting-with-winners.json
+++ b/exchange/exchangetest/targeting-with-winners.json
@@ -49,7 +49,20 @@
       ],
       "ext": {
         "prebid": {
-          "targeting": {}
+          "targeting": {
+            "pricegranularity": {
+              "precision": 2,
+              "ranges": [
+                {
+                  "min": 0,
+                  "max": 20,
+                  "increment": 0.1
+                }
+              ]
+            },
+            "includewinners": true,
+            "includebidderkeys": true
+          }
         }
       }
     }
@@ -226,4 +239,4 @@
       ]
     }
   }
-}
\ No newline at end of file
+}
diff --git a/exchange/price_granularity_test.go b/exchange/price_granularity_test.go
index 6349713e..50e83778 100644
--- a/exchange/price_granularity_test.go
+++ b/exchange/price_granularity_test.go
@@ -5,17 +5,19 @@ import (
 	"testing"
 
 	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/prebid/prebid-server/util/ptrutil"
 	"github.com/stretchr/testify/assert"
 )
 
 func TestGetPriceBucketString(t *testing.T) {
-	low := openrtb_ext.PriceGranularityFromString("low")
-	medium := openrtb_ext.PriceGranularityFromString("medium")
-	high := openrtb_ext.PriceGranularityFromString("high")
-	auto := openrtb_ext.PriceGranularityFromString("auto")
-	dense := openrtb_ext.PriceGranularityFromString("dense")
+	low, _ := openrtb_ext.NewPriceGranularityFromLegacyID("low")
+	medium, _ := openrtb_ext.NewPriceGranularityFromLegacyID("medium")
+	high, _ := openrtb_ext.NewPriceGranularityFromLegacyID("high")
+	auto, _ := openrtb_ext.NewPriceGranularityFromLegacyID("auto")
+	dense, _ := openrtb_ext.NewPriceGranularityFromLegacyID("dense")
+
 	custom1 := openrtb_ext.PriceGranularity{
-		Precision: 2,
+		Precision: ptrutil.ToPtr(2),
 		Ranges: []openrtb_ext.GranularityRange{
 			{
 				Min:       0.0,
@@ -31,7 +33,7 @@ func TestGetPriceBucketString(t *testing.T) {
 	}
 
 	custom2 := openrtb_ext.PriceGranularity{
-		Precision: 2,
+		Precision: ptrutil.ToPtr(2),
 		Ranges: []openrtb_ext.GranularityRange{
 			{
 				Min:       0.0,
@@ -97,17 +99,17 @@ func TestGetPriceBucketString(t *testing.T) {
 			testCases: []aTest{
 				{
 					"Negative precision defaults to number of digits already in CPM float",
-					openrtb_ext.PriceGranularity{Precision: -1, Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 0.05}}},
+					openrtb_ext.PriceGranularity{Precision: ptrutil.ToPtr(-1), Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 0.05}}},
 					"1.85",
 				},
 				{
 					"Precision value equals zero, we expect to round up to the nearest integer",
-					openrtb_ext.PriceGranularity{Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 0.05}}},
+					openrtb_ext.PriceGranularity{Precision: ptrutil.ToPtr(0), Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 0.05}}},
 					"2",
 				},
 				{
 					"Largest precision value PBS supports 15",
-					openrtb_ext.PriceGranularity{Precision: 15, Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 0.05}}},
+					openrtb_ext.PriceGranularity{Precision: ptrutil.ToPtr(15), Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 0.05}}},
 					"1.850000000000000",
 				},
 			},
@@ -118,17 +120,17 @@ func TestGetPriceBucketString(t *testing.T) {
 			testCases: []aTest{
 				{
 					"Negative increment, return empty string",
-					openrtb_ext.PriceGranularity{Precision: 2, Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: -0.05}}},
+					openrtb_ext.PriceGranularity{Precision: ptrutil.ToPtr(2), Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: -0.05}}},
 					"",
 				},
 				{
 					"Zero increment, return empty string",
-					openrtb_ext.PriceGranularity{Precision: 2, Ranges: []openrtb_ext.GranularityRange{{Max: 5}}},
+					openrtb_ext.PriceGranularity{Precision: ptrutil.ToPtr(2), Ranges: []openrtb_ext.GranularityRange{{Max: 5}}},
 					"",
 				},
 				{
 					"Increment value is greater than CPM itself, return zero float value",
-					openrtb_ext.PriceGranularity{Precision: 2, Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 1.877}}},
+					openrtb_ext.PriceGranularity{Precision: ptrutil.ToPtr(2), Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 1.877}}},
 					"0.00",
 				},
 			},
@@ -151,9 +153,9 @@ func TestGetPriceBucketString(t *testing.T) {
 	}
 
 	for _, testGroup := range testGroups {
-		for _, test := range testGroup.testCases {
-			priceBucket := GetPriceBucket(testGroup.cpm, test.granularity)
-
+		for i, test := range testGroup.testCases {
+			var priceBucket string
+			assert.NotPanics(t, func() { priceBucket = GetPriceBucket(testGroup.cpm, test.granularity) }, "Group: %s Granularity: %d", testGroup.groupDesc, i)
 			assert.Equal(t, test.expectedPriceBucket, priceBucket, "Group: %s Granularity: %s :: Expected %s, got %s from %f", testGroup.groupDesc, test.granularityId, test.expectedPriceBucket, priceBucket, testGroup.cpm)
 		}
 	}
diff --git a/exchange/targeting_test.go b/exchange/targeting_test.go
index 809b58af..19b6ea5e 100644
--- a/exchange/targeting_test.go
+++ b/exchange/targeting_test.go
@@ -158,13 +158,13 @@ func buildAdapterMap(bids map[openrtb_ext.BidderName][]*openrtb2.Bid, mockServer
 func buildTargetingExt(includeCache bool, includeWinners bool, includeBidderKeys bool) json.RawMessage {
 	var targeting string
 	if includeWinners && includeBidderKeys {
-		targeting = "{}"
+		targeting = `{"pricegranularity":{"precision":2,"ranges": [{"min": 0,"max": 20,"increment": 0.1}]},"includewinners": true, "includebidderkeys": true}`
 	} else if !includeWinners && includeBidderKeys {
-		targeting = `{"includewinners": false}`
+		targeting = `{"precision":2,"includewinners": false}`
 	} else if includeWinners && !includeBidderKeys {
-		targeting = `{"includebidderkeys": false}`
+		targeting = `{"precision":2,"includebidderkeys": false}`
 	} else {
-		targeting = `{"includewinners": false, "includebidderkeys": false}`
+		targeting = `{"precision":2,"includewinners": false, "includebidderkeys": false}`
 	}
 
 	if includeCache {
@@ -286,15 +286,23 @@ var bid084 *openrtb2.Bid = &openrtb2.Bid{
 	Price: 0.84,
 }
 
-var truncateTargetAttrValue10 int = 10
-var truncateTargetAttrValue5 int = 5
-var truncateTargetAttrValue25 int = 25
-var truncateTargetAttrValueNegative int = -1
+var (
+	truncateTargetAttrValue10       int = 10
+	truncateTargetAttrValue5        int = 5
+	truncateTargetAttrValue25       int = 25
+	truncateTargetAttrValueNegative int = -1
+)
+
+func lookupPriceGranularity(v string) openrtb_ext.PriceGranularity {
+	priceGranularity, _ := openrtb_ext.NewPriceGranularityFromLegacyID(v)
+	return priceGranularity
+}
+
 var TargetingTests []TargetingTestData = []TargetingTestData{
 	{
 		Description: "Targeting winners only (most basic targeting example)",
 		TargetData: targetData{
-			priceGranularity: openrtb_ext.PriceGranularityFromString("med"),
+			priceGranularity: lookupPriceGranularity("med"),
 			includeWinners:   true,
 		},
 		Auction: auction{
@@ -325,7 +333,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 	{
 		Description: "Targeting on bidders only",
 		TargetData: targetData{
-			priceGranularity:  openrtb_ext.PriceGranularityFromString("med"),
+			priceGranularity:  lookupPriceGranularity("med"),
 			includeBidderKeys: true,
 		},
 		Auction: auction{
@@ -359,7 +367,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 	{
 		Description: "Full basic targeting with hd_format",
 		TargetData: targetData{
-			priceGranularity:  openrtb_ext.PriceGranularityFromString("med"),
+			priceGranularity:  lookupPriceGranularity("med"),
 			includeWinners:    true,
 			includeBidderKeys: true,
 			includeFormat:     true,
@@ -400,7 +408,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 	{
 		Description: "Cache and deal targeting test",
 		TargetData: targetData{
-			priceGranularity:  openrtb_ext.PriceGranularityFromString("med"),
+			priceGranularity:  lookupPriceGranularity("med"),
 			includeBidderKeys: true,
 			cacheHost:         "cache.prebid.com",
 			cachePath:         "cache",
@@ -447,7 +455,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 	{
 		Description: "bidder with no dealID should not have deal targeting",
 		TargetData: targetData{
-			priceGranularity:  openrtb_ext.PriceGranularityFromString("med"),
+			priceGranularity:  lookupPriceGranularity("med"),
 			includeBidderKeys: true,
 		},
 		Auction: auction{
@@ -473,7 +481,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 	{
 		Description: "Truncate Targeting Attribute value is given and is less than const MaxKeyLength",
 		TargetData: targetData{
-			priceGranularity:  openrtb_ext.PriceGranularityFromString("med"),
+			priceGranularity:  lookupPriceGranularity("med"),
 			includeBidderKeys: true,
 		},
 		Auction: auction{
@@ -507,7 +515,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 	{
 		Description: "Truncate Targeting Attribute value is given and is greater than const MaxKeyLength",
 		TargetData: targetData{
-			priceGranularity:  openrtb_ext.PriceGranularityFromString("med"),
+			priceGranularity:  lookupPriceGranularity("med"),
 			includeBidderKeys: true,
 		},
 		Auction: auction{
@@ -541,7 +549,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 	{
 		Description: "Truncate Targeting Attribute value is given and is negative",
 		TargetData: targetData{
-			priceGranularity:  openrtb_ext.PriceGranularityFromString("med"),
+			priceGranularity:  lookupPriceGranularity("med"),
 			includeBidderKeys: true,
 		},
 		Auction: auction{
@@ -575,7 +583,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 	{
 		Description: "Check that key gets truncated properly when value is smaller than key",
 		TargetData: targetData{
-			priceGranularity: openrtb_ext.PriceGranularityFromString("med"),
+			priceGranularity: lookupPriceGranularity("med"),
 			includeWinners:   true,
 		},
 		Auction: auction{
@@ -606,7 +614,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 	{
 		Description: "Check that key gets truncated properly when value is greater than key",
 		TargetData: targetData{
-			priceGranularity: openrtb_ext.PriceGranularityFromString("med"),
+			priceGranularity: lookupPriceGranularity("med"),
 			includeWinners:   true,
 		},
 		Auction: auction{
@@ -637,7 +645,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 	{
 		Description: "Check that key gets truncated properly when value is negative",
 		TargetData: targetData{
-			priceGranularity: openrtb_ext.PriceGranularityFromString("med"),
+			priceGranularity: lookupPriceGranularity("med"),
 			includeWinners:   true,
 		},
 		Auction: auction{
diff --git a/exchange/utils_test.go b/exchange/utils_test.go
index 257449bd..4a2d5fe7 100644
--- a/exchange/utils_test.go
+++ b/exchange/utils_test.go
@@ -15,6 +15,7 @@ import (
 	"github.com/prebid/prebid-server/gdpr"
 	"github.com/prebid/prebid-server/metrics"
 	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/prebid/prebid-server/util/ptrutil"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
 )
@@ -1756,12 +1757,12 @@ func TestGetExtTargetData(t *testing.T) {
 				requestExt: &openrtb_ext.ExtRequest{
 					Prebid: openrtb_ext.ExtRequestPrebid{
 						Targeting: &openrtb_ext.ExtRequestTargeting{
-							PriceGranularity: openrtb_ext.PriceGranularity{
-								Precision: 2,
+							PriceGranularity: &openrtb_ext.PriceGranularity{
+								Precision: ptrutil.ToPtr(2),
 								Ranges:    []openrtb_ext.GranularityRange{{Min: 0.00, Max: 5.00, Increment: 1.00}},
 							},
-							IncludeWinners:    true,
-							IncludeBidderKeys: true,
+							IncludeWinners:    ptrutil.ToPtr(true),
+							IncludeBidderKeys: ptrutil.ToPtr(true),
 						},
 					},
 				},
@@ -1773,7 +1774,7 @@ func TestGetExtTargetData(t *testing.T) {
 			outTest{
 				targetData: &targetData{
 					priceGranularity: openrtb_ext.PriceGranularity{
-						Precision: 2,
+						Precision: ptrutil.ToPtr(2),
 						Ranges:    []openrtb_ext.GranularityRange{{Min: 0.00, Max: 5.00, Increment: 1.00}},
 					},
 					includeWinners:    true,
diff --git a/openrtb_ext/request_test.go b/openrtb_ext/request_test.go
index 979c2edb..8d1ecee2 100644
--- a/openrtb_ext/request_test.go
+++ b/openrtb_ext/request_test.go
@@ -2,87 +2,62 @@ package openrtb_ext
 
 import (
 	"encoding/json"
-	"reflect"
+	"fmt"
 	"testing"
 
+	"github.com/prebid/prebid-server/util/ptrutil"
 	"github.com/stretchr/testify/assert"
 )
 
-// Test the unmarshalling of the prebid extensions and setting default Price Granularity
-func TestExtRequestTargeting(t *testing.T) {
-	extRequest := &ExtRequest{}
-	err := json.Unmarshal([]byte(ext1), extRequest)
-	if err != nil {
-		t.Errorf("ext1 Unmarshall failure: %s", err.Error())
-	}
-	if extRequest.Prebid.Targeting != nil {
-		t.Error("ext1 Targeting is not nil")
-	}
-
-	extRequest = &ExtRequest{}
-	err = json.Unmarshal([]byte(ext2), extRequest)
-	if err != nil {
-		t.Errorf("ext2 Unmarshall failure: %s", err.Error())
-	}
-	if extRequest.Prebid.Targeting == nil {
-		t.Error("ext2 Targeting is nil")
-	} else {
-		pgDense := PriceGranularityFromString("dense")
-		if !reflect.DeepEqual(extRequest.Prebid.Targeting.PriceGranularity, pgDense) {
-			t.Errorf("ext2 expected Price granularity \"dense\" (%v), found \"%v\"", pgDense, extRequest.Prebid.Targeting.PriceGranularity)
-		}
-	}
-
-	extRequest = &ExtRequest{}
-	err = json.Unmarshal([]byte(ext3), extRequest)
-	if err != nil {
-		t.Errorf("ext3 Unmarshall failure: %s", err.Error())
-	}
-	if extRequest.Prebid.Targeting == nil {
-		t.Error("ext3 Targeting is nil")
-	} else {
-		pgMed := PriceGranularityFromString("medium")
-		if !reflect.DeepEqual(extRequest.Prebid.Targeting.PriceGranularity, pgMed) {
-			t.Errorf("ext3 expected Price granularity \"medium\", found \"%v\"", extRequest.Prebid.Targeting.PriceGranularity)
-		}
-	}
-}
-
-const ext1 = `{
-	"prebid": {
-		"non_target": "some junk"
-	}
-}
-`
-
-const ext2 = `{
-	"prebid": {
-		"targeting": {
-			"pricegranularity": "dense"
-		}
-	}
-}`
-
-const ext3 = `{
-	"prebid": {
-		"targeting": { }
-	}
-}`
-
 type granularityTestData struct {
 	json   []byte
 	target PriceGranularity
 }
 
 func TestGranularityUnmarshal(t *testing.T) {
-	for _, test := range validGranularityTests {
-		var resolved PriceGranularity
-		err := json.Unmarshal(test.json, &resolved)
-		if err != nil {
-			t.Errorf("Failed to Unmarshall granularity: %s", err.Error())
-		}
-		if !reflect.DeepEqual(test.target, resolved) {
-			t.Errorf("Granularity unmarshal failed, the unmarshalled JSON did not match the target\nExpected: %v\nActual  : %v", test.target, resolved)
+	testGroups := []struct {
+		desc        string
+		in          []granularityTestData
+		expectError bool
+	}{
+		{
+			desc:        "Unmarshal without error",
+			in:          validGranularityTests,
+			expectError: false,
+		},
+		{
+			desc: "Malformed json. Expect unmarshall error",
+			in: []granularityTestData{
+				{json: []byte(`[]`)},
+			},
+			expectError: true,
+		},
+	}
+	for _, tg := range testGroups {
+		for i, tc := range tg.in {
+			var resolved PriceGranularity
+			err := json.Unmarshal(tc.json, &resolved)
+
+			// Assert validation error
+			if tg.expectError && !assert.Errorf(t, err, "%s test case %d", tg.desc, i) {
+				continue
+			}
+
+			// Assert Targeting.Precision
+			assert.Equal(t, tc.target.Precision, resolved.Precision, "%s test case %d", tg.desc, i)
+
+			// Assert Targeting.Ranges
+			if assert.Len(t, resolved.Ranges, len(tc.target.Ranges), "%s test case %d", tg.desc, i) {
+				expected := make(map[string]struct{}, len(tc.target.Ranges))
+				for _, r := range tc.target.Ranges {
+					expected[fmt.Sprintf("%2.2f-%2.2f-%2.2f", r.Min, r.Max, r.Increment)] = struct{}{}
+				}
+				for _, actualRange := range resolved.Ranges {
+					targetRange := fmt.Sprintf("%2.2f-%2.2f-%2.2f", actualRange.Min, actualRange.Max, actualRange.Increment)
+					_, exists := expected[targetRange]
+					assert.True(t, exists, "%s test case %d target.range %s not found", tg.desc, i, targetRange)
+				}
+			}
 		}
 	}
 }
@@ -91,7 +66,7 @@ var validGranularityTests []granularityTestData = []granularityTestData{
 	{
 		json: []byte(`{"precision": 4, "ranges": [{"min": 0, "max": 5, "increment": 0.1}, {"min": 5, "max":10, "increment":0.5}, {"min":10, "max":20, "increment":1}]}`),
 		target: PriceGranularity{
-			Precision: 4,
+			Precision: ptrutil.ToPtr(4),
 			Ranges: []GranularityRange{
 				{Min: 0.0, Max: 5.0, Increment: 0.1},
 				{Min: 5.0, Max: 10.0, Increment: 0.5},
@@ -102,99 +77,67 @@ var validGranularityTests []granularityTestData = []granularityTestData{
 	{
 		json: []byte(`{"ranges":[{ "max":5, "increment": 0.05}, {"max": 10, "increment": 0.25}, {"max": 20, "increment": 0.5}]}`),
 		target: PriceGranularity{
-			Precision: 2,
 			Ranges: []GranularityRange{
 				{Min: 0.0, Max: 5.0, Increment: 0.05},
-				{Min: 5.0, Max: 10.0, Increment: 0.25},
-				{Min: 10.0, Max: 20.0, Increment: 0.5},
+				{Min: 0.0, Max: 10.0, Increment: 0.25},
+				{Min: 0.0, Max: 20.0, Increment: 0.5},
 			},
 		},
 	},
 	{
-		json:   []byte(`"medium"`),
-		target: priceGranularityMed,
+		json: []byte(`"medium"`),
+		target: PriceGranularity{
+			Precision: ptrutil.ToPtr(2),
+			Ranges: []GranularityRange{{
+				Min:       0,
+				Max:       20,
+				Increment: 0.1}},
+		},
 	},
 	{
 		json: []byte(`{ "precision": 3, "ranges": [{"max":20, "increment":0.005}]}`),
 		target: PriceGranularity{
-			Precision: 3,
+			Precision: ptrutil.ToPtr(3),
 			Ranges:    []GranularityRange{{Min: 0.0, Max: 20.0, Increment: 0.005}},
 		},
 	},
 	{
 		json: []byte(`{"precision": 0, "ranges": [{"max":5, "increment": 1}, {"max": 10, "increment": 2}, {"max": 20, "increment": 5}]}`),
 		target: PriceGranularity{
-			Precision: 0,
+			Precision: ptrutil.ToPtr(0),
 			Ranges: []GranularityRange{
 				{Min: 0.0, Max: 5.0, Increment: 1.0},
-				{Min: 5.0, Max: 10.0, Increment: 2.0},
-				{Min: 10.0, Max: 20.0, Increment: 5.0},
+				{Min: 0.0, Max: 10.0, Increment: 2.0},
+				{Min: 0.0, Max: 20.0, Increment: 5.0},
 			},
 		},
 	},
 	{
 		json: []byte(`{"precision": 2, "ranges": [{"min": 0.5, "max":5, "increment": 0.1}, {"min": 54, "max": 10, "increment": 1}, {"min": -42, "max": 20, "increment": 5}]}`),
 		target: PriceGranularity{
-			Precision: 2,
+			Precision: ptrutil.ToPtr(2),
 			Ranges: []GranularityRange{
-				{Min: 0.0, Max: 5.0, Increment: 0.1},
-				{Min: 5.0, Max: 10.0, Increment: 1.0},
-				{Min: 10.0, Max: 20.0, Increment: 5.0},
+				{Min: 0.5, Max: 5.0, Increment: 0.1},
+				{Min: 54.0, Max: 10.0, Increment: 1.0},
+				{Min: -42.0, Max: 20.0, Increment: 5.0},
 			},
 		},
 	},
 	{
 		json:   []byte(`{}`),
-		target: priceGranularityMed,
+		target: PriceGranularity{},
 	},
 	{
-		json:   []byte(`{"precision": 2}`),
-		target: priceGranularityMed,
+		json: []byte(`{"precision": 2}`),
+		target: PriceGranularity{
+			Precision: ptrutil.ToPtr(2),
+		},
 	},
 	{
-		json:   []byte(`{"precision": 2, "ranges":[]}`),
-		target: priceGranularityMed,
-	},
-}
-
-func TestGranularityUnmarshalBad(t *testing.T) {
-	testCases := []struct {
-		description          string
-		jsonPriceGranularity []byte
-	}{
-		{
-			"Malformed",
-			[]byte(`[]`),
-		},
-		{
-			"Negative precision",
-			[]byte(`{"precision": -1, "ranges": [{"max":20, "increment":0.5}]}`),
-		},
-		{
-			"Precision greater than MaxDecimalFigures supported",
-			[]byte(`{"precision": 16, "ranges": [{"max":20, "increment":0.5}]}`),
-		},
-		{
-			"Negative increment",
-			[]byte(`{"ranges":[{"max":20, "increment": -1}]}`),
-		},
-		{
-			"Range with non float64 max value",
-			[]byte(`{"ranges":[{"max":"20", "increment": "0.1"}]}`),
-		},
-		{
-			"Ranges in decreasing order",
-			[]byte(`{"ranges":[{"max":20, "increment":0.1}. {"max":10, "increment":0.02}]}`),
-		},
-		{
-			"Max equal to previous max",
-			[]byte(`{"ranges":[{"max":1.0, "increment": 0.07}, {"max" 1.0, "increment": 0.03}]}`),
+		json: []byte(`{"precision": 2, "ranges":[]}`),
+		target: PriceGranularity{
+			Precision: ptrutil.ToPtr(2),
+			Ranges:    []GranularityRange{},
 		},
-	}
-
-	for _, test := range testCases {
-		resolved := PriceGranularity{}
-		err := json.Unmarshal(test.jsonPriceGranularity, &resolved)
-		assert.Errorf(t, err, "Invalid granularity unmarshalled without error.\nJSON was: %s\n Resolved to: %v. Test: %s", string(test.jsonPriceGranularity), resolved, test.description)
-	}
+	},
 }
diff --git a/ortb/default_test.go b/ortb/default_test.go
new file mode 100644
index 00000000..ad353ccb
--- /dev/null
+++ b/ortb/default_test.go
@@ -0,0 +1,331 @@
+package ortb
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/prebid/openrtb/v17/openrtb2"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/prebid/prebid-server/util/ptrutil"
+)
+
+func TestSetDefaults(t *testing.T) {
+	testCases := []struct {
+		name            string
+		givenRequest    openrtb2.BidRequest
+		expectedRequest openrtb2.BidRequest
+		expectedErr     string
+	}{
+		{
+			name:            "empty",
+			givenRequest:    openrtb2.BidRequest{},
+			expectedRequest: openrtb2.BidRequest{},
+		},
+		{
+			name:            "malformed request.ext",
+			givenRequest:    openrtb2.BidRequest{Ext: json.RawMessage(`malformed`)},
+			expectedRequest: openrtb2.BidRequest{Ext: json.RawMessage(`malformed`)},
+			expectedErr:     "invalid character 'm' looking for beginning of value",
+		},
+		{
+			name:            "targeting", // tests integration with setDefaultsTargeting
+			givenRequest:    openrtb2.BidRequest{Ext: json.RawMessage(`{"prebid":{"targeting":{}}}`)},
+			expectedRequest: openrtb2.BidRequest{Ext: json.RawMessage(`{"prebid":{"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":20,"increment":0.1}]},"includewinners":true,"includebidderkeys":true}}}`)},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			wrapper := &openrtb_ext.RequestWrapper{BidRequest: &test.givenRequest}
+
+			// run
+			err := SetDefaults(wrapper)
+
+			// assert error
+			if len(test.expectedErr) > 0 {
+				assert.EqualError(t, err, test.expectedErr, "Error")
+			}
+
+			// rebuild request
+			require.NoError(t, wrapper.RebuildRequest(), "Rebuild Request")
+
+			// assert
+			if len(test.expectedErr) > 0 {
+				assert.EqualError(t, err, test.expectedErr, "Error")
+				assert.Equal(t, &test.expectedRequest, wrapper.BidRequest, "Request")
+			} else {
+				// assert request as json to ignore order in ext fields
+				expectedRequestJSON, err := json.Marshal(test.expectedRequest)
+				require.NoError(t, err, "Marshal Expected Request")
+
+				actualRequestJSON, err := json.Marshal(wrapper.BidRequest)
+				require.NoError(t, err, "Marshal Actual Request")
+
+				assert.JSONEq(t, string(expectedRequestJSON), string(actualRequestJSON), "Request")
+			}
+		})
+	}
+}
+
+func TestSetDefaultsTargeting(t *testing.T) {
+	defaultGranularity := openrtb_ext.PriceGranularity{
+		Precision: ptrutil.ToPtr(DefaultPriceGranularityPrecision),
+		Ranges:    []openrtb_ext.GranularityRange{{Min: 0, Max: 20, Increment: 0.1}},
+	}
+
+	testCases := []struct {
+		name              string
+		givenTargeting    *openrtb_ext.ExtRequestTargeting
+		expectedTargeting *openrtb_ext.ExtRequestTargeting
+		expectedModified  bool
+	}{
+		{
+			name:              "nil",
+			givenTargeting:    nil,
+			expectedTargeting: nil,
+		},
+		{
+			name:           "empty-targeting",
+			givenTargeting: &openrtb_ext.ExtRequestTargeting{},
+			expectedTargeting: &openrtb_ext.ExtRequestTargeting{
+				PriceGranularity:  &defaultGranularity,
+				IncludeWinners:    ptrutil.ToPtr(DefaultTargetingIncludeWinners),
+				IncludeBidderKeys: ptrutil.ToPtr(DefaultTargetingIncludeBidderKeys),
+			},
+			expectedModified: true,
+		},
+		{
+			name: "populated-partial", // precision and includewinners defaults set
+			givenTargeting: &openrtb_ext.ExtRequestTargeting{
+				PriceGranularity: &openrtb_ext.PriceGranularity{
+					Ranges: []openrtb_ext.GranularityRange{{Min: 0, Max: 10, Increment: 1}},
+				},
+				IncludeBidderKeys: ptrutil.ToPtr(false),
+			},
+			expectedTargeting: &openrtb_ext.ExtRequestTargeting{
+				PriceGranularity: &openrtb_ext.PriceGranularity{
+					Precision: ptrutil.ToPtr(DefaultPriceGranularityPrecision),
+					Ranges:    []openrtb_ext.GranularityRange{{Min: 0, Max: 10, Increment: 1}},
+				},
+				IncludeWinners:    ptrutil.ToPtr(DefaultTargetingIncludeWinners),
+				IncludeBidderKeys: ptrutil.ToPtr(false),
+			},
+			expectedModified: true,
+		},
+		{
+			name: "populated-no-granularity",
+			givenTargeting: &openrtb_ext.ExtRequestTargeting{
+				PriceGranularity:  &openrtb_ext.PriceGranularity{},
+				IncludeWinners:    ptrutil.ToPtr(false),
+				IncludeBidderKeys: ptrutil.ToPtr(false),
+			},
+			expectedTargeting: &openrtb_ext.ExtRequestTargeting{
+				PriceGranularity:  &defaultGranularity,
+				IncludeWinners:    ptrutil.ToPtr(false),
+				IncludeBidderKeys: ptrutil.ToPtr(false),
+			},
+			expectedModified: true,
+		},
+		{
+			name: "populated-ranges-nil",
+			givenTargeting: &openrtb_ext.ExtRequestTargeting{
+				PriceGranularity: &openrtb_ext.PriceGranularity{
+					Precision: ptrutil.ToPtr(4),
+					Ranges:    nil,
+				},
+			},
+			expectedTargeting: &openrtb_ext.ExtRequestTargeting{
+				PriceGranularity:  &defaultGranularity,
+				IncludeWinners:    ptrutil.ToPtr(DefaultTargetingIncludeWinners),
+				IncludeBidderKeys: ptrutil.ToPtr(DefaultTargetingIncludeBidderKeys),
+			},
+			expectedModified: true,
+		},
+		{
+			name: "populated-ranges-empty",
+			givenTargeting: &openrtb_ext.ExtRequestTargeting{
+				PriceGranularity: &openrtb_ext.PriceGranularity{
+					Precision: ptrutil.ToPtr(4),
+					Ranges:    []openrtb_ext.GranularityRange{},
+				},
+			},
+			expectedTargeting: &openrtb_ext.ExtRequestTargeting{
+				PriceGranularity:  &defaultGranularity,
+				IncludeWinners:    ptrutil.ToPtr(DefaultTargetingIncludeWinners),
+				IncludeBidderKeys: ptrutil.ToPtr(DefaultTargetingIncludeBidderKeys),
+			},
+			expectedModified: true,
+		},
+		{
+			name: "populated-full", // no defaults set
+			givenTargeting: &openrtb_ext.ExtRequestTargeting{
+				PriceGranularity: &openrtb_ext.PriceGranularity{
+					Precision: ptrutil.ToPtr(4),
+					Ranges:    []openrtb_ext.GranularityRange{{Min: 0, Max: 10, Increment: 1}},
+				},
+				IncludeWinners:    ptrutil.ToPtr(false),
+				IncludeBidderKeys: ptrutil.ToPtr(false),
+			},
+			expectedTargeting: &openrtb_ext.ExtRequestTargeting{
+				PriceGranularity: &openrtb_ext.PriceGranularity{
+					Precision: ptrutil.ToPtr(4),
+					Ranges:    []openrtb_ext.GranularityRange{{Min: 0, Max: 10, Increment: 1}},
+				},
+				IncludeWinners:    ptrutil.ToPtr(false),
+				IncludeBidderKeys: ptrutil.ToPtr(false),
+			},
+			expectedModified: false,
+		},
+		{
+			name: "setDefaultsPriceGranularity-integration",
+			givenTargeting: &openrtb_ext.ExtRequestTargeting{
+				PriceGranularity: &openrtb_ext.PriceGranularity{
+					Precision: ptrutil.ToPtr(4),
+					Ranges:    []openrtb_ext.GranularityRange{{Min: 5, Max: 10, Increment: 1}},
+				},
+				IncludeWinners:    ptrutil.ToPtr(false),
+				IncludeBidderKeys: ptrutil.ToPtr(false),
+			},
+			expectedTargeting: &openrtb_ext.ExtRequestTargeting{
+				PriceGranularity: &openrtb_ext.PriceGranularity{
+					Precision: ptrutil.ToPtr(4),
+					Ranges:    []openrtb_ext.GranularityRange{{Min: 0, Max: 10, Increment: 1}},
+				},
+				IncludeWinners:    ptrutil.ToPtr(false),
+				IncludeBidderKeys: ptrutil.ToPtr(false),
+			},
+			expectedModified: true,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			actualModified := setDefaultsTargeting(test.givenTargeting)
+			assert.Equal(t, test.expectedModified, actualModified)
+			assert.Equal(t, test.expectedTargeting, test.givenTargeting)
+		})
+	}
+}
+
+func TestSetDefaultsPriceGranularity(t *testing.T) {
+	testCases := []struct {
+		name                string
+		givenGranularity    *openrtb_ext.PriceGranularity
+		expectedGranularity *openrtb_ext.PriceGranularity
+		expectedModified    bool
+	}{
+		{
+			name: "no-precision",
+			givenGranularity: &openrtb_ext.PriceGranularity{
+				Precision: nil,
+				Ranges:    []openrtb_ext.GranularityRange{{Min: 0, Max: 10, Increment: 1}},
+			},
+			expectedGranularity: &openrtb_ext.PriceGranularity{
+				Precision: ptrutil.ToPtr(2),
+				Ranges:    []openrtb_ext.GranularityRange{{Min: 0, Max: 10, Increment: 1}},
+			},
+			expectedModified: true,
+		},
+		{
+			name: "incomplete-range",
+			givenGranularity: &openrtb_ext.PriceGranularity{
+				Precision: ptrutil.ToPtr(2),
+				Ranges:    []openrtb_ext.GranularityRange{{Min: 5, Max: 10, Increment: 1}},
+			},
+			expectedGranularity: &openrtb_ext.PriceGranularity{
+				Precision: ptrutil.ToPtr(2),
+				Ranges:    []openrtb_ext.GranularityRange{{Min: 0, Max: 10, Increment: 1}},
+			},
+			expectedModified: true,
+		},
+		{
+			name: "no-precision+incomplete-range",
+			givenGranularity: &openrtb_ext.PriceGranularity{
+				Precision: nil,
+				Ranges:    []openrtb_ext.GranularityRange{{Min: 5, Max: 10, Increment: 1}},
+			},
+			expectedGranularity: &openrtb_ext.PriceGranularity{
+				Precision: ptrutil.ToPtr(2),
+				Ranges:    []openrtb_ext.GranularityRange{{Min: 0, Max: 10, Increment: 1}},
+			},
+			expectedModified: true,
+		},
+		{
+			name: "all-set",
+			givenGranularity: &openrtb_ext.PriceGranularity{
+				Precision: ptrutil.ToPtr(2),
+				Ranges:    []openrtb_ext.GranularityRange{{Min: 0, Max: 10, Increment: 1}},
+			},
+			expectedGranularity: &openrtb_ext.PriceGranularity{
+				Precision: ptrutil.ToPtr(2),
+				Ranges:    []openrtb_ext.GranularityRange{{Min: 0, Max: 10, Increment: 1}},
+			},
+			expectedModified: false,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			actualModified := setDefaultsPriceGranularity(test.givenGranularity)
+			assert.Equal(t, test.expectedModified, actualModified)
+			assert.Equal(t, test.expectedGranularity, test.givenGranularity)
+		})
+	}
+}
+
+func TestSetDefaultsPriceGranularityRange(t *testing.T) {
+	testCases := []struct {
+		name             string
+		givenRange       []openrtb_ext.GranularityRange
+		expectedRange    []openrtb_ext.GranularityRange
+		expectedModified bool
+	}{
+		{
+			name:             "nil",
+			givenRange:       nil,
+			expectedRange:    nil,
+			expectedModified: false,
+		},
+		{
+			name:             "empty",
+			givenRange:       []openrtb_ext.GranularityRange{},
+			expectedRange:    []openrtb_ext.GranularityRange{},
+			expectedModified: false,
+		},
+		{
+			name:             "one-ok",
+			givenRange:       []openrtb_ext.GranularityRange{{Min: 0, Max: 10, Increment: 1}},
+			expectedRange:    []openrtb_ext.GranularityRange{{Min: 0, Max: 10, Increment: 1}},
+			expectedModified: false,
+		},
+		{
+			name:             "one-fixed",
+			givenRange:       []openrtb_ext.GranularityRange{{Min: 5, Max: 10, Increment: 1}},
+			expectedRange:    []openrtb_ext.GranularityRange{{Min: 0, Max: 10, Increment: 1}},
+			expectedModified: true,
+		},
+		{
+			name:             "many-ok",
+			givenRange:       []openrtb_ext.GranularityRange{{Min: 0, Max: 10, Increment: 1}, {Min: 10, Max: 20, Increment: 1}},
+			expectedRange:    []openrtb_ext.GranularityRange{{Min: 0, Max: 10, Increment: 1}, {Min: 10, Max: 20, Increment: 1}},
+			expectedModified: false,
+		},
+		{
+			name:             "many-fixed",
+			givenRange:       []openrtb_ext.GranularityRange{{Min: 0, Max: 10, Increment: 1}, {Min: 15, Max: 20, Increment: 1}},
+			expectedRange:    []openrtb_ext.GranularityRange{{Min: 0, Max: 10, Increment: 1}, {Min: 10, Max: 20, Increment: 1}},
+			expectedModified: true,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			actualModified := setDefaultsPriceGranularityRange(test.givenRange)
+			assert.Equal(t, test.expectedModified, actualModified)
+			assert.Equal(t, test.expectedRange, test.givenRange)
+		})
+	}
+}
diff --git a/usersync/syncer_test.go b/usersync/syncer_test.go
index 21a86932..6e727c3a 100644
--- a/usersync/syncer_test.go
+++ b/usersync/syncer_test.go
@@ -177,7 +177,7 @@ func TestBuildTemplate(t *testing.T) {
 		key           = "anyKey"
 		syncTypeValue = "x"
 		hostConfig    = config.UserSync{ExternalURL: "http://host.com", RedirectURL: "{{.ExternalURL}}/host"}
-		macroValues   = macros.UserSyncTemplateParams{GDPR: "A", GDPRConsent: "B", USPrivacy: "C"}
+		macroValues   = macros.UserSyncTemplateParams{GDPR: "A", GDPRConsent: "B", USPrivacy: "C", GPP: "D", GPPSID: "1"}
 	)
 
 	testCases := []struct {
@@ -199,11 +199,11 @@ func TestBuildTemplate(t *testing.T) {
 			description: "All Composed Macros",
 			givenSyncerEndpoint: config.SyncerEndpoint{
 				URL:         "https://bidder.com/sync?redirect={{.RedirectURL}}",
-				RedirectURL: "{{.ExternalURL}}/setuid?bidder={{.SyncerKey}}&f={{.SyncType}}&gdpr={{.GDPR}}&uid={{.UserMacro}}",
+				RedirectURL: "{{.ExternalURL}}/setuid?bidder={{.SyncerKey}}&f={{.SyncType}}&gdpr={{.GDPR}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&uid={{.UserMacro}}",
 				ExternalURL: "http://syncer.com",
 				UserMacro:   "$UID$",
 			},
-			expectedRendered: "https://bidder.com/sync?redirect=http%3A%2F%2Fsyncer.com%2Fsetuid%3Fbidder%3DanyKey%26f%3Dx%26gdpr%3DA%26uid%3D%24UID%24",
+			expectedRendered: "https://bidder.com/sync?redirect=http%3A%2F%2Fsyncer.com%2Fsetuid%3Fbidder%3DanyKey%26f%3Dx%26gdpr%3DA%26gpp%3DD%26gpp_sid%3D1%26uid%3D%24UID%24",
 		},
 		{
 			description: "Redirect URL + External URL From Host",