Repository: prebid/prebid-server
Pull request #3106 (2023-09-14 16:29:44)

Cleanup Prometheus Type Conversions
Use generics to eliminate boilerplate code in Prometheus preload type conversions.


Patch:
diff --git a/adapters/infoawarebidder.go b/adapters/infoawarebidder.go
index 321a87a3..538a46ae 100644
--- a/adapters/infoawarebidder.go
+++ b/adapters/infoawarebidder.go
@@ -13,7 +13,7 @@ import (
 // media types defined in the static/bidder-info/{bidder}.yaml file.
 //
 // It adjusts incoming requests in the following ways:
-//  1. If App or Site traffic is not supported by the info file, then requests from
+//  1. If App, Site or DOOH traffic is not supported by the info file, then requests from
 //     those sources will be rejected before the delegate is called.
 //  2. If a given MediaType is not supported for the platform, then it will be set
 //     to nil before the request is forwarded to the delegate.
@@ -24,7 +24,7 @@ type InfoAwareBidder struct {
 	info parsedBidderInfo
 }
 
-// BuildInfoAwareBidder wraps a bidder to enforce site, app, and media type support.
+// BuildInfoAwareBidder wraps a bidder to enforce inventory {site, app, dooh} and media type support.
 func BuildInfoAwareBidder(bidder Bidder, info config.BidderInfo) Bidder {
 	return &InfoAwareBidder{
 		Bidder: bidder,
@@ -47,6 +47,12 @@ func (i *InfoAwareBidder) MakeRequests(request *openrtb2.BidRequest, reqInfo *Ex
 		}
 		allowedMediaTypes = i.info.app
 	}
+	if request.DOOH != nil {
+		if !i.info.dooh.enabled {
+			return nil, []error{&errortypes.Warning{Message: "this bidder does not support dooh requests"}}
+		}
+		allowedMediaTypes = i.info.dooh
+	}
 
 	// Filtering imps is quite expensive (array filter with large, non-pointer elements)... but should be rare,
 	// because it only happens if the publisher makes a really bad request.
@@ -136,6 +142,7 @@ func filterImps(imps []openrtb2.Imp, numToFilter int) ([]openrtb2.Imp, []error)
 type parsedBidderInfo struct {
 	app  parsedSupports
 	site parsedSupports
+	dooh parsedSupports
 }
 
 type parsedSupports struct {
@@ -148,13 +155,22 @@ type parsedSupports struct {
 
 func parseBidderInfo(info config.BidderInfo) parsedBidderInfo {
 	var parsedInfo parsedBidderInfo
-	if info.Capabilities != nil && info.Capabilities.App != nil {
+
+	if info.Capabilities == nil {
+		return parsedInfo
+	}
+
+	if info.Capabilities.App != nil {
 		parsedInfo.app.enabled = true
 		parsedInfo.app.banner, parsedInfo.app.video, parsedInfo.app.audio, parsedInfo.app.native = parseAllowedTypes(info.Capabilities.App.MediaTypes)
 	}
-	if info.Capabilities != nil && info.Capabilities.Site != nil {
+	if info.Capabilities.Site != nil {
 		parsedInfo.site.enabled = true
 		parsedInfo.site.banner, parsedInfo.site.video, parsedInfo.site.audio, parsedInfo.site.native = parseAllowedTypes(info.Capabilities.Site.MediaTypes)
 	}
+	if info.Capabilities.DOOH != nil {
+		parsedInfo.dooh.enabled = true
+		parsedInfo.dooh.banner, parsedInfo.dooh.video, parsedInfo.dooh.audio, parsedInfo.dooh.native = parseAllowedTypes(info.Capabilities.DOOH.MediaTypes)
+	}
 	return parsedInfo
 }
diff --git a/adapters/mabidder/mabidder.go b/adapters/mabidder/mabidder.go
new file mode 100644
index 00000000..184f068b
--- /dev/null
+++ b/adapters/mabidder/mabidder.go
@@ -0,0 +1,99 @@
+package mabidder
+
+import (
+	"encoding/json"
+
+	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+type serverResponse struct {
+	Responses       []bidResponse
+	PrivateIdStatus string `json:"-"`
+}
+
+type bidResponse struct {
+	RequestID         string  `json:"requestId"`
+	Currency          string  `json:"currency"`
+	Width             int32   `json:"width"`
+	Height            int32   `json:"height"`
+	PlacementId       string  `json:"creativeId"`
+	Deal              string  `json:"dealId,omitempty"`
+	NetRevenue        bool    `json:"netRevenue"`
+	TimeToLiveSeconds int32   `json:"ttl"`
+	AdTag             string  `json:"ad"`
+	MediaType         string  `json:"mediaType"`
+	Meta              meta    `json:"meta"`
+	CPM               float32 `json:"cpm"`
+}
+
+type meta struct {
+	AdDomain []string `json:"advertiserDomains"`
+}
+
+type adapter struct {
+	endpoint string
+}
+
+// Builder builds a new instance of the Mabidder adapter for the given bidder with the given config.
+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, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	requestJSON, err := json.Marshal(request)
+	if err != nil {
+		return nil, []error{err}
+	}
+
+	requestData := &adapters.RequestData{
+		Method: "POST",
+		Uri:    a.endpoint,
+		Body:   requestJSON,
+	}
+
+	return []*adapters.RequestData{requestData}, nil
+}
+
+func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	if adapters.IsResponseStatusCodeNoContent(responseData) {
+		return nil, nil
+	}
+
+	if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil {
+		return nil, []error{err}
+	}
+
+	var response serverResponse
+	if err := json.Unmarshal(responseData.Body, &response); err != nil {
+		return nil, []error{err}
+	}
+
+	bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
+	for _, maBidResp := range response.Responses {
+		b := &adapters.TypedBid{
+			Bid: &openrtb2.Bid{
+				ID:      maBidResp.RequestID,
+				ImpID:   maBidResp.RequestID,
+				Price:   float64(maBidResp.CPM),
+				AdM:     maBidResp.AdTag,
+				W:       int64(maBidResp.Width),
+				H:       int64(maBidResp.Height),
+				CrID:    maBidResp.PlacementId,
+				DealID:  maBidResp.Deal,
+				ADomain: maBidResp.Meta.AdDomain,
+			},
+			BidType: openrtb_ext.BidType(maBidResp.MediaType),
+		}
+		bidResponse.Bids = append(bidResponse.Bids, b)
+		if maBidResp.Currency != "" {
+			bidResponse.Currency = maBidResp.Currency
+		}
+	}
+	return bidResponse, nil
+}
diff --git a/adapters/pgamssp/pgamssp.go b/adapters/pgamssp/pgamssp.go
new file mode 100644
index 00000000..378b8e5c
--- /dev/null
+++ b/adapters/pgamssp/pgamssp.go
@@ -0,0 +1,145 @@
+package pgamssp
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+
+	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+type adapter struct {
+	endpoint string
+}
+
+type reqBodyExt struct {
+	PGAMSspBidderExt reqBodyExtBidder `json:"bidder"`
+}
+
+type reqBodyExtBidder struct {
+	Type        string `json:"type"`
+	PlacementID string `json:"placementId,omitempty"`
+	EndpointID  string `json:"endpointId,omitempty"`
+}
+
+func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
+	bidder := &adapter{
+		endpoint: config.Endpoint,
+	}
+	return bidder, nil
+}
+
+func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	var err error
+	var adapterRequests []*adapters.RequestData
+
+	reqCopy := *request
+	for _, imp := range request.Imp {
+		reqCopy.Imp = []openrtb2.Imp{imp}
+
+		var bidderExt adapters.ExtImpBidder
+		var pgamExt openrtb_ext.ImpExtPgamSsp
+
+		if err = json.Unmarshal(reqCopy.Imp[0].Ext, &bidderExt); err != nil {
+			return nil, []error{err}
+		}
+		if err = json.Unmarshal(bidderExt.Bidder, &pgamExt); err != nil {
+			return nil, []error{err}
+		}
+
+		temp := reqBodyExt{PGAMSspBidderExt: reqBodyExtBidder{}}
+
+		if pgamExt.PlacementID != "" {
+			temp.PGAMSspBidderExt.PlacementID = pgamExt.PlacementID
+			temp.PGAMSspBidderExt.Type = "publisher"
+		} else if pgamExt.EndpointID != "" {
+			temp.PGAMSspBidderExt.EndpointID = pgamExt.EndpointID
+			temp.PGAMSspBidderExt.Type = "network"
+		}
+
+		finalyImpExt, err := json.Marshal(temp)
+		if err != nil {
+			return nil, []error{err}
+		}
+
+		reqCopy.Imp[0].Ext = finalyImpExt
+
+		adapterReq, err := a.makeRequest(&reqCopy)
+		if err != nil {
+			return nil, []error{err}
+		}
+
+		if adapterReq != nil {
+			adapterRequests = append(adapterRequests, adapterReq)
+		}
+	}
+	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,
+	}, err
+}
+
+func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	if adapters.IsResponseStatusCodeNoContent(responseData) {
+		return nil, nil
+	}
+
+	if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil {
+		return nil, []error{err}
+	}
+
+	var response openrtb2.BidResponse
+	if err := json.Unmarshal(responseData.Body, &response); err != nil {
+		return nil, []error{err}
+	}
+
+	bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
+	bidResponse.Currency = response.Cur
+	for _, seatBid := range response.SeatBid {
+		for i := range seatBid.Bid {
+			bidType, err := getBidMediaType(&seatBid.Bid[i])
+			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 getBidMediaType(bid *openrtb2.Bid) (openrtb_ext.BidType, error) {
+	switch bid.MType {
+	case openrtb2.MarkupBanner:
+		return openrtb_ext.BidTypeBanner, nil
+	case openrtb2.MarkupVideo:
+		return openrtb_ext.BidTypeVideo, nil
+	case openrtb2.MarkupAudio:
+		return openrtb_ext.BidTypeAudio, nil
+	case openrtb2.MarkupNative:
+		return openrtb_ext.BidTypeNative, nil
+	default:
+		return "", fmt.Errorf("Unable to fetch mediaType in multi-format: %s", bid.ImpID)
+	}
+}
diff --git a/config/account.go b/config/account.go
index e5265072..4b0ceeef 100644
--- a/config/account.go
+++ b/config/account.go
@@ -20,6 +20,7 @@ const (
 	ChannelApp   ChannelType = "app"
 	ChannelVideo ChannelType = "video"
 	ChannelWeb   ChannelType = "web"
+	ChannelDOOH  ChannelType = "dooh"
 )
 
 // Account represents a publisher account configuration
@@ -251,6 +252,7 @@ type AccountChannel struct {
 	App   *bool `mapstructure:"app" json:"app,omitempty"`
 	Video *bool `mapstructure:"video" json:"video,omitempty"`
 	Web   *bool `mapstructure:"web" json:"web,omitempty"`
+	DOOH  *bool `mapstructure:"dooh" json:"dooh,omitempty"`
 }
 
 // GetByChannelType looks up the account integration enabled setting for the specified channel type
@@ -266,6 +268,8 @@ func (a *AccountChannel) GetByChannelType(channelType ChannelType) *bool {
 		channelEnabled = a.Video
 	case ChannelWeb:
 		channelEnabled = a.Web
+	case ChannelDOOH:
+		channelEnabled = a.DOOH
 	}
 
 	return channelEnabled
@@ -296,7 +300,7 @@ func (m AccountModules) ModuleConfig(id string) (json.RawMessage, error) {
 }
 
 func (a *AccountChannel) IsSet() bool {
-	return a.AMP != nil || a.App != nil || a.Video != nil || a.Web != nil
+	return a.AMP != nil || a.App != nil || a.Video != nil || a.Web != nil || a.DOOH != nil
 }
 
 type AccountPrivacy struct {
diff --git a/config/bidderinfo.go b/config/bidderinfo.go
index 96d6fd15..0027e4d2 100644
--- a/config/bidderinfo.go
+++ b/config/bidderinfo.go
@@ -78,6 +78,7 @@ type MaintainerInfo struct {
 type CapabilitiesInfo struct {
 	App  *PlatformInfo `yaml:"app" mapstructure:"app"`
 	Site *PlatformInfo `yaml:"site" mapstructure:"site"`
+	DOOH *PlatformInfo `yaml:"dooh" mapstructure:"dooh"`
 }
 
 // PlatformInfo specifies the supported media types for a bidder.
@@ -461,7 +462,9 @@ func validateAliasCapabilities(aliasBidderInfo BidderInfo, infos BidderInfos, bi
 			return fmt.Errorf("capabilities for alias: %s should be a subset of capabilities for parent bidder: %s", bidderName, aliasBidderInfo.AliasOf)
 		}
 
-		if (aliasBidderInfo.Capabilities.App != nil && parentBidder.Capabilities.App == nil) || (aliasBidderInfo.Capabilities.Site != nil && parentBidder.Capabilities.Site == nil) {
+		if (aliasBidderInfo.Capabilities.App != nil && parentBidder.Capabilities.App == nil) ||
+			(aliasBidderInfo.Capabilities.Site != nil && parentBidder.Capabilities.Site == nil) ||
+			(aliasBidderInfo.Capabilities.DOOH != nil && parentBidder.Capabilities.DOOH == nil) {
 			return fmt.Errorf("capabilities for alias: %s should be a subset of capabilities for parent bidder: %s", bidderName, aliasBidderInfo.AliasOf)
 		}
 
@@ -476,6 +479,12 @@ func validateAliasCapabilities(aliasBidderInfo BidderInfo, infos BidderInfos, bi
 				return err
 			}
 		}
+
+		if aliasBidderInfo.Capabilities.DOOH != nil && parentBidder.Capabilities.DOOH != nil {
+			if err := isAliasPlatformInfoSubsetOfParent(*parentBidder.Capabilities.DOOH, *aliasBidderInfo.Capabilities.DOOH, bidderName, aliasBidderInfo.AliasOf); err != nil {
+				return err
+			}
+		}
 	}
 
 	return nil
@@ -501,8 +510,8 @@ func validateCapabilities(info *CapabilitiesInfo, bidderName string) error {
 		return fmt.Errorf("missing required field: capabilities for adapter: %s", bidderName)
 	}
 
-	if info.App == nil && info.Site == nil {
-		return fmt.Errorf("at least one of capabilities.site or capabilities.app must exist for adapter: %s", bidderName)
+	if info.App == nil && info.Site == nil && info.DOOH == nil {
+		return fmt.Errorf("at least one of capabilities.site, capabilities.app, or capabilities.dooh must exist for adapter: %s", bidderName)
 	}
 
 	if info.App != nil {
@@ -513,9 +522,16 @@ func validateCapabilities(info *CapabilitiesInfo, bidderName string) error {
 
 	if info.Site != nil {
 		if err := validatePlatformInfo(info.Site); err != nil {
-			return fmt.Errorf("capabilities.site failed validation: %v, for adapter: %s", err, bidderName)
+			return fmt.Errorf("capabilities.site failed validation: %v for adapter: %s", err, bidderName)
 		}
 	}
+
+	if info.DOOH != nil {
+		if err := validatePlatformInfo(info.DOOH); err != nil {
+			return fmt.Errorf("capabilities.dooh failed validation: %v for adapter: %s", err, bidderName)
+		}
+	}
+
 	return nil
 }
 
diff --git a/config/config.go b/config/config.go
index e564c34a..f503d0fc 100644
--- a/config/config.go
+++ b/config/config.go
@@ -944,6 +944,8 @@ func SetupViper(v *viper.Viper, filename string, bidderInfos BidderInfos) {
 
 	v.SetDefault("event.timeout_ms", 1000)
 
+	v.SetDefault("user_sync.priority_groups", [][]string{})
+
 	v.SetDefault("accounts.filesystem.enabled", false)
 	v.SetDefault("accounts.filesystem.directorypath", "./stored_requests/data/by_id")
 	v.SetDefault("accounts.in_memory_cache.type", "none")
diff --git a/config/usersync.go b/config/usersync.go
index 27badbcb..d490853d 100644
--- a/config/usersync.go
+++ b/config/usersync.go
@@ -2,13 +2,13 @@ package config
 
 // UserSync specifies the static global user sync configuration.
 type UserSync struct {
-	Cooperative UserSyncCooperative `mapstructure:"coop_sync"`
-	ExternalURL string              `mapstructure:"external_url"`
-	RedirectURL string              `mapstructure:"redirect_url"`
+	Cooperative    UserSyncCooperative `mapstructure:"coop_sync"`
+	ExternalURL    string              `mapstructure:"external_url"`
+	RedirectURL    string              `mapstructure:"redirect_url"`
+	PriorityGroups [][]string          `mapstructure:"priority_groups"`
 }
 
 // UserSyncCooperative specifies the static global default cooperative cookie sync
 type UserSyncCooperative struct {
-	EnabledByDefault bool       `mapstructure:"default"`
-	PriorityGroups   [][]string `mapstructure:"priority_groups"`
+	EnabledByDefault bool `mapstructure:"default"`
 }
diff --git a/endpoints/cookie_sync.go b/endpoints/cookie_sync.go
index 0a81c2d5..250d5fd0 100644
--- a/endpoints/cookie_sync.go
+++ b/endpoints/cookie_sync.go
@@ -39,6 +39,7 @@ var (
 	errCookieSyncAccountBlocked                    = errors.New("account is disabled, please reach out to the prebid server host")
 	errCookieSyncAccountConfigMalformed            = errors.New("account config is malformed and could not be read")
 	errCookieSyncAccountInvalid                    = errors.New("account must be valid if provided, please reach out to the prebid server host")
+	errSyncerIsNotPriority                         = errors.New("syncer key is not a priority, and there are only priority elements left")
 )
 
 var cookieSyncBidderFilterAllowAll = usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude)
@@ -168,7 +169,7 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, ma
 		Bidders: request.Bidders,
 		Cooperative: usersync.Cooperative{
 			Enabled:        (request.CooperativeSync != nil && *request.CooperativeSync) || (request.CooperativeSync == nil && c.config.UserSync.Cooperative.EnabledByDefault),
-			PriorityGroups: c.config.UserSync.Cooperative.PriorityGroups,
+			PriorityGroups: c.config.UserSync.PriorityGroups,
 		},
 		Limit: request.Limit,
 		Privacy: usersyncPrivacy{
diff --git a/endpoints/info/bidders.go b/endpoints/info/bidders.go
index 989f70b8..9984abe2 100644
--- a/endpoints/info/bidders.go
+++ b/endpoints/info/bidders.go
@@ -11,7 +11,8 @@ import (
 	"github.com/prebid/prebid-server/config"
 )
 
-var invalidEnabledOnly = []byte(`Invalid value for 'enabledonly' query param, must be of boolean type`)
+var invalidEnabledOnlyMsg = []byte(`Invalid value for 'enabledonly' query param, must be of boolean type`)
+var invalidBaseAdaptersOnlyMsg = []byte(`Invalid value for 'baseadaptersonly' query param, must be of boolean type`)
 
 // NewBiddersEndpoint builds a handler for the /info/bidders endpoint.
 func NewBiddersEndpoint(bidders config.BidderInfos, aliases map[string]string) httprouter.Handle {
@@ -20,43 +21,74 @@ func NewBiddersEndpoint(bidders config.BidderInfos, aliases map[string]string) h
 		glog.Fatalf("error creating /info/bidders endpoint all bidders response: %v", err)
 	}
 
+	responseAllBaseOnly, err := prepareBiddersResponseAllBaseOnly(bidders)
+	if err != nil {
+		glog.Fatalf("error creating /info/bidders endpoint all bidders (base adapters only) response: %v", err)
+	}
+
 	responseEnabledOnly, err := prepareBiddersResponseEnabledOnly(bidders, aliases)
 	if err != nil {
 		glog.Fatalf("error creating /info/bidders endpoint enabled only response: %v", err)
 	}
 
+	responseEnabledOnlyBaseOnly, err := prepareBiddersResponseEnabledOnlyBaseOnly(bidders)
+	if err != nil {
+		glog.Fatalf("error creating /info/bidders endpoint enabled only (base adapters only) response: %v", err)
+	}
+
 	return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
-		var writeErr error
-		switch readEnabledOnly(r) {
-		case "true":
-			w.Header().Set("Content-Type", "application/json")
-			_, writeErr = w.Write(responseEnabledOnly)
-		case "false":
-			w.Header().Set("Content-Type", "application/json")
-			_, writeErr = w.Write(responseAll)
-		default:
-			w.WriteHeader(http.StatusBadRequest)
-			_, writeErr = w.Write(invalidEnabledOnly)
+		enabledOnly, baseAdaptersOnly, errMsg := readQueryFlags(r)
+		if errMsg != nil {
+			writeBadRequest(w, errMsg)
+			return
 		}
 
-		if writeErr != nil {
-			glog.Errorf("error writing response to /info/bidders: %v", writeErr)
+		var response []byte
+		switch {
+		case !enabledOnly && !baseAdaptersOnly:
+			response = responseAll
+		case !enabledOnly && baseAdaptersOnly:
+			response = responseAllBaseOnly
+		case enabledOnly && !baseAdaptersOnly:
+			response = responseEnabledOnly
+		case enabledOnly && baseAdaptersOnly:
+			response = responseEnabledOnlyBaseOnly
 		}
+		writeResponse(w, response)
+	}
+}
+
+func readQueryFlags(r *http.Request) (enabledOnly, baseAdaptersOnly bool, errMsg []byte) {
+	enabledOnly, ok := readQueryFlag(r, "enabledonly")
+	if !ok {
+		return false, false, invalidEnabledOnlyMsg
+	}
+
+	baseAdapterOnly, ok := readQueryFlag(r, "baseadaptersonly")
+	if !ok {
+		return false, false, invalidBaseAdaptersOnlyMsg
 	}
+
+	return enabledOnly, baseAdapterOnly, nil
 }
 
-func readEnabledOnly(r *http.Request) string {
+func readQueryFlag(r *http.Request, queryParam string) (flag, ok bool) {
 	q := r.URL.Query()
 
-	v, exists := q["enabledonly"]
+	v, exists := q[queryParam]
 
 	if !exists || len(v) == 0 {
-		// if the enabledOnly query parameter is not specified, default to false to match
-		// previous behavior of returning all adapters regardless of their enabled status.
-		return "false"
+		return false, true
 	}
 
-	return strings.ToLower(v[0])
+	switch strings.ToLower(v[0]) {
+	case "true":
+		return true, true
+	case "false":
+		return false, true
+	default:
+		return false, false
+	}
 }
 
 func prepareBiddersResponseAll(bidders config.BidderInfos, aliases map[string]string) ([]byte, error) {
@@ -71,7 +103,19 @@ func prepareBiddersResponseAll(bidders config.BidderInfos, aliases map[string]st
 	}
 
 	sort.Strings(bidderNames)
+	return json.Marshal(bidderNames)
+}
 
+func prepareBiddersResponseAllBaseOnly(bidders config.BidderInfos) ([]byte, error) {
+	bidderNames := make([]string, 0, len(bidders))
+
+	for name, info := range bidders {
+		if len(info.AliasOf) == 0 {
+			bidderNames = append(bidderNames, name)
+		}
+	}
+
+	sort.Strings(bidderNames)
 	return json.Marshal(bidderNames)
 }
 
@@ -94,3 +138,32 @@ func prepareBiddersResponseEnabledOnly(bidders config.BidderInfos, aliases map[s
 
 	return json.Marshal(bidderNames)
 }
+
+func prepareBiddersResponseEnabledOnlyBaseOnly(bidders config.BidderInfos) ([]byte, error) {
+	bidderNames := make([]string, 0, len(bidders))
+
+	for name, info := range bidders {
+		if info.IsEnabled() && len(info.AliasOf) == 0 {
+			bidderNames = append(bidderNames, name)
+		}
+	}
+
+	sort.Strings(bidderNames)
+	return json.Marshal(bidderNames)
+}
+
+func writeBadRequest(w http.ResponseWriter, data []byte) {
+	w.WriteHeader(http.StatusBadRequest)
+	writeWithErrorHandling(w, data)
+}
+
+func writeResponse(w http.ResponseWriter, data []byte) {
+	w.Header().Set("Content-Type", "application/json")
+	writeWithErrorHandling(w, data)
+}
+
+func writeWithErrorHandling(w http.ResponseWriter, data []byte) {
+	if _, err := w.Write(data); err != nil {
+		glog.Errorf("error writing response to /info/bidders: %v", err)
+	}
+}
diff --git a/endpoints/info/bidders_detail.go b/endpoints/info/bidders_detail.go
index 1446e3ac..9b8d4268 100644
--- a/endpoints/info/bidders_detail.go
+++ b/endpoints/info/bidders_detail.go
@@ -122,6 +122,7 @@ type maintainer struct {
 type capabilities struct {
 	App  *platform `json:"app,omitempty"`
 	Site *platform `json:"site,omitempty"`
+	DOOH *platform `json:"dooh,omitempty"`
 }
 
 type platform struct {
@@ -157,6 +158,12 @@ func mapDetailFromConfig(c config.BidderInfo) bidderDetail {
 					MediaTypes: mapMediaTypes(c.Capabilities.Site.MediaTypes),
 				}
 			}
+
+			if c.Capabilities.DOOH != nil {
+				bidderDetail.Capabilities.DOOH = &platform{
+					MediaTypes: mapMediaTypes(c.Capabilities.DOOH.MediaTypes),
+				}
+			}
 		}
 	} else {
 		bidderDetail.Status = statusDisabled
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index aeb5c20e..375756cc 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -69,13 +69,16 @@ var (
 )
 
 var accountIdSearchPath = [...]struct {
-	isApp bool
-	key   []string
+	isApp  bool
+	isDOOH bool
+	key    []string
 }{
-	{true, []string{"app", "publisher", "ext", openrtb_ext.PrebidExtKey, "parentAccount"}},
-	{true, []string{"app", "publisher", "id"}},
-	{false, []string{"site", "publisher", "ext", openrtb_ext.PrebidExtKey, "parentAccount"}},
-	{false, []string{"site", "publisher", "id"}},
+	{true, false, []string{"app", "publisher", "ext", openrtb_ext.PrebidExtKey, "parentAccount"}},
+	{true, false, []string{"app", "publisher", "id"}},
+	{false, false, []string{"site", "publisher", "ext", openrtb_ext.PrebidExtKey, "parentAccount"}},
+	{false, false, []string{"site", "publisher", "id"}},
+	{false, true, []string{"dooh", "publisher", "ext", openrtb_ext.PrebidExtKey, "parentAccount"}},
+	{false, true, []string{"dooh", "publisher", "id"}},
 }
 
 func NewEndpoint(
@@ -457,12 +460,16 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request, labels *metric
 		return
 	}
 
-	accountId, isAppReq, errs := getAccountIdFromRawRequest(hasStoredBidRequest, storedRequests[storedBidRequestId], requestJson)
+	accountId, isAppReq, isDOOHReq, errs := getAccountIdFromRawRequest(hasStoredBidRequest, storedRequests[storedBidRequestId], requestJson)
 	// fill labels here in order to pass correct metrics in case of errors
 	if isAppReq {
 		labels.Source = metrics.DemandApp
 		labels.RType = metrics.ReqTypeORTB2App
 		labels.PubID = accountId
+	} else if isDOOHReq {
+		labels.Source = metrics.DemandDOOH
+		labels.RType = metrics.ReqTypeORTB2DOOH
+		labels.PubID = accountId
 	} else { // is Site request
 		labels.Source = metrics.DemandWeb
 		labels.PubID = accountId
@@ -804,8 +811,8 @@ func (deps *endpointDeps) validateRequest(req *openrtb_ext.RequestWrapper, isAmp
 		return []error{err}
 	}
 
-	if (req.Site == nil && req.App == nil) || (req.Site != nil && req.App != nil) {
-		return append(errL, errors.New("request.site or request.app must be defined, but not both."))
+	if err := validateExactlyOneInventoryType(req); err != nil {
+		return []error{err}
 	}
 
 	if errs := validateRequestExt(req); len(errs) != 0 {
@@ -823,6 +830,9 @@ func (deps *endpointDeps) validateRequest(req *openrtb_ext.RequestWrapper, isAmp
 		return append(errL, err)
 	}
 
+	if err := deps.validateDOOH(req); err != nil {
+		return append(errL, err)
+	}
 	var gpp gpplib.GppContainer
 	if req.BidRequest.Regs != nil && len(req.BidRequest.Regs.GPP) > 0 {
 		gpp, err = gpplib.Parse(req.BidRequest.Regs.GPP)
@@ -1747,6 +1757,18 @@ func (deps *endpointDeps) validateApp(req *openrtb_ext.RequestWrapper) error {
 	return err
 }
 
+func (deps *endpointDeps) validateDOOH(req *openrtb_ext.RequestWrapper) error {
+	if req.DOOH == nil {
+		return nil
+	}
+
+	if req.DOOH.ID == "" && len(req.DOOH.VenueType) == 0 {
+		return errors.New("request.dooh should include at least one of request.dooh.id or request.dooh.venuetype.")
+	}
+
+	return nil
+}
+
 func (deps *endpointDeps) validateUser(req *openrtb_ext.RequestWrapper, aliases map[string]string, gpp gpplib.GppContainer) []error {
 	var errL []error
 
@@ -1872,6 +1894,30 @@ func validateDevice(device *openrtb2.Device) error {
 	return nil
 }
 
+func validateExactlyOneInventoryType(reqWrapper *openrtb_ext.RequestWrapper) error {
+
+	// Prep for mutual exclusion check
+	invTypeNumMatches := 0
+	if reqWrapper.Site != nil {
+		invTypeNumMatches++
+	}
+	if reqWrapper.App != nil {
+		invTypeNumMatches++
+	}
+	if reqWrapper.DOOH != nil {
+		invTypeNumMatches++
+	}
+
+	if invTypeNumMatches == 0 {
+		return errors.New("One of request.site or request.app or request.dooh must be defined")
+	} else if invTypeNumMatches >= 2 {
+		return errors.New("No more than one of request.site or request.app or request.dooh can be defined")
+	} else {
+		return nil
+	}
+
+}
+
 func validateOrFillChannel(reqWrapper *openrtb_ext.RequestWrapper, isAmp bool) error {
 	requestExt, err := reqWrapper.GetRequestExt()
 	if err != nil {
@@ -1934,9 +1980,9 @@ func (deps *endpointDeps) setFieldsImplicitly(httpReq *http.Request, r *openrtb_
 
 	setDeviceImplicitly(httpReq, r, deps.privateNetworkIPValidator)
 
-	// Per the OpenRTB spec: A bid request must not contain both a Site and an App object. If neither are
-	// present, we'll assume it's a site request.
-	if r.App == nil {
+	// Per the OpenRTB spec: A bid request must not contain more than one of Site|App|DOOH
+	// Assume it's a site request if it's not declared as one of the other values
+	if r.App == nil && r.DOOH == nil {
 		setSiteImplicitly(httpReq, r)
 	}
 
@@ -2325,43 +2371,43 @@ func getAccountID(pub *openrtb2.Publisher) string {
 	return metrics.PublisherUnknown
 }
 
-func getAccountIdFromRawRequest(hasStoredRequest bool, storedRequest json.RawMessage, originalRequest []byte) (string, bool, []error) {
+func getAccountIdFromRawRequest(hasStoredRequest bool, storedRequest json.RawMessage, originalRequest []byte) (string, bool, bool, []error) {
 	request := originalRequest
 	if hasStoredRequest {
 		request = storedRequest
 	}
 
-	accountId, isAppReq, err := searchAccountId(request)
+	accountId, isAppReq, isDOOHReq, err := searchAccountId(request)
 	if err != nil {
-		return "", isAppReq, []error{err}
+		return "", isAppReq, isDOOHReq, []error{err}
 	}
 
 	// In case the stored request did not have account data we specifically search it in the original request
 	if accountId == "" && hasStoredRequest {
-		accountId, _, err = searchAccountId(originalRequest)
+		accountId, _, _, err = searchAccountId(originalRequest)
 		if err != nil {
-			return "", isAppReq, []error{err}
+			return "", isAppReq, isDOOHReq, []error{err}
 		}
 	}
 
 	if accountId == "" {
-		return metrics.PublisherUnknown, isAppReq, nil
+		return metrics.PublisherUnknown, isAppReq, isDOOHReq, nil
 	}
 
-	return accountId, isAppReq, nil
+	return accountId, isAppReq, isDOOHReq, nil
 }
 
-func searchAccountId(request []byte) (string, bool, error) {
+func searchAccountId(request []byte) (string, bool, bool, error) {
 	for _, path := range accountIdSearchPath {
 		accountId, exists, err := getStringValueFromRequest(request, path.key)
 		if err != nil {
-			return "", path.isApp, err
+			return "", path.isApp, path.isDOOH, err
 		}
 		if exists {
-			return accountId, path.isApp, nil
+			return accountId, path.isApp, path.isDOOH, nil
 		}
 	}
-	return "", false, nil
+	return "", false, false, nil
 }
 
 func getStringValueFromRequest(request []byte, key []string) (string, bool, error) {
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/no-site-or-app.json b/endpoints/openrtb2/sample-requests/invalid-whole/no-site-or-app-or-dooh.json
similarity index 85%
rename from endpoints/openrtb2/sample-requests/invalid-whole/no-site-or-app.json
rename to endpoints/openrtb2/sample-requests/invalid-whole/no-site-or-app-or-dooh.json
index 8ec2273e..c883979c 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/no-site-or-app.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/no-site-or-app-or-dooh.json
@@ -1,5 +1,5 @@
 {
-  "description": "Request does not come with site field nor app field",
+  "description": "Request does not come with site nor app nor dooh field",
   "mockBidRequest": {
     "id": "req-id",
     "imp": [
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/site-app-both.json b/endpoints/openrtb2/sample-requests/invalid-whole/site-app-both.json
index 5bc3054c..255ea59e 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/site-app-both.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/site-app-both.json
@@ -1,5 +1,5 @@
 {
-  "description": "Bid request comes with both site and app fields, it should only come with one or the other",
+  "description": "Bid request comes with both site and app fields, it should only come with one",
   "mockBidRequest": {
     "id": "req-id",
     "site": {
@@ -23,5 +23,5 @@
     ]
   },
   "expectedReturnCode": 400,
-  "expectedErrorMessage": "Invalid request: request.site or request.app must be defined, but not both.\n"
+  "expectedErrorMessage": "Invalid request: No more than one of request.site or request.app or request.dooh can be defined\n"
 }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/site-dooh-both.json b/endpoints/openrtb2/sample-requests/invalid-whole/site-dooh-both.json
new file mode 100644
index 00000000..d4760d12
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/site-dooh-both.json
@@ -0,0 +1,27 @@
+{
+  "description": "Bid request comes with both site and dooh fields, it should only come with one",
+  "mockBidRequest": {
+    "id": "req-id",
+    "site": {
+      "page": "test.mysite.com"
+    },
+    "dooh": {},
+    "imp": [
+      {
+        "id": "imp-id",
+        "video": {
+          "mimes": [
+            "video/mp4"
+            ]
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          }
+        }
+      }
+    ]
+  },
+  "expectedReturnCode": 400,
+  "expectedErrorMessage": "Invalid request: No more than one of request.site or request.app or request.dooh can be defined\n"
+}
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/dooh.json b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/dooh.json
new file mode 100644
index 00000000..faa9a7f6
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/dooh.json
@@ -0,0 +1,53 @@
+{
+  "description": "Simple DOOH request",
+  "config": {
+    "mockBidders": [
+      {"bidderName": "appnexus", "currency": "USD", "price": 0.00}
+    ]
+  },
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "dooh": {
+      "id": "12345"
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "banner": {
+          "format": [
+            {
+              "w": 1920,
+              "h": 1080
+            }
+          ]
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          }
+        }
+      }
+    ],
+    "tmax": 500,
+    "ext": {}
+  },
+  "expectedBidResponse": {
+    "id": "some-request-id",
+    "seatbid": [
+      {
+        "bid": [
+          {
+            "id": "appnexus-bid",
+            "impid": "some-impression-id",
+            "price": 0
+          }
+        ],
+        "seat": "appnexus"
+      }
+    ],
+    "bidid": "test bid id",
+    "cur": "USD",
+    "nbr": 0
+  },
+  "expectedReturnCode": 200
+}
diff --git a/endpoints/setuid.go b/endpoints/setuid.go
index 64178444..520a133b 100644
--- a/endpoints/setuid.go
+++ b/endpoints/setuid.go
@@ -50,9 +50,7 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use
 
 		cookie := usersync.ReadCookie(r, decoder, &cfg.HostCookie)
 		if !cookie.AllowSyncs() {
-			w.WriteHeader(http.StatusUnauthorized)
-			metricsEngine.RecordSetUid(metrics.SetUidOptOut)
-			so.Status = http.StatusUnauthorized
+			handleBadStatus(w, http.StatusUnauthorized, metrics.SetUidOptOut, nil, metricsEngine, &so)
 			return
 		}
 		usersync.SyncHostCookie(r, cookie, &cfg.HostCookie)
@@ -61,22 +59,14 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use
 
 		syncer, bidderName, err := getSyncer(query, syncersByBidder)
 		if err != nil {
-			w.WriteHeader(http.StatusBadRequest)
-			w.Write([]byte(err.Error()))
-			metricsEngine.RecordSetUid(metrics.SetUidSyncerUnknown)
-			so.Errors = []error{err}
-			so.Status = http.StatusBadRequest
+			handleBadStatus(w, http.StatusBadRequest, metrics.SetUidSyncerUnknown, err, metricsEngine, &so)
 			return
 		}
 		so.Bidder = syncer.Key()
 
 		responseFormat, err := getResponseFormat(query, syncer)
 		if err != nil {
-			w.WriteHeader(http.StatusBadRequest)
-			w.Write([]byte(err.Error()))
-			metricsEngine.RecordSetUid(metrics.SetUidBadRequest)
-			so.Errors = []error{err}
-			so.Status = http.StatusBadRequest
+			handleBadStatus(w, http.StatusBadRequest, metrics.SetUidBadRequest, err, metricsEngine, &so)
 			return
 		}
 
@@ -86,21 +76,19 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use
 		}
 		account, fetchErrs := accountService.GetAccount(context.Background(), cfg, accountsFetcher, accountID, metricsEngine)
 		if len(fetchErrs) > 0 {
-			w.WriteHeader(http.StatusBadRequest)
+			var metricValue metrics.SetUidStatus
 			err := combineErrors(fetchErrs)
-			w.Write([]byte(err.Error()))
 			switch err {
 			case errCookieSyncAccountBlocked:
-				metricsEngine.RecordSetUid(metrics.SetUidAccountBlocked)
+				metricValue = metrics.SetUidAccountBlocked
 			case errCookieSyncAccountConfigMalformed:
-				metricsEngine.RecordSetUid(metrics.SetUidAccountConfigMalformed)
+				metricValue = metrics.SetUidAccountConfigMalformed
 			case errCookieSyncAccountInvalid:
-				metricsEngine.RecordSetUid(metrics.SetUidAccountInvalid)
+				metricValue = metrics.SetUidAccountInvalid
 			default:
-				metricsEngine.RecordSetUid(metrics.SetUidBadRequest)
+				metricValue = metrics.SetUidBadRequest
 			}
-			so.Errors = []error{err}
-			so.Status = http.StatusBadRequest
+			handleBadStatus(w, http.StatusBadRequest, metricValue, err, metricsEngine, &so)
 			return
 		}
 
@@ -134,11 +122,7 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use
 		if err != nil {
 			// Only exit if non-warning
 			if !errortypes.IsWarning(err) {
-				w.WriteHeader(http.StatusBadRequest)
-				w.Write([]byte(err.Error()))
-				metricsEngine.RecordSetUid(metrics.SetUidBadRequest)
-				so.Errors = []error{err}
-				so.Status = http.StatusBadRequest
+				handleBadStatus(w, http.StatusBadRequest, metrics.SetUidBadRequest, err, metricsEngine, &so)
 				return
 			}
 			w.Write([]byte("Warning: " + err.Error()))
@@ -147,16 +131,14 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use
 		tcf2Cfg := tcf2CfgBuilder(cfg.GDPR.TCF2, account.GDPR)
 
 		if shouldReturn, status, body := preventSyncsGDPR(gdprRequestInfo, gdprPermsBuilder, tcf2Cfg); shouldReturn {
-			w.WriteHeader(status)
-			w.Write([]byte(body))
+			var metricValue metrics.SetUidStatus
 			switch status {
 			case http.StatusBadRequest:
-				metricsEngine.RecordSetUid(metrics.SetUidBadRequest)
+				metricValue = metrics.SetUidBadRequest
 			case http.StatusUnavailableForLegalReasons:
-				metricsEngine.RecordSetUid(metrics.SetUidGDPRHostCookieBlocked)
+				metricValue = metrics.SetUidGDPRHostCookieBlocked
 			}
-			so.Errors = []error{errors.New(body)}
-			so.Status = status
+			handleBadStatus(w, status, metricValue, errors.New(body), metricsEngine, &so)
 			return
 		}
 
@@ -176,14 +158,22 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use
 
 		setSiteCookie := siteCookieCheck(r.UserAgent())
 
+		// Priority Ejector Set Up
+		priorityEjector := &usersync.PriorityBidderEjector{PriorityGroups: cfg.UserSync.PriorityGroups, TieEjector: &usersync.OldestEjector{}, SyncersByBidder: syncersByBidder}
+		priorityEjector.IsSyncerPriority = isSyncerPriority(bidderName, cfg.UserSync.PriorityGroups)
+
 		// Write Cookie
-		encodedCookie, err := cookie.PrepareCookieForWrite(&cfg.HostCookie, encoder)
+		encodedCookie, err := cookie.PrepareCookieForWrite(&cfg.HostCookie, encoder, priorityEjector)
 		if err != nil {
-			w.WriteHeader(http.StatusBadRequest)
-			metricsEngine.RecordSetUid(metrics.SetUidBadRequest)
-			so.Errors = []error{err}
-			so.Status = http.StatusBadRequest
-			return
+			if err.Error() == errSyncerIsNotPriority.Error() {
+				w.WriteHeader(http.StatusOK)
+				w.Write([]byte("Warning: " + err.Error() + ", cookie not updated"))
+				so.Status = http.StatusOK
+				return
+			} else {
+				handleBadStatus(w, http.StatusBadRequest, metrics.SetUidBadRequest, err, metricsEngine, &so)
+				return
+			}
 		}
 		usersync.WriteCookie(w, encodedCookie, &cfg.HostCookie, setSiteCookie)
 
@@ -347,6 +337,17 @@ func getSyncer(query url.Values, syncersByBidder map[string]usersync.Syncer) (us
 	return syncer, bidder, nil
 }
 
+func isSyncerPriority(bidderNameFromSyncerQuery string, priorityGroups [][]string) bool {
+	for _, group := range priorityGroups {
+		for _, bidder := range group {
+			if bidderNameFromSyncerQuery == bidder {
+				return true
+			}
+		}
+	}
+	return false
+}
+
 // getResponseFormat reads the format query parameter or falls back to the syncer's default.
 // Returns either "b" (iframe), "i" (redirect), or an empty string "" (legacy behavior of an
 // empty response body with no content type).
@@ -422,3 +423,14 @@ func preventSyncsGDPR(gdprRequestInfo gdpr.RequestInfo, permsBuilder gdpr.Permis
 
 	return true, http.StatusUnavailableForLegalReasons, "The gdpr_consent string prevents cookies from being saved"
 }
+
+func handleBadStatus(w http.ResponseWriter, status int, metricValue metrics.SetUidStatus, err error, me metrics.MetricsEngine, so *analytics.SetUIDObject) {
+	w.WriteHeader(status)
+	me.RecordSetUid(metricValue)
+	so.Status = status
+
+	if err != nil {
+		so.Errors = []error{err}
+		w.Write([]byte(err.Error()))
+	}
+}
diff --git a/errortypes/code.go b/errortypes/code.go
index c68eb196..ef8287f2 100644
--- a/errortypes/code.go
+++ b/errortypes/code.go
@@ -14,6 +14,7 @@ const (
 	NoConversionRateErrorCode
 	MalformedAcctErrorCode
 	ModuleRejectionErrorCode
+	TmaxTimeoutErrorCode
 )
 
 // Defines numeric codes for well-known warnings.
diff --git a/errortypes/errortypes.go b/errortypes/errortypes.go
index aff04822..01a5cca4 100644
--- a/errortypes/errortypes.go
+++ b/errortypes/errortypes.go
@@ -20,6 +20,25 @@ func (err *Timeout) Severity() Severity {
 	return SeverityFatal
 }
 
+// TmaxTimeout should be used to flag that remaining tmax duration is not enough to get response from bidder
+//
+// TmaxTimeout will not be written to the app log, since it's not an actionable item for the Prebid Server hosts.
+type TmaxTimeout struct {
+	Message string
+}
+
+func (err *TmaxTimeout) Error() string {
+	return err.Message
+}
+
+func (err *TmaxTimeout) Code() int {
+	return TmaxTimeoutErrorCode
+}
+
+func (err *TmaxTimeout) Severity() Severity {
+	return SeverityFatal
+}
+
 // BadInput should be used when returning errors which are caused by bad input.
 // It should _not_ be used if the error is a server-side issue (e.g. failed to send the external request).
 //
diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go
index c5e9dc49..395e4fbb 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -113,6 +113,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/logan"
 	"github.com/prebid/prebid-server/adapters/logicad"
 	"github.com/prebid/prebid-server/adapters/lunamedia"
+	mabidder "github.com/prebid/prebid-server/adapters/mabidder"
 	"github.com/prebid/prebid-server/adapters/madvertise"
 	"github.com/prebid/prebid-server/adapters/marsmedia"
 	"github.com/prebid/prebid-server/adapters/medianet"
@@ -133,6 +134,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/outbrain"
 	"github.com/prebid/prebid-server/adapters/ownadx"
 	"github.com/prebid/prebid-server/adapters/pangle"
+	"github.com/prebid/prebid-server/adapters/pgamssp"
 	"github.com/prebid/prebid-server/adapters/pubmatic"
 	"github.com/prebid/prebid-server/adapters/pubnative"
 	"github.com/prebid/prebid-server/adapters/pulsepoint"
@@ -316,6 +318,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderLogan:             logan.Builder,
 		openrtb_ext.BidderLogicad:           logicad.Builder,
 		openrtb_ext.BidderLunaMedia:         lunamedia.Builder,
+		openrtb_ext.BidderMabidder:          mabidder.Builder,
 		openrtb_ext.BidderMadvertise:        madvertise.Builder,
 		openrtb_ext.BidderMarsmedia:         marsmedia.Builder,
 		openrtb_ext.BidderMediafuse:         appnexus.Builder,
@@ -338,6 +341,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderOwnAdx:            ownadx.Builder,
 		openrtb_ext.BidderPangle:            pangle.Builder,
 		openrtb_ext.BidderPGAM:              adtelligent.Builder,
+		openrtb_ext.BidderPGAMSsp:           pgamssp.Builder,
 		openrtb_ext.BidderPubmatic:          pubmatic.Builder,
 		openrtb_ext.BidderPubnative:         pubnative.Builder,
 		openrtb_ext.BidderPulsepoint:        pulsepoint.Builder,
diff --git a/exchange/bidder.go b/exchange/bidder.go
index 838c46f7..dfdb7cdf 100644
--- a/exchange/bidder.go
+++ b/exchange/bidder.go
@@ -88,8 +88,6 @@ const (
 	Gzip string = "GZIP"
 )
 
-var errTmaxTimeout = errors.New("exceeded tmax duration")
-
 // AdaptBidder converts an adapters.Bidder into an exchange.AdaptedBidder.
 //
 // The name refers to the "Adapter" architecture pattern, and should not be confused with a Prebid "Adapter"
@@ -551,7 +549,7 @@ func (bidder *bidderAdapter) doRequestImpl(ctx context.Context, req *adapters.Re
 			bidder.me.RecordTMaxTimeout()
 			return &httpCallInfo{
 				request: req,
-				err:     errTmaxTimeout,
+				err:     &errortypes.TmaxTimeout{Message: "exceeded tmax duration"},
 			}
 		}
 	}
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 136f1040..7544b4a7 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -844,6 +844,8 @@ func errorsToMetric(errs []error) map[metrics.AdapterError]struct{} {
 			ret[metrics.AdapterErrorFailedToRequestBids] = s
 		case errortypes.AlternateBidderCodeWarningCode:
 			ret[metrics.AdapterErrorValidation] = s
+		case errortypes.TmaxTimeoutErrorCode:
+			ret[metrics.AdapterErrorTmaxTimeout] = s
 		default:
 			ret[metrics.AdapterErrorUnknown] = s
 		}
diff --git a/exchange/utils.go b/exchange/utils.go
index 0becca21..6c47bcaa 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -29,10 +29,11 @@ import (
 )
 
 var channelTypeMap = map[metrics.RequestType]config.ChannelType{
-	metrics.ReqTypeAMP:      config.ChannelAMP,
-	metrics.ReqTypeORTB2App: config.ChannelApp,
-	metrics.ReqTypeVideo:    config.ChannelVideo,
-	metrics.ReqTypeORTB2Web: config.ChannelWeb,
+	metrics.ReqTypeAMP:       config.ChannelAMP,
+	metrics.ReqTypeORTB2App:  config.ChannelApp,
+	metrics.ReqTypeVideo:     config.ChannelVideo,
+	metrics.ReqTypeORTB2Web:  config.ChannelWeb,
+	metrics.ReqTypeORTB2DOOH: config.ChannelDOOH,
 }
 
 const unknownBidder string = ""
diff --git a/metrics/metrics.go b/metrics/metrics.go
index c30f8167..a73019be 100644
--- a/metrics/metrics.go
+++ b/metrics/metrics.go
@@ -164,6 +164,7 @@ const PublisherUnknown = "unknown"
 const (
 	DemandWeb     DemandSource = "web"
 	DemandApp     DemandSource = "app"
+	DemandDOOH    DemandSource = "dooh"
 	DemandUnknown DemandSource = "unknown"
 )
 
@@ -171,22 +172,25 @@ func DemandTypes() []DemandSource {
 	return []DemandSource{
 		DemandWeb,
 		DemandApp,
+		DemandDOOH,
 		DemandUnknown,
 	}
 }
 
 // The request types (endpoints)
 const (
-	ReqTypeORTB2Web RequestType = "openrtb2-web"
-	ReqTypeORTB2App RequestType = "openrtb2-app"
-	ReqTypeAMP      RequestType = "amp"
-	ReqTypeVideo    RequestType = "video"
+	ReqTypeORTB2Web  RequestType = "openrtb2-web"
+	ReqTypeORTB2App  RequestType = "openrtb2-app"
+	ReqTypeORTB2DOOH RequestType = "openrtb2-dooh"
+	ReqTypeAMP       RequestType = "amp"
+	ReqTypeVideo     RequestType = "video"
 )
 
 func RequestTypes() []RequestType {
 	return []RequestType{
 		ReqTypeORTB2Web,
 		ReqTypeORTB2App,
+		ReqTypeORTB2DOOH,
 		ReqTypeAMP,
 		ReqTypeVideo,
 	}
@@ -267,6 +271,7 @@ const (
 	AdapterErrorTimeout             AdapterError = "timeout"
 	AdapterErrorFailedToRequestBids AdapterError = "failedtorequestbid"
 	AdapterErrorValidation          AdapterError = "validation"
+	AdapterErrorTmaxTimeout         AdapterError = "tmaxtimeout"
 	AdapterErrorUnknown             AdapterError = "unknown_error"
 )
 
diff --git a/metrics/prometheus/preload.go b/metrics/prometheus/preload.go
index dae7c14d..59f70cfb 100644
--- a/metrics/prometheus/preload.go
+++ b/metrics/prometheus/preload.go
@@ -2,28 +2,30 @@ package prometheusmetrics
 
 import (
 	"github.com/prebid/prebid-server/metrics"
+	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prometheus/client_golang/prometheus"
 )
 
 func preloadLabelValues(m *Metrics, syncerKeys []string, moduleStageNames map[string][]string) {
 	var (
-		setUidStatusValues        = setUidStatusesAsString()
-		adapterErrorValues        = adapterErrorsAsString()
-		adapterValues             = adaptersAsString()
+		adapterErrorValues        = enumAsString(metrics.AdapterErrors())
+		adapterValues             = enumAsString(openrtb_ext.CoreBidderNames())
 		bidTypeValues             = []string{markupDeliveryAdm, markupDeliveryNurl}
 		boolValues                = boolValuesAsString()
-		cacheResultValues         = cacheResultsAsString()
+		cacheResultValues         = enumAsString(metrics.CacheResults())
 		connectionErrorValues     = []string{connectionAcceptError, connectionCloseError}
-		cookieValues              = cookieTypesAsString()
-		cookieSyncStatusValues    = cookieSyncStatusesAsString()
-		overheadTypes             = overheadTypesAsString()
-		requestTypeValues         = requestTypesAsString()
-		requestStatusValues       = requestStatusesAsString()
-		storedDataFetchTypeValues = storedDataFetchTypesAsString()
-		storedDataErrorValues     = storedDataErrorsAsString()
-		syncerRequestStatusValues = syncerRequestStatusesAsString()
-		syncerSetsStatusValues    = syncerSetStatusesAsString()
+		cookieSyncStatusValues    = enumAsString(metrics.CookieSyncStatuses())
+		cookieValues              = enumAsString(metrics.CookieTypes())
+		overheadTypes             = enumAsString(metrics.OverheadTypes())
+		requestStatusValues       = enumAsString(metrics.RequestStatuses())
+		requestTypeValues         = enumAsString(metrics.RequestTypes())
+		setUidStatusValues        = enumAsString(metrics.SetUidStatuses())
 		sourceValues              = []string{sourceRequest}
+		storedDataErrorValues     = enumAsString(metrics.StoredDataErrors())
+		storedDataFetchTypeValues = enumAsString(metrics.StoredDataFetchTypes())
+		syncerRequestStatusValues = enumAsString(metrics.SyncerRequestStatuses())
+		syncerSetsStatusValues    = enumAsString(metrics.SyncerSetUidStatuses())
+		tcfVersionValues          = enumAsString(metrics.TCFVersions())
 	)
 
 	preloadLabelValuesForCounter(m.connectionsError, map[string][]string{
@@ -224,7 +226,7 @@ func preloadLabelValues(m *Metrics, syncerKeys []string, moduleStageNames map[st
 
 	preloadLabelValuesForCounter(m.privacyTCF, map[string][]string{
 		sourceLabel:  sourceValues,
-		versionLabel: tcfVersionsAsString(),
+		versionLabel: tcfVersionValues,
 	})
 
 	if !m.metricsDisabled.AdapterGDPRRequestBlocked {
diff --git a/metrics/prometheus/type_conversion.go b/metrics/prometheus/type_conversion.go
index 07e4e89c..0ae5366b 100644
--- a/metrics/prometheus/type_conversion.go
+++ b/metrics/prometheus/type_conversion.go
@@ -2,22 +2,9 @@ package prometheusmetrics
 
 import (
 	"strconv"
-
-	"github.com/prebid/prebid-server/metrics"
-	"github.com/prebid/prebid-server/openrtb_ext"
 )
 
-func adaptersAsString() []string {
-	values := openrtb_ext.CoreBidderNames()
-	valuesAsString := make([]string, len(values))
-	for i, v := range values {
-		valuesAsString[i] = string(v)
-	}
-	return valuesAsString
-}
-
-func adapterErrorsAsString() []string {
-	values := metrics.AdapterErrors()
+func enumAsString[T ~string](values []T) []string {
 	valuesAsString := make([]string, len(values))
 	for i, v := range values {
 		valuesAsString[i] = string(v)
@@ -31,111 +18,3 @@ func boolValuesAsString() []string {
 		strconv.FormatBool(false),
 	}
 }
-
-func cacheResultsAsString() []string {
-	values := metrics.CacheResults()
-	valuesAsString := make([]string, len(values))
-	for i, v := range values {
-		valuesAsString[i] = string(v)
-	}
-	return valuesAsString
-}
-
-func cookieTypesAsString() []string {
-	values := metrics.CookieTypes()
-	valuesAsString := make([]string, len(values))
-	for i, v := range values {
-		valuesAsString[i] = string(v)
-	}
-	return valuesAsString
-}
-
-func cookieSyncStatusesAsString() []string {
-	values := metrics.CookieSyncStatuses()
-	valuesAsString := make([]string, len(values))
-	for i, v := range values {
-		valuesAsString[i] = string(v)
-	}
-	return valuesAsString
-}
-
-func requestStatusesAsString() []string {
-	values := metrics.RequestStatuses()
-	valuesAsString := make([]string, len(values))
-	for i, v := range values {
-		valuesAsString[i] = string(v)
-	}
-	return valuesAsString
-}
-
-func syncerRequestStatusesAsString() []string {
-	values := metrics.SyncerRequestStatuses()
-	valuesAsString := make([]string, len(values))
-	for i, v := range values {
-		valuesAsString[i] = string(v)
-	}
-	return valuesAsString
-}
-
-func overheadTypesAsString() []string {
-	overheadTypes := metrics.OverheadTypes()
-	overheadTypesAsString := make([]string, len(overheadTypes))
-	for i, ot := range overheadTypes {
-		overheadTypesAsString[i] = ot.String()
-	}
-	return overheadTypesAsString
-}
-
-func syncerSetStatusesAsString() []string {
-	values := metrics.SyncerSetUidStatuses()
-	valuesAsString := make([]string, len(values))
-	for i, v := range values {
-		valuesAsString[i] = string(v)
-	}
-	return valuesAsString
-}
-
-func requestTypesAsString() []string {
-	values := metrics.RequestTypes()
-	valuesAsString := make([]string, len(values))
-	for i, v := range values {
-		valuesAsString[i] = string(v)
-	}
-	return valuesAsString
-}
-
-func setUidStatusesAsString() []string {
-	values := metrics.SetUidStatuses()
-	valuesAsString := make([]string, len(values))
-	for i, v := range values {
-		valuesAsString[i] = string(v)
-	}
-	return valuesAsString
-}
-
-func storedDataFetchTypesAsString() []string {
-	values := metrics.StoredDataFetchTypes()
-	valuesAsString := make([]string, len(values))
-	for i, v := range values {
-		valuesAsString[i] = string(v)
-	}
-	return valuesAsString
-}
-
-func storedDataErrorsAsString() []string {
-	values := metrics.StoredDataErrors()
-	valuesAsString := make([]string, len(values))
-	for i, v := range values {
-		valuesAsString[i] = string(v)
-	}
-	return valuesAsString
-}
-
-func tcfVersionsAsString() []string {
-	values := metrics.TCFVersions()
-	valuesAsString := make([]string, len(values))
-	for i, v := range values {
-		valuesAsString[i] = string(v)
-	}
-	return valuesAsString
-}
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 11f01988..e12316f7 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -140,6 +140,7 @@ var coreBidderNames []BidderName = []BidderName{
 	BidderLogan,
 	BidderLogicad,
 	BidderLunaMedia,
+	BidderMabidder,
 	BidderMadvertise,
 	BidderMarsmedia,
 	BidderMediafuse,
@@ -162,6 +163,7 @@ var coreBidderNames []BidderName = []BidderName{
 	BidderOwnAdx,
 	BidderPangle,
 	BidderPGAM,
+	BidderPGAMSsp,
 	BidderPubmatic,
 	BidderPubnative,
 	BidderPulsepoint,
@@ -433,6 +435,7 @@ const (
 	BidderLogan             BidderName = "logan"
 	BidderLogicad           BidderName = "logicad"
 	BidderLunaMedia         BidderName = "lunamedia"
+	BidderMabidder          BidderName = "mabidder"
 	BidderMadvertise        BidderName = "madvertise"
 	BidderMarsmedia         BidderName = "marsmedia"
 	BidderMediafuse         BidderName = "mediafuse"
@@ -455,6 +458,7 @@ const (
 	BidderOwnAdx            BidderName = "ownadx"
 	BidderPangle            BidderName = "pangle"
 	BidderPGAM              BidderName = "pgam"
+	BidderPGAMSsp           BidderName = "pgamssp"
 	BidderPubmatic          BidderName = "pubmatic"
 	BidderPubnative         BidderName = "pubnative"
 	BidderPulsepoint        BidderName = "pulsepoint"
diff --git a/openrtb_ext/imp_mabidder.go b/openrtb_ext/imp_mabidder.go
new file mode 100644
index 00000000..796a4877
--- /dev/null
+++ b/openrtb_ext/imp_mabidder.go
@@ -0,0 +1,5 @@
+package openrtb_ext
+
+type ImpExtMabidder struct {
+	Ppid string `json:"ppid"`
+}
diff --git a/openrtb_ext/imp_pgamssp.go b/openrtb_ext/imp_pgamssp.go
new file mode 100644
index 00000000..56e4b8bf
--- /dev/null
+++ b/openrtb_ext/imp_pgamssp.go
@@ -0,0 +1,6 @@
+package openrtb_ext
+
+type ImpExtPgamSsp struct {
+	PlacementID string `json:"placementId"`
+	EndpointID  string `json:"endpointId"`
+}
diff --git a/openrtb_ext/request_wrapper.go b/openrtb_ext/request_wrapper.go
index 5f5636d6..f4ef69b0 100644
--- a/openrtb_ext/request_wrapper.go
+++ b/openrtb_ext/request_wrapper.go
@@ -42,6 +42,7 @@ type RequestWrapper struct {
 	appExt              *AppExt
 	regExt              *RegExt
 	siteExt             *SiteExt
+	doohExt             *DOOHExt
 	sourceExt           *SourceExt
 }
 
@@ -160,6 +161,17 @@ func (rw *RequestWrapper) GetSiteExt() (*SiteExt, error) {
 	return rw.siteExt, rw.siteExt.unmarshal(rw.Site.Ext)
 }
 
+func (rw *RequestWrapper) GetDOOHExt() (*DOOHExt, error) {
+	if rw.doohExt != nil {
+		return rw.doohExt, nil
+	}
+	rw.doohExt = &DOOHExt{}
+	if rw.BidRequest == nil || rw.DOOH == nil || rw.DOOH.Ext == nil {
+		return rw.doohExt, rw.doohExt.unmarshal(json.RawMessage{})
+	}
+	return rw.doohExt, rw.doohExt.unmarshal(rw.DOOH.Ext)
+}
+
 func (rw *RequestWrapper) GetSourceExt() (*SourceExt, error) {
 	if rw.sourceExt != nil {
 		return rw.sourceExt, nil
@@ -197,6 +209,9 @@ func (rw *RequestWrapper) RebuildRequest() error {
 	if err := rw.rebuildSiteExt(); err != nil {
 		return err
 	}
+	if err := rw.rebuildDOOHExt(); err != nil {
+		return err
+	}
 	if err := rw.rebuildSourceExt(); err != nil {
 		return err
 	}
@@ -297,6 +312,25 @@ func (rw *RequestWrapper) rebuildAppExt() error {
 	return nil
 }
 
+func (rw *RequestWrapper) rebuildDOOHExt() error {
+	if rw.doohExt == nil || !rw.doohExt.Dirty() {
+		return nil
+	}
+
+	doohJson, err := rw.doohExt.marshal()
+	if err != nil {
+		return err
+	}
+
+	if doohJson != nil && rw.DOOH == nil {
+		rw.DOOH = &openrtb2.DOOH{Ext: doohJson}
+	} else if rw.DOOH != nil {
+		rw.DOOH.Ext = doohJson
+	}
+
+	return nil
+}
+
 func (rw *RequestWrapper) rebuildRegExt() error {
 	if rw.regExt == nil || !rw.regExt.Dirty() {
 		return nil
@@ -370,6 +404,7 @@ func (rw *RequestWrapper) Clone() *RequestWrapper {
 	clone.appExt = rw.appExt.Clone()
 	clone.regExt = rw.regExt.Clone()
 	clone.siteExt = rw.siteExt.Clone()
+	clone.doohExt = rw.doohExt.Clone()
 	clone.sourceExt = rw.sourceExt.Clone()
 
 	return &clone
@@ -1040,6 +1075,70 @@ func (ae *AppExt) Clone() *AppExt {
 	return &clone
 }
 
+// ---------------------------------------------------------------
+// DOOHExt provides an interface for request.dooh.ext
+// This is currently a placeholder for consistency with others - no useful attributes and getters/setters exist yet
+// ---------------------------------------------------------------
+
+type DOOHExt struct {
+	ext      map[string]json.RawMessage
+	extDirty bool
+}
+
+func (de *DOOHExt) unmarshal(extJson json.RawMessage) error {
+	if len(de.ext) != 0 || de.Dirty() {
+		return nil
+	}
+
+	de.ext = make(map[string]json.RawMessage)
+
+	if len(extJson) == 0 {
+		return nil
+	}
+
+	if err := json.Unmarshal(extJson, &de.ext); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (de *DOOHExt) marshal() (json.RawMessage, error) {
+	de.extDirty = false
+	if len(de.ext) == 0 {
+		return nil, nil
+	}
+	return json.Marshal(de.ext)
+}
+
+func (de *DOOHExt) Dirty() bool {
+	return de.extDirty
+}
+
+func (de *DOOHExt) GetExt() map[string]json.RawMessage {
+	ext := make(map[string]json.RawMessage)
+	for k, v := range de.ext {
+		ext[k] = v
+	}
+	return ext
+}
+
+func (de *DOOHExt) SetExt(ext map[string]json.RawMessage) {
+	de.ext = ext
+	de.extDirty = true
+}
+
+func (de *DOOHExt) Clone() *DOOHExt {
+	if de == nil {
+		return nil
+	}
+
+	clone := *de
+	clone.ext = maputil.Clone(de.ext)
+
+	return &clone
+}
+
 // ---------------------------------------------------------------
 // RegExt provides an interface for request.regs.ext
 // ---------------------------------------------------------------
diff --git a/pbs/usersync.go b/pbs/usersync.go
index 7b468cb0..a5b49f6d 100644
--- a/pbs/usersync.go
+++ b/pbs/usersync.go
@@ -22,6 +22,7 @@ type UserSyncDeps struct {
 	ExternalUrl      string
 	RecaptchaSecret  string
 	HostCookieConfig *config.HostCookie
+	PriorityGroups   [][]string
 }
 
 // Struct for parsing json in google's response
@@ -81,7 +82,7 @@ func (deps *UserSyncDeps) OptOut(w http.ResponseWriter, r *http.Request, _ httpr
 	pc.SetOptOut(optout != "")
 
 	// Write Cookie
-	encodedCookie, err := pc.PrepareCookieForWrite(deps.HostCookieConfig, encoder)
+	encodedCookie, err := encoder.Encode(pc)
 	if err != nil {
 		w.WriteHeader(http.StatusBadRequest)
 		return
diff --git a/router/router.go b/router/router.go
index f7270445..f2b1f7e7 100644
--- a/router/router.go
+++ b/router/router.go
@@ -274,6 +274,7 @@ func New(cfg *config.Configuration, rateConvertor *currency.RateConverter) (r *R
 		HostCookieConfig: &(cfg.HostCookie),
 		ExternalUrl:      cfg.ExternalURL,
 		RecaptchaSecret:  cfg.RecaptchaSecret,
+		PriorityGroups:   cfg.UserSync.PriorityGroups,
 	}
 
 	r.GET("/setuid", endpoints.NewSetUIDEndpoint(cfg, syncersByBidder, gdprPermsBuilder, tcf2CfgBuilder, pbsAnalytics, accounts, r.MetricsEngine))
diff --git a/static/bidder-info/mabidder.yaml b/static/bidder-info/mabidder.yaml
new file mode 100644
index 00000000..3f03fd87
--- /dev/null
+++ b/static/bidder-info/mabidder.yaml
@@ -0,0 +1,11 @@
+endpoint: "https://prebid.ecdrsvc.com/pbs"
+maintainer:
+  email: lmprebidadapter@loblaw.ca
+endpointCompression: gzip
+capabilities:
+  app:
+    mediaTypes:
+      - banner
+  site:
+    mediaTypes:
+      - banner
\ No newline at end of file
diff --git a/static/bidder-info/pgamssp.yaml b/static/bidder-info/pgamssp.yaml
new file mode 100644
index 00000000..a762d8f1
--- /dev/null
+++ b/static/bidder-info/pgamssp.yaml
@@ -0,0 +1,19 @@
+endpoint: "http://us-east.pgammedia.com/pserver"
+maintainer:
+  email: "info@pgammedia.com"
+capabilities:
+  site:
+    mediaTypes:
+    - banner
+    - video
+    - native
+    
+  app:
+    mediaTypes:
+    - banner
+    - video
+    - native
+userSync:
+  redirect:
+    url: "https://cs.pgammedia.com/pserver?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir={{.RedirectURL}}"
+    userMacro: "[UID]"
diff --git a/static/bidder-params/mabidder.json b/static/bidder-params/mabidder.json
new file mode 100644
index 00000000..f005e88e
--- /dev/null
+++ b/static/bidder-params/mabidder.json
@@ -0,0 +1,15 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "Mabidder Adapter Params",
+  "description": "A schema which validates params accepted by the Mabidder adapter",
+  "type": "object",
+
+  "properties": {
+    "ppid": {
+      "type": "string",
+      "description": "Publisher Placement ID"
+    }
+  },
+
+  "required": ["ppid"]
+}
\ No newline at end of file
diff --git a/static/bidder-params/pgamssp.json b/static/bidder-params/pgamssp.json
new file mode 100644
index 00000000..9d060241
--- /dev/null
+++ b/static/bidder-params/pgamssp.json
@@ -0,0 +1,23 @@
+{
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "title": "PgamSSP Adapter Params",
+    "description": "A schema which validates params accepted by the PgamSSP adapter",
+    "type": "object",
+  
+    "properties": {
+      "placementId": {
+        "type": "string",
+        "minLength": 1,
+        "description": "Placement ID"
+      },
+      "endpointId": {
+        "type": "string",
+        "minLength": 1,
+        "description": "Endpoint ID"
+      }
+    },
+    "oneOf": [
+      { "required": ["placementId"] },
+      { "required": ["endpointId"] }
+    ]
+  }
\ No newline at end of file
diff --git a/usersync/cookie.go b/usersync/cookie.go
index c0eb898c..94ada94e 100644
--- a/usersync/cookie.go
+++ b/usersync/cookie.go
@@ -4,7 +4,6 @@ import (
 	"encoding/json"
 	"errors"
 	"net/http"
-	"sort"
 	"time"
 
 	"github.com/prebid/prebid-server/config"
@@ -58,10 +57,7 @@ func ReadCookie(r *http.Request, decoder Decoder, host *config.HostCookie) *Cook
 }
 
 // PrepareCookieForWrite ejects UIDs as long as the cookie is too full
-func (cookie *Cookie) PrepareCookieForWrite(cfg *config.HostCookie, encoder Encoder) (string, error) {
-	uuidKeys := sortUIDs(cookie.uids)
-
-	i := 0
+func (cookie *Cookie) PrepareCookieForWrite(cfg *config.HostCookie, encoder Encoder, ejector Ejector) (string, error) {
 	for len(cookie.uids) > 0 {
 		encodedCookie, err := encoder.Encode(cookie)
 		if err != nil {
@@ -80,12 +76,15 @@ func (cookie *Cookie) PrepareCookieForWrite(cfg *config.HostCookie, encoder Enco
 		isCookieTooBig := cookieSize > cfg.MaxCookieSizeBytes && cfg.MaxCookieSizeBytes > 0
 		if !isCookieTooBig {
 			return encodedCookie, nil
+		} else if len(cookie.uids) == 1 {
+			return "", errors.New("uid that's trying to be synced is bigger than MaxCookieSize")
 		}
 
-		uidToDelete := uuidKeys[i]
+		uidToDelete, err := ejector.Choose(cookie.uids)
+		if err != nil {
+			return encodedCookie, err
+		}
 		delete(cookie.uids, uidToDelete)
-
-		i++
 	}
 	return "", nil
 }
@@ -132,23 +131,6 @@ func (cookie *Cookie) Sync(key string, uid string) error {
 	return nil
 }
 
-// sortUIDs is used to get a list of uids sorted from oldest to newest
-// This list is used to eject oldest uids from the cookie
-// This will be incorporated with a more complex ejection framework in a future PR
-func sortUIDs(uids map[string]UIDEntry) []string {
-	if len(uids) > 0 {
-		uuidKeys := make([]string, 0, len(uids))
-		for key := range uids {
-			uuidKeys = append(uuidKeys, key)
-		}
-		sort.SliceStable(uuidKeys, func(i, j int) bool {
-			return uids[uuidKeys[i]].Expires.Before(uids[uuidKeys[j]].Expires)
-		})
-		return uuidKeys
-	}
-	return nil
-}
-
 // SyncHostCookie syncs the request cookie with the host cookie
 func SyncHostCookie(r *http.Request, requestCookie *Cookie, host *config.HostCookie) {
 	if uid, _, _ := requestCookie.GetUID(host.Family); uid == "" && host.CookieName != "" {
diff --git a/usersync/ejector.go b/usersync/ejector.go
new file mode 100644
index 00000000..84299f72
--- /dev/null
+++ b/usersync/ejector.go
@@ -0,0 +1,127 @@
+package usersync
+
+import (
+	"errors"
+	"time"
+)
+
+type Ejector interface {
+	Choose(uids map[string]UIDEntry) (string, error)
+}
+
+type OldestEjector struct{}
+
+type PriorityBidderEjector struct {
+	PriorityGroups   [][]string
+	SyncersByBidder  map[string]Syncer
+	IsSyncerPriority bool
+	TieEjector       Ejector
+}
+
+// Choose method for oldest ejector will return the oldest uid
+func (o *OldestEjector) Choose(uids map[string]UIDEntry) (string, error) {
+	var oldestElement string
+	var oldestDate time.Time = time.Unix(1<<63-62135596801, 999999999) // Max value for time
+
+	for key, value := range uids {
+		if value.Expires.Before(oldestDate) {
+			oldestElement = key
+			oldestDate = value.Expires
+		}
+	}
+	return oldestElement, nil
+}
+
+// Choose method for priority ejector will return the oldest lowest priority element
+func (p *PriorityBidderEjector) Choose(uids map[string]UIDEntry) (string, error) {
+	nonPriorityUids := getNonPriorityUids(uids, p.PriorityGroups, p.SyncersByBidder)
+	if err := p.checkSyncerPriority(nonPriorityUids); err != nil {
+		return "", err
+	}
+
+	if len(nonPriorityUids) > 0 {
+		return p.TieEjector.Choose(nonPriorityUids)
+	}
+
+	lowestPriorityGroup := p.PriorityGroups[len(p.PriorityGroups)-1]
+	if len(lowestPriorityGroup) == 1 {
+		uidToDelete := lowestPriorityGroup[0]
+		p.PriorityGroups = removeElementFromPriorityGroup(p.PriorityGroups, uidToDelete)
+		return uidToDelete, nil
+	}
+
+	lowestPriorityUids := getPriorityUids(lowestPriorityGroup, uids, p.SyncersByBidder)
+	uidToDelete, err := p.TieEjector.Choose(lowestPriorityUids)
+	if err != nil {
+		return "", err
+	}
+	p.PriorityGroups = removeElementFromPriorityGroup(p.PriorityGroups, uidToDelete)
+	return uidToDelete, nil
+}
+
+// updatePriorityGroup will remove the selected element from the priority groups, and will remove the entire priority group if it's empty
+func removeElementFromPriorityGroup(priorityGroups [][]string, oldestElement string) [][]string {
+	lowestPriorityGroup := priorityGroups[len(priorityGroups)-1]
+	if len(lowestPriorityGroup) <= 1 {
+		return priorityGroups[:len(priorityGroups)-1]
+	}
+
+	for index, elem := range lowestPriorityGroup {
+		if elem == oldestElement {
+			updatedPriorityGroup := append(lowestPriorityGroup[:index], lowestPriorityGroup[index+1:]...)
+			priorityGroups[len(priorityGroups)-1] = updatedPriorityGroup
+			return priorityGroups
+		}
+	}
+	return priorityGroups
+}
+
+func getNonPriorityUids(uids map[string]UIDEntry, priorityGroups [][]string, syncersByBidder map[string]Syncer) map[string]UIDEntry {
+	// If no priority groups, then all keys in uids are non-priority
+	if len(priorityGroups) == 0 {
+		return uids
+	}
+
+	// Create map of keys that are a priority
+	isPriority := make(map[string]bool)
+	for _, group := range priorityGroups {
+		for _, bidder := range group {
+			if bidderSyncer, ok := syncersByBidder[bidder]; ok {
+				isPriority[bidderSyncer.Key()] = true
+			}
+		}
+	}
+
+	// Create a map for non-priority uids
+	nonPriorityUIDs := make(map[string]UIDEntry)
+
+	// Loop over uids and populate the nonPriorityUIDs map with non-priority keys
+	for key, value := range uids {
+		if _, found := isPriority[key]; !found {
+			nonPriorityUIDs[key] = value
+		}
+	}
+
+	return nonPriorityUIDs
+}
+
+func getPriorityUids(lowestPriorityGroup []string, uids map[string]UIDEntry, syncersByBidder map[string]Syncer) map[string]UIDEntry {
+	lowestPriorityUIDs := make(map[string]UIDEntry)
+
+	// Loop over lowestPriorityGroup and populate the lowestPriorityUIDs map
+	for _, bidder := range lowestPriorityGroup {
+		if bidderSyncer, ok := syncersByBidder[bidder]; ok {
+			if uidEntry, exists := uids[bidderSyncer.Key()]; exists {
+				lowestPriorityUIDs[bidderSyncer.Key()] = uidEntry
+			}
+		}
+	}
+	return lowestPriorityUIDs
+}
+
+func (p *PriorityBidderEjector) checkSyncerPriority(nonPriorityUids map[string]UIDEntry) error {
+	if len(nonPriorityUids) == 1 && !p.IsSyncerPriority && len(p.PriorityGroups) > 0 {
+		return errors.New("syncer key is not a priority, and there are only priority elements left")
+	}
+	return nil
+}


Test patch:
diff --git a/adapters/infoawarebidder_test.go b/adapters/infoawarebidder_test.go
index f42716bc..87d9d4d4 100644
--- a/adapters/infoawarebidder_test.go
+++ b/adapters/infoawarebidder_test.go
@@ -10,6 +10,7 @@ import (
 	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestAppNotSupported(t *testing.T) {
@@ -56,6 +57,26 @@ func TestSiteNotSupported(t *testing.T) {
 	assert.Len(t, bids, 0)
 }
 
+func TestDOOHNotSupported(t *testing.T) {
+	bidder := &mockBidder{}
+	info := config.BidderInfo{
+		Capabilities: &config.CapabilitiesInfo{
+			Site: &config.PlatformInfo{
+				MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner},
+			},
+		},
+	}
+	constrained := adapters.BuildInfoAwareBidder(bidder, info)
+	bids, errs := constrained.MakeRequests(&openrtb2.BidRequest{
+		Imp:  []openrtb2.Imp{{ID: "imp-1", Banner: &openrtb2.Banner{}}},
+		DOOH: &openrtb2.DOOH{},
+	}, &adapters.ExtraRequestInfo{})
+	require.Len(t, errs, 1)
+	assert.EqualError(t, errs[0], "this bidder does not support dooh requests")
+	assert.IsType(t, &errortypes.Warning{}, errs[0])
+	assert.Len(t, bids, 0)
+}
+
 func TestImpFiltering(t *testing.T) {
 	bidder := &mockBidder{}
 	info := config.BidderInfo{
@@ -66,6 +87,9 @@ func TestImpFiltering(t *testing.T) {
 			App: &config.PlatformInfo{
 				MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner},
 			},
+			DOOH: &config.PlatformInfo{
+				MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeNative},
+			},
 		},
 	}
 
@@ -153,10 +177,10 @@ func TestImpFiltering(t *testing.T) {
 			description: "All imps with correct media type, MakeRequest() call expected",
 			inBidRequest: &openrtb2.BidRequest{
 				Imp: []openrtb2.Imp{
-					{ID: "imp-1", Video: &openrtb2.Video{}},
-					{ID: "imp-2", Video: &openrtb2.Video{}},
+					{ID: "imp-1", Native: &openrtb2.Native{}},
+					{ID: "imp-2", Native: &openrtb2.Native{}},
 				},
-				Site: &openrtb2.Site{},
+				DOOH: &openrtb2.DOOH{},
 			},
 			expectedErrors: nil,
 			expectedImpLen: 2,
diff --git a/adapters/mabidder/mabidder_test.go b/adapters/mabidder/mabidder_test.go
new file mode 100644
index 00000000..89cfd316
--- /dev/null
+++ b/adapters/mabidder/mabidder_test.go
@@ -0,0 +1,21 @@
+package mabidder
+
+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.BidderMabidder, config.Adapter{
+		Endpoint: "https://prebid.ecdrsvc.com/pbs"},
+		config.Server{ExternalUrl: "https://prebid.ecdrsvc.com/pbs", GvlID: 1, DataCenter: "2"})
+
+	if buildErr != nil {
+		t.Fatalf("Builder returned unexpected error %v", buildErr)
+	}
+
+	adapterstest.RunJSONBidderTest(t, "mabiddertest", bidder)
+}
diff --git a/adapters/mabidder/mabiddertest/exemplary/simple-app-banner.json b/adapters/mabidder/mabiddertest/exemplary/simple-app-banner.json
new file mode 100644
index 00000000..4d3cbdfd
--- /dev/null
+++ b/adapters/mabidder/mabiddertest/exemplary/simple-app-banner.json
@@ -0,0 +1,110 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "app": {
+      "bundle": "com.prebid"
+    },
+    "device": {
+      "ifa":"87857b31-8942-4646-ae80-ab9c95bf3fab"
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "ppid": "ppidtest"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://prebid.ecdrsvc.com/pbs",
+        "body": {
+          "id": "test-request-id",
+          "app": {
+            "bundle": "com.prebid"
+          },
+          "device": {
+            "ifa":"87857b31-8942-4646-ae80-ab9c95bf3fab"
+          },
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "ppid": "ppidtest"
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "Responses": [
+            {
+              "requestId": "1",
+              "currency": "CAD",
+              "width": 300,
+              "height": 250,
+              "creativeId": "6002677",
+              "dealId": "testdeal",
+              "netRevenue": false,
+              "ttl": 5,
+              "ad": "<script type='text/javascript' src='https://adsvr.ecdrsvc.com/js?6002677'></script>",
+              "meta": {
+                "advertiserDomains": [
+                  "https://www.loblaws.ca/"
+                ]
+              },
+              "cpm": 3.5764000415802
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "id": "test-request-id",
+      "bids": [
+        {
+          "bid": {
+            "id": "1",
+            "impid": "1",
+            "price":3.5764000415802, 
+            "adm": "<script type='text/javascript' src='https://adsvr.ecdrsvc.com/js?6002677'></script>",
+            "adomain": [
+              "https://www.loblaws.ca/"
+            ],
+            "crid": "6002677",
+            "dealid": "testdeal",
+            "w": 300,
+            "h": 250
+          }
+        }
+      ],
+      "cur": "USD"
+    }
+  ]
+}
diff --git a/adapters/mabidder/mabiddertest/supplemental/bad-request-example.json b/adapters/mabidder/mabiddertest/supplemental/bad-request-example.json
new file mode 100644
index 00000000..8ab37e13
--- /dev/null
+++ b/adapters/mabidder/mabiddertest/supplemental/bad-request-example.json
@@ -0,0 +1,74 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "app": {
+      "bundle": "com.prebid"
+    },
+    "device": {
+      "ifa":"87857b31-8942-4646-ae80-ab9c95bf3fab"
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "ppid": "mabidder-test"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://prebid.ecdrsvc.com/pbs",
+        "body": {
+          "id": "test-request-id",
+          "app": {
+            "bundle": "com.prebid"
+          },
+          "device": {
+            "ifa":"87857b31-8942-4646-ae80-ab9c95bf3fab"
+          },
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "ppid": "mabidder-test"
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 400,
+        "body": {
+        }
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Unexpected status code: 400. Run with request.debug = 1 for more info",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/mabidder/mabiddertest/supplemental/bad-response-malformed.json b/adapters/mabidder/mabiddertest/supplemental/bad-response-malformed.json
new file mode 100644
index 00000000..f62ed2a7
--- /dev/null
+++ b/adapters/mabidder/mabiddertest/supplemental/bad-response-malformed.json
@@ -0,0 +1,72 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "app": {
+      "bundle": "com.prebid"
+    },
+    "device": {
+      "ifa":"87857b31-8942-4646-ae80-ab9c95bf3fab"
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "ppid": "mabidder-test"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://prebid.ecdrsvc.com/pbs",
+        "body": {
+          "id": "test-request-id",
+          "app": {
+            "bundle": "com.prebid"
+          },
+          "device": {
+            "ifa":"87857b31-8942-4646-ae80-ab9c95bf3fab"
+          },
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "ppid": "mabidder-test"
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "unexpected end of JSON input",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/mabidder/mabiddertest/supplemental/bad-response-status-500.json b/adapters/mabidder/mabiddertest/supplemental/bad-response-status-500.json
new file mode 100644
index 00000000..8ee59384
--- /dev/null
+++ b/adapters/mabidder/mabiddertest/supplemental/bad-response-status-500.json
@@ -0,0 +1,74 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "app": {
+      "bundle": "com.prebid"
+    },
+    "device": {
+      "ifa":"87857b31-8942-4646-ae80-ab9c95bf3fab"
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "ppid": "mabidder-test"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://prebid.ecdrsvc.com/pbs",
+        "body": {
+          "id": "test-request-id",
+          "app": {
+            "bundle": "com.prebid"
+          },
+          "device": {
+            "ifa":"87857b31-8942-4646-ae80-ab9c95bf3fab"
+          },
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "ppid": "mabidder-test"
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 500,
+        "body": {
+        }
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Unexpected status code: 500. Run with request.debug = 1 for more info",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/mabidder/mabiddertest/supplemental/no-content-response.json b/adapters/mabidder/mabiddertest/supplemental/no-content-response.json
new file mode 100644
index 00000000..640e78dc
--- /dev/null
+++ b/adapters/mabidder/mabiddertest/supplemental/no-content-response.json
@@ -0,0 +1,69 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "app": {
+            "bundle": "com.prebid"
+        },
+        "device": {
+            "ifa":"87857b31-8942-4646-ae80-ab9c95bf3fab"
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "ppid": "mabidder-test"
+                    }
+                }
+            }
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "https://prebid.ecdrsvc.com/pbs",
+                "body": {
+                    "id": "test-request-id",
+                    "app": {
+                        "bundle": "com.prebid"
+                    },
+                    "device": {
+                        "ifa":"87857b31-8942-4646-ae80-ab9c95bf3fab"
+                    },
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 300,
+                                        "h": 250
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "ppid": "mabidder-test"
+                                }
+                            }
+                        }
+                    ]
+                }
+            },
+            "mockResponse": {
+                "status": 204,
+                "body": {
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": []
+}
diff --git a/adapters/mabidder/params_test.go b/adapters/mabidder/params_test.go
new file mode 100644
index 00000000..b0f3762d
--- /dev/null
+++ b/adapters/mabidder/params_test.go
@@ -0,0 +1,50 @@
+package mabidder
+
+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.BidderMabidder, 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.BidderMabidder, json.RawMessage(p)); err == nil {
+			t.Errorf("Schema allowed invalid params: %s", p)
+		}
+	}
+}
+
+var validParams = []string{
+	`{"ppid": ""}`,
+	`{"ppid": "testppid"}`,
+}
+
+var invalidParams = []string{
+	``,
+	`null`,
+	`true`,
+	`5`,
+	`4.2`,
+	`[]`,
+	`{}`,
+	`{"ppid": 42}`,
+}
diff --git a/adapters/pgamssp/params_test.go b/adapters/pgamssp/params_test.go
new file mode 100644
index 00000000..3e603237
--- /dev/null
+++ b/adapters/pgamssp/params_test.go
@@ -0,0 +1,47 @@
+package pgamssp
+
+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.BidderPGAMSsp, 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.BidderPGAMSsp, json.RawMessage(p)); err == nil {
+			t.Errorf("Schema allowed invalid params: %s", p)
+		}
+	}
+}
+
+var validParams = []string{
+	`{"placementId": "test"}`,
+	`{"placementId": "1"}`,
+	`{"endpointId": "test"}`,
+	`{"endpointId": "1"}`,
+}
+
+var invalidParams = []string{
+	`{"placementId": 42}`,
+	`{"endpointId": 42}`,
+	`{"placementId": "1", "endpointId": "1"}`,
+}
diff --git a/adapters/pgamssp/pgamssp_test.go b/adapters/pgamssp/pgamssp_test.go
new file mode 100644
index 00000000..596b813c
--- /dev/null
+++ b/adapters/pgamssp/pgamssp_test.go
@@ -0,0 +1,20 @@
+package pgamssp
+
+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.BidderPGAMSsp, config.Adapter{
+		Endpoint: "http://test.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, "pgamssptest", bidder)
+}
diff --git a/adapters/pgamssp/pgamssptest/exemplary/endpointId.json b/adapters/pgamssp/pgamssptest/exemplary/endpointId.json
new file mode 100644
index 00000000..72805db1
--- /dev/null
+++ b/adapters/pgamssp/pgamssptest/exemplary/endpointId.json
@@ -0,0 +1,125 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "device": {
+            "ip": "123.123.123.123",
+            "ua": "iPad"
+        },
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "endpointId": "test"
+                    }
+                }
+            }
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://test.com/pserver",
+                "body": {
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "tagid": "test",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 300,
+                                        "h": 250
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "endpointId": "test",
+                                    "type": "network"
+                                }
+                            }
+                        }
+                    ],
+                    "app": {
+                        "id": "1",
+                        "bundle": "com.wls.testwlsapplication"
+                    },
+                    "device": {
+                        "ip": "123.123.123.123",
+                        "ua": "iPad"
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "bid": [
+                                {
+                                    "id": "test_bid_id",
+                                    "impid": "test-imp-id",
+                                    "price": 0.27543,
+                                    "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://test.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                                    "cid": "test_cid",
+                                    "crid": "test_crid",
+                                    "dealid": "test_dealid",
+                                    "w": 300,
+                                    "h": 250,
+                                    "mtype": 1
+                                }
+                            ],
+                            "seat": "pgamssp"
+                        }
+                    ],
+                    "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://test.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                        "cid": "test_cid",
+                        "crid": "test_crid",
+                        "dealid": "test_dealid",
+                        "w": 300,
+                        "h": 250,
+                        "mtype": 1
+                    },
+                    "type": "banner"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/pgamssp/pgamssptest/exemplary/simple-banner.json b/adapters/pgamssp/pgamssptest/exemplary/simple-banner.json
new file mode 100644
index 00000000..3850c449
--- /dev/null
+++ b/adapters/pgamssp/pgamssptest/exemplary/simple-banner.json
@@ -0,0 +1,125 @@
+{
+    "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://test.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://test.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                                    "cid": "test_cid",
+                                    "crid": "test_crid",
+                                    "dealid": "test_dealid",
+                                    "w": 300,
+                                    "h": 250,
+                                    "mtype": 1
+                                }
+                            ],
+                            "seat": "pgamssp"
+                        }
+                    ],
+                    "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://test.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                        "cid": "test_cid",
+                        "crid": "test_crid",
+                        "dealid": "test_dealid",
+                        "w": 300,
+                        "h": 250,
+                        "mtype": 1
+                    },
+                    "type": "banner"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/pgamssp/pgamssptest/exemplary/simple-native.json b/adapters/pgamssp/pgamssptest/exemplary/simple-native.json
new file mode 100644
index 00000000..8796adac
--- /dev/null
+++ b/adapters/pgamssp/pgamssptest/exemplary/simple-native.json
@@ -0,0 +1,109 @@
+{
+    "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://test.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://test.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                                    "cid": "test_cid",
+                                    "crid": "test_crid",
+                                    "dealid": "test_dealid",
+                                    "w": 300,
+                                    "h": 250,
+                                    "mtype": 4
+                                }
+                            ],
+                            "seat": "pgamssp"
+                        }
+                    ],
+                    "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://test.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                        "cid": "test_cid",
+                        "crid": "test_crid",
+                        "dealid": "test_dealid",
+                        "w": 300,
+                        "h": 250,
+                        "mtype": 4
+                    },
+                    "type": "native"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/pgamssp/pgamssptest/exemplary/simple-video.json b/adapters/pgamssp/pgamssptest/exemplary/simple-video.json
new file mode 100644
index 00000000..a39d0111
--- /dev/null
+++ b/adapters/pgamssp/pgamssptest/exemplary/simple-video.json
@@ -0,0 +1,120 @@
+{
+    "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://test.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",
+                                    "mtype": 2
+                                }
+                            ],
+                            "seat": "pgamssp"
+                        }
+                    ],
+                    "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",
+                        "mtype": 2
+                    },
+                    "type": "video"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/pgamssp/pgamssptest/exemplary/simple-web-banner.json b/adapters/pgamssp/pgamssptest/exemplary/simple-web-banner.json
new file mode 100644
index 00000000..fca8ce17
--- /dev/null
+++ b/adapters/pgamssp/pgamssptest/exemplary/simple-web-banner.json
@@ -0,0 +1,125 @@
+{
+    "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://test.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://test.com/pserver&k=bc2d316f39931a07d9a8dd249bf85fc0\"></iframe>",
+                                    "cid": "test_cid",
+                                    "crid": "test_crid",
+                                    "dealid": "test_dealid",
+                                    "w": 468,
+                                    "h": 60,
+                                    "mtype": 1
+                                }
+                            ],
+                            "seat": "pgamssp"
+                        }
+                    ],
+                    "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://test.com/pserver&k=bc2d316f39931a07d9a8dd249bf85fc0\"></iframe>",
+                        "cid": "test_cid",
+                        "crid": "test_crid",
+                        "dealid": "test_dealid",
+                        "w": 468,
+                        "h": 60,
+                        "mtype": 1
+                    },
+                    "type": "banner"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/pgamssp/pgamssptest/supplemental/bad_media_type.json b/adapters/pgamssp/pgamssptest/supplemental/bad_media_type.json
new file mode 100644
index 00000000..6f016367
--- /dev/null
+++ b/adapters/pgamssp/pgamssptest/supplemental/bad_media_type.json
@@ -0,0 +1,82 @@
+{
+	"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://test.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://test.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                                "cid": "test_cid",
+                                "crid": "test_crid",
+                                "dealid": "test_dealid",
+                                "w": 300,
+                                "h": 250,
+                                "ext": {}
+                            }
+                        ],
+                        "seat": "pgamssp"
+                    }
+                ],
+                "cur": "USD"
+            }
+        }
+    }],
+    "expectedMakeBidsErrors": [
+        {
+          "value": "Unable to fetch mediaType in multi-format: test-imp-id",
+          "comparison": "literal"
+        }
+    ]
+}
diff --git a/adapters/pgamssp/pgamssptest/supplemental/bad_response.json b/adapters/pgamssp/pgamssptest/supplemental/bad_response.json
new file mode 100644
index 00000000..8c9f6f52
--- /dev/null
+++ b/adapters/pgamssp/pgamssptest/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://test.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/pgamssp/pgamssptest/supplemental/status-204.json b/adapters/pgamssp/pgamssptest/supplemental/status-204.json
new file mode 100644
index 00000000..3869591e
--- /dev/null
+++ b/adapters/pgamssp/pgamssptest/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://test.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/pgamssp/pgamssptest/supplemental/status-not-200.json b/adapters/pgamssp/pgamssptest/supplemental/status-not-200.json
new file mode 100644
index 00000000..0c9b3eec
--- /dev/null
+++ b/adapters/pgamssp/pgamssptest/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://test.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/config/account_test.go b/config/account_test.go
index 31d9796a..58f2a52e 100644
--- a/config/account_test.go
+++ b/config/account_test.go
@@ -205,6 +205,7 @@ func TestAccountChannelGetByChannelType(t *testing.T) {
 		giveAppEnabled   *bool
 		giveVideoEnabled *bool
 		giveWebEnabled   *bool
+		giveDOOHEnabled  *bool
 		giveChannelType  ChannelType
 		wantEnabled      *bool
 	}{
@@ -276,6 +277,23 @@ func TestAccountChannelGetByChannelType(t *testing.T) {
 			giveChannelType: ChannelWeb,
 			wantEnabled:     &trueValue,
 		},
+		{
+			description:     "DOOH channel setting unspecified, returns nil",
+			giveChannelType: ChannelDOOH,
+			wantEnabled:     nil,
+		},
+		{
+			description:     "DOOH channel disabled, returns false",
+			giveDOOHEnabled: &falseValue,
+			giveChannelType: ChannelDOOH,
+			wantEnabled:     &falseValue,
+		},
+		{
+			description:     "DOOH channel enabled, returns true",
+			giveDOOHEnabled: &trueValue,
+			giveChannelType: ChannelDOOH,
+			wantEnabled:     &trueValue,
+		},
 	}
 
 	for _, tt := range tests {
@@ -284,6 +302,7 @@ func TestAccountChannelGetByChannelType(t *testing.T) {
 			App:   tt.giveAppEnabled,
 			Video: tt.giveVideoEnabled,
 			Web:   tt.giveWebEnabled,
+			DOOH:  tt.giveDOOHEnabled,
 		}
 
 		result := accountChannel.GetByChannelType(tt.giveChannelType)
@@ -836,7 +855,7 @@ func TestAccountChannelIsSet(t *testing.T) {
 	}{
 		{
 			name:                "AccountChannelSetAllFields",
-			givenAccountChannel: &AccountChannel{AMP: &trueBool, App: &falseBool, Video: &falseBool, Web: &falseBool},
+			givenAccountChannel: &AccountChannel{AMP: &trueBool, App: &falseBool, Video: &falseBool, Web: &falseBool, DOOH: &falseBool},
 			expected:            true,
 		},
 		{
diff --git a/config/bidderinfo_test.go b/config/bidderinfo_test.go
index 77625c0c..438ed5af 100644
--- a/config/bidderinfo_test.go
+++ b/config/bidderinfo_test.go
@@ -31,6 +31,9 @@ capabilities:
       - banner
       - video
       - native
+  dooh:
+    mediaTypes:
+      - banner
 modifyingVastXmlAllowed: true
 debug:
   allow: true
@@ -92,6 +95,9 @@ func TestLoadBidderInfoFromDisk(t *testing.T) {
 				Site: &PlatformInfo{
 					MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner, openrtb_ext.BidTypeVideo, openrtb_ext.BidTypeNative},
 				},
+				DOOH: &PlatformInfo{
+					MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner, openrtb_ext.BidTypeVideo},
+				},
 			},
 			Syncer: &Syncer{
 				Key: "foo",
@@ -176,6 +182,9 @@ func TestProcessBidderInfo(t *testing.T) {
 						Site: &PlatformInfo{
 							MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner, openrtb_ext.BidTypeVideo, openrtb_ext.BidTypeNative},
 						},
+						DOOH: &PlatformInfo{
+							MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner},
+						},
 					},
 					Debug: &DebugInfo{
 						Allow: true,
@@ -225,6 +234,9 @@ func TestProcessBidderInfo(t *testing.T) {
 						Site: &PlatformInfo{
 							MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner, openrtb_ext.BidTypeVideo, openrtb_ext.BidTypeNative},
 						},
+						DOOH: &PlatformInfo{
+							MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner},
+						},
 					},
 					Debug: &DebugInfo{
 						Allow: true,
@@ -533,7 +545,7 @@ func TestBidderInfoValidationPositive(t *testing.T) {
 			Endpoint:   "http://bidderB.com/openrtb2",
 			PlatformID: "B",
 			Maintainer: &MaintainerInfo{
-				Email: "maintainer@bidderA.com",
+				Email: "maintainer@bidderB.com",
 			},
 			GVLVendorID: 2,
 			Capabilities: &CapabilitiesInfo{
@@ -569,6 +581,23 @@ func TestBidderInfoValidationPositive(t *testing.T) {
 			},
 			AliasOf: "bidderB",
 		},
+		"bidderD": BidderInfo{
+			Endpoint:   "http://bidderD.com/openrtb2",
+			PlatformID: "D",
+			Maintainer: &MaintainerInfo{
+				Email: "maintainer@bidderD.com",
+			},
+			GVLVendorID: 3,
+			Capabilities: &CapabilitiesInfo{
+				DOOH: &PlatformInfo{
+					MediaTypes: []openrtb_ext.BidType{
+						openrtb_ext.BidTypeVideo,
+						openrtb_ext.BidTypeNative,
+						openrtb_ext.BidTypeBanner,
+					},
+				},
+			},
+		},
 	}
 	errs := bidderInfos.validate(make([]error, 0))
 	assert.Len(t, errs, 0, "All bidder infos should be correct")
@@ -769,7 +798,7 @@ func TestBidderInfoValidationNegative(t *testing.T) {
 			},
 		},
 		{
-			"One bidder missing capabilities site and app",
+			"One bidder missing capabilities site and app and dooh",
 			BidderInfos{
 				"bidderA": BidderInfo{
 					Endpoint: "http://bidderA.com/openrtb2",
@@ -780,7 +809,7 @@ func TestBidderInfoValidationNegative(t *testing.T) {
 				},
 			},
 			[]error{
-				errors.New("at least one of capabilities.site or capabilities.app must exist for adapter: bidderA"),
+				errors.New("at least one of capabilities.site, capabilities.app, or capabilities.dooh must exist for adapter: bidderA"),
 			},
 		},
 		{
@@ -804,6 +833,27 @@ func TestBidderInfoValidationNegative(t *testing.T) {
 				errors.New("capabilities.app failed validation: unrecognized media type at index 0: incorrect for adapter: bidderA"),
 			},
 		},
+		{
+			"One bidder incorrect capabilities for dooh",
+			BidderInfos{
+				"bidderA": BidderInfo{
+					Endpoint: "http://bidderA.com/openrtb2",
+					Maintainer: &MaintainerInfo{
+						Email: "maintainer@bidderA.com",
+					},
+					Capabilities: &CapabilitiesInfo{
+						DOOH: &PlatformInfo{
+							MediaTypes: []openrtb_ext.BidType{
+								"incorrect",
+							},
+						},
+					},
+				},
+			},
+			[]error{
+				errors.New("capabilities.dooh failed validation: unrecognized media type at index 0: incorrect for adapter: bidderA"),
+			},
+		},
 		{
 			"One bidder nil capabilities",
 			BidderInfos{
@@ -1008,7 +1058,7 @@ func TestBidderInfoValidationNegative(t *testing.T) {
 				},
 			},
 			[]error{
-				errors.New("at least one of capabilities.site or capabilities.app must exist for adapter: bidderA"),
+				errors.New("at least one of capabilities.site, capabilities.app, or capabilities.dooh must exist for adapter: bidderA"),
 				errors.New("capabilities for alias: bidderB should be a subset of capabilities for parent bidder: bidderA"),
 			},
 		},
@@ -1687,6 +1737,9 @@ func TestReadFullYamlBidderConfig(t *testing.T) {
 				Site: &PlatformInfo{
 					MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner, openrtb_ext.BidTypeVideo, openrtb_ext.BidTypeNative},
 				},
+				DOOH: &PlatformInfo{
+					MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner},
+				},
 			},
 			ModifyingVastXmlAllowed: true,
 			Debug: &DebugInfo{
diff --git a/config/test/bidder-info-valid/stroeerCore.yaml b/config/test/bidder-info-valid/stroeerCore.yaml
index 0a1b3059..ef95d238 100644
--- a/config/test/bidder-info-valid/stroeerCore.yaml
+++ b/config/test/bidder-info-valid/stroeerCore.yaml
@@ -11,6 +11,10 @@ capabilities:
       - banner
       - video
       - native
+  dooh:
+    mediaTypes:
+      - banner
+      - video
 userSync:
   key: "foo"
   default: "iframe"
diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go
index 68ab0ced..29fe74b2 100644
--- a/endpoints/cookie_sync_test.go
+++ b/endpoints/cookie_sync_test.go
@@ -515,9 +515,9 @@ func TestCookieSyncParseRequest(t *testing.T) {
 			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
 			givenCCPAEnabled: true,
 			givenConfig: config.UserSync{
+				PriorityGroups: [][]string{{"a", "b", "c"}},
 				Cooperative: config.UserSyncCooperative{
 					EnabledByDefault: false,
-					PriorityGroups:   [][]string{{"a", "b", "c"}},
 				},
 			},
 			expectedPrivacy: macros.UserSyncPrivacy{
@@ -557,9 +557,9 @@ func TestCookieSyncParseRequest(t *testing.T) {
 			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
 			givenCCPAEnabled: true,
 			givenConfig: config.UserSync{
+				PriorityGroups: [][]string{{"a", "b", "c"}},
 				Cooperative: config.UserSyncCooperative{
 					EnabledByDefault: false,
-					PriorityGroups:   [][]string{{"a", "b", "c"}},
 				},
 			},
 			expectedPrivacy: macros.UserSyncPrivacy{
@@ -608,9 +608,9 @@ func TestCookieSyncParseRequest(t *testing.T) {
 			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
 			givenCCPAEnabled: true,
 			givenConfig: config.UserSync{
+				PriorityGroups: [][]string{{"a", "b", "c"}},
 				Cooperative: config.UserSyncCooperative{
 					EnabledByDefault: true,
-					PriorityGroups:   [][]string{{"a", "b", "c"}},
 				},
 			},
 			expectedPrivacy: macros.UserSyncPrivacy{},
@@ -635,9 +635,9 @@ func TestCookieSyncParseRequest(t *testing.T) {
 			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
 			givenCCPAEnabled: true,
 			givenConfig: config.UserSync{
+				PriorityGroups: [][]string{{"a", "b", "c"}},
 				Cooperative: config.UserSyncCooperative{
 					EnabledByDefault: false,
-					PriorityGroups:   [][]string{{"a", "b", "c"}},
 				},
 			},
 			expectedPrivacy: macros.UserSyncPrivacy{},
@@ -662,9 +662,9 @@ func TestCookieSyncParseRequest(t *testing.T) {
 			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
 			givenCCPAEnabled: true,
 			givenConfig: config.UserSync{
+				PriorityGroups: [][]string{{"a", "b", "c"}},
 				Cooperative: config.UserSyncCooperative{
 					EnabledByDefault: true,
-					PriorityGroups:   [][]string{{"a", "b", "c"}},
 				},
 			},
 			expectedPrivacy: macros.UserSyncPrivacy{},
@@ -689,9 +689,9 @@ func TestCookieSyncParseRequest(t *testing.T) {
 			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
 			givenCCPAEnabled: true,
 			givenConfig: config.UserSync{
+				PriorityGroups: [][]string{{"a", "b", "c"}},
 				Cooperative: config.UserSyncCooperative{
 					EnabledByDefault: false,
-					PriorityGroups:   [][]string{{"a", "b", "c"}},
 				},
 			},
 			expectedPrivacy: macros.UserSyncPrivacy{},
@@ -716,9 +716,9 @@ func TestCookieSyncParseRequest(t *testing.T) {
 			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
 			givenCCPAEnabled: true,
 			givenConfig: config.UserSync{
+				PriorityGroups: [][]string{{"a", "b", "c"}},
 				Cooperative: config.UserSyncCooperative{
 					EnabledByDefault: true,
-					PriorityGroups:   [][]string{{"a", "b", "c"}},
 				},
 			},
 			expectedPrivacy: macros.UserSyncPrivacy{},
@@ -743,9 +743,9 @@ func TestCookieSyncParseRequest(t *testing.T) {
 			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
 			givenCCPAEnabled: true,
 			givenConfig: config.UserSync{
+				PriorityGroups: [][]string{{"a", "b", "c"}},
 				Cooperative: config.UserSyncCooperative{
 					EnabledByDefault: false,
-					PriorityGroups:   [][]string{{"a", "b", "c"}},
 				},
 			},
 			expectedPrivacy: macros.UserSyncPrivacy{},
@@ -890,9 +890,8 @@ func TestCookieSyncParseRequest(t *testing.T) {
 			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
 			givenCCPAEnabled: true,
 			givenConfig: config.UserSync{
-				Cooperative: config.UserSyncCooperative{
-					PriorityGroups: [][]string{{"a", "b", "c"}},
-				},
+				PriorityGroups: [][]string{{"a", "b", "c"}},
+				Cooperative:    config.UserSyncCooperative{},
 			},
 			expectedPrivacy: macros.UserSyncPrivacy{},
 			expectedRequest: usersync.Request{
@@ -921,9 +920,9 @@ func TestCookieSyncParseRequest(t *testing.T) {
 			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
 			givenCCPAEnabled: true,
 			givenConfig: config.UserSync{
+				PriorityGroups: [][]string{{"a", "b", "c"}},
 				Cooperative: config.UserSyncCooperative{
 					EnabledByDefault: false,
-					PriorityGroups:   [][]string{{"a", "b", "c"}},
 				},
 			},
 			expectedPrivacy: macros.UserSyncPrivacy{},
@@ -953,9 +952,9 @@ func TestCookieSyncParseRequest(t *testing.T) {
 			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
 			givenCCPAEnabled: true,
 			givenConfig: config.UserSync{
+				PriorityGroups: [][]string{{"a", "b", "c"}},
 				Cooperative: config.UserSyncCooperative{
 					EnabledByDefault: false,
-					PriorityGroups:   [][]string{{"a", "b", "c"}},
 				},
 			},
 			expectedPrivacy: macros.UserSyncPrivacy{},
diff --git a/endpoints/info/bidders_detail_test.go b/endpoints/info/bidders_detail_test.go
index 47e5a068..3be24acf 100644
--- a/endpoints/info/bidders_detail_test.go
+++ b/endpoints/info/bidders_detail_test.go
@@ -225,6 +225,7 @@ func TestMapDetailFromConfig(t *testing.T) {
 				Capabilities: &config.CapabilitiesInfo{
 					App:  &config.PlatformInfo{MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner}},
 					Site: &config.PlatformInfo{MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeVideo}},
+					DOOH: &config.PlatformInfo{MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeNative}},
 				},
 			},
 			expected: bidderDetail{
@@ -236,6 +237,7 @@ func TestMapDetailFromConfig(t *testing.T) {
 				Capabilities: &capabilities{
 					App:  &platform{MediaTypes: []string{"banner"}},
 					Site: &platform{MediaTypes: []string{"video"}},
+					DOOH: &platform{MediaTypes: []string{"native"}},
 				},
 				AliasOf: "",
 			},
diff --git a/endpoints/info/bidders_test.go b/endpoints/info/bidders_test.go
index 8aec0972..1f483e5d 100644
--- a/endpoints/info/bidders_test.go
+++ b/endpoints/info/bidders_test.go
@@ -12,238 +12,470 @@ import (
 
 func TestPrepareBiddersResponseAll(t *testing.T) {
 	var (
-		enabled  = config.BidderInfo{Disabled: false}
-		disabled = config.BidderInfo{Disabled: true}
+		enabledCore   = config.BidderInfo{Disabled: false}
+		enabledAlias  = config.BidderInfo{Disabled: false, AliasOf: "something"}
+		disabledCore  = config.BidderInfo{Disabled: true}
+		disabledAlias = config.BidderInfo{Disabled: true, AliasOf: "something"}
 	)
 
 	testCases := []struct {
-		description  string
+		name                string
+		givenBidders        config.BidderInfos
+		givenRequestAliases map[string]string
+		expected            string
+	}{
+		{
+			name:                "none",
+			givenBidders:        config.BidderInfos{},
+			givenRequestAliases: nil,
+			expected:            `[]`,
+		},
+		{
+			name:                "core-one-enabled",
+			givenBidders:        config.BidderInfos{"a": enabledCore},
+			givenRequestAliases: nil,
+			expected:            `["a"]`,
+		},
+		{
+			name:                "core-one-disabled",
+			givenBidders:        config.BidderInfos{"a": disabledCore},
+			givenRequestAliases: nil,
+			expected:            `["a"]`,
+		},
+		{
+			name:                "core-one-mixed",
+			givenBidders:        config.BidderInfos{"a": disabledCore, "b": enabledCore},
+			givenRequestAliases: nil,
+			expected:            `["a","b"]`,
+		},
+		{
+			name:                "core-one-mixed-sorted",
+			givenBidders:        config.BidderInfos{"z": enabledCore, "a": enabledCore},
+			givenRequestAliases: nil,
+			expected:            `["a","z"]`,
+		},
+		{
+			name:                "alias-one",
+			givenBidders:        config.BidderInfos{"a": enabledAlias},
+			givenRequestAliases: nil,
+			expected:            `["a"]`,
+		},
+		{
+			name:                "alias-mixed",
+			givenBidders:        config.BidderInfos{"a": disabledCore, "b": disabledAlias, "c": enabledCore, "d": enabledAlias},
+			givenRequestAliases: nil,
+			expected:            `["a","b","c","d"]`,
+		},
+		{
+			name:                "alias-mixed-sorted",
+			givenBidders:        config.BidderInfos{"z": enabledAlias, "a": enabledCore},
+			givenRequestAliases: nil,
+			expected:            `["a","z"]`,
+		},
+		{
+			name:                "defaultrequest-one",
+			givenBidders:        config.BidderInfos{"a": enabledCore},
+			givenRequestAliases: map[string]string{"b": "a"},
+			expected:            `["a","b"]`,
+		},
+		{
+			name:                "defaultrequest-mixed",
+			givenBidders:        config.BidderInfos{"a": enabledCore, "b": disabledCore},
+			givenRequestAliases: map[string]string{"x": "a", "y": "b"},
+			expected:            `["a","b","x","y"]`,
+		},
+		{
+			name:                "defaultrequest-mixed-sorted",
+			givenBidders:        config.BidderInfos{"z": enabledCore},
+			givenRequestAliases: map[string]string{"a": "z"},
+			expected:            `["a","z"]`,
+		},
+		{
+			name:                "mixed",
+			givenBidders:        config.BidderInfos{"a": disabledCore, "b": disabledAlias, "c": enabledCore, "d": enabledAlias},
+			givenRequestAliases: map[string]string{"z": "a"},
+			expected:            `["a","b","c","d","z"]`,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			result, err := prepareBiddersResponseAll(test.givenBidders, test.givenRequestAliases)
+			assert.NoError(t, err)
+			assert.Equal(t, []byte(test.expected), result)
+		})
+	}
+}
+
+func TestPrepareBiddersResponseAllBaseOnly(t *testing.T) {
+	var (
+		enabledCore   = config.BidderInfo{Disabled: false}
+		enabledAlias  = config.BidderInfo{Disabled: false, AliasOf: "something"}
+		disabledCore  = config.BidderInfo{Disabled: true}
+		disabledAlias = config.BidderInfo{Disabled: true, AliasOf: "something"}
+	)
+
+	testCases := []struct {
+		name         string
 		givenBidders config.BidderInfos
-		givenAliases map[string]string
 		expected     string
 	}{
 		{
-			description:  "None",
+			name:         "none",
 			givenBidders: config.BidderInfos{},
-			givenAliases: nil,
 			expected:     `[]`,
 		},
 		{
-			description:  "Core Bidders Only - One - Enabled",
-			givenBidders: config.BidderInfos{"a": enabled},
-			givenAliases: nil,
+			name:         "core-one-enabled",
+			givenBidders: config.BidderInfos{"a": enabledCore},
 			expected:     `["a"]`,
 		},
 		{
-			description:  "Core Bidders Only - One - Disabled",
-			givenBidders: config.BidderInfos{"a": disabled},
-			givenAliases: nil,
+			name:         "core-one-disabled",
+			givenBidders: config.BidderInfos{"a": disabledCore},
 			expected:     `["a"]`,
 		},
 		{
-			description:  "Core Bidders Only - Many",
-			givenBidders: config.BidderInfos{"a": enabled, "b": enabled},
-			givenAliases: nil,
+			name:         "core-one-mixed",
+			givenBidders: config.BidderInfos{"a": disabledCore, "b": enabledCore},
 			expected:     `["a","b"]`,
 		},
 		{
-			description:  "Core Bidders Only - Many - Mixed",
-			givenBidders: config.BidderInfos{"a": disabled, "b": enabled},
-			givenAliases: nil,
-			expected:     `["a","b"]`,
+			name:         "core-one-mixed-sorted",
+			givenBidders: config.BidderInfos{"z": enabledCore, "a": enabledCore},
+			expected:     `["a","z"]`,
 		},
 		{
-			description:  "Core Bidders Only - Many - Sorted",
-			givenBidders: config.BidderInfos{"b": enabled, "a": enabled},
-			givenAliases: nil,
-			expected:     `["a","b"]`,
+			name:         "alias-one",
+			givenBidders: config.BidderInfos{"a": enabledAlias},
+			expected:     `[]`,
 		},
 		{
-			description:  "With Aliases - One",
-			givenBidders: config.BidderInfos{"a": enabled},
-			givenAliases: map[string]string{"b": "a"},
-			expected:     `["a","b"]`,
+			name:         "alias-mixed",
+			givenBidders: config.BidderInfos{"a": disabledCore, "b": disabledAlias, "c": enabledCore, "d": enabledAlias},
+			expected:     `["a","c"]`,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			result, err := prepareBiddersResponseAllBaseOnly(test.givenBidders)
+			assert.NoError(t, err)
+			assert.Equal(t, []byte(test.expected), result)
+		})
+	}
+}
+
+func TestPrepareBiddersResponseEnabledOnly(t *testing.T) {
+	var (
+		enabledCore   = config.BidderInfo{Disabled: false}
+		enabledAlias  = config.BidderInfo{Disabled: false, AliasOf: "something"}
+		disabledCore  = config.BidderInfo{Disabled: true}
+		disabledAlias = config.BidderInfo{Disabled: true, AliasOf: "something"}
+	)
+
+	testCases := []struct {
+		name                string
+		givenBidders        config.BidderInfos
+		givenRequestAliases map[string]string
+		expected            string
+	}{
+		{
+			name:                "none",
+			givenBidders:        config.BidderInfos{},
+			givenRequestAliases: nil,
+			expected:            `[]`,
 		},
 		{
-			description:  "With Aliases - Many",
-			givenBidders: config.BidderInfos{"a": enabled, "b": disabled},
-			givenAliases: map[string]string{"x": "a", "y": "b"},
-			expected:     `["a","b","x","y"]`,
+			name:                "core-one-enabled",
+			givenBidders:        config.BidderInfos{"a": enabledCore},
+			givenRequestAliases: nil,
+			expected:            `["a"]`,
 		},
 		{
-			description:  "With Aliases - Sorted",
-			givenBidders: config.BidderInfos{"z": enabled},
-			givenAliases: map[string]string{"a": "z"},
-			expected:     `["a","z"]`,
+			name:                "core-one-disabled",
+			givenBidders:        config.BidderInfos{"a": disabledCore},
+			givenRequestAliases: nil,
+			expected:            `[]`,
+		},
+		{
+			name:                "core-one-mixed",
+			givenBidders:        config.BidderInfos{"a": disabledCore, "b": enabledCore},
+			givenRequestAliases: nil,
+			expected:            `["b"]`,
+		},
+		{
+			name:                "core-one-mixed-sorted",
+			givenBidders:        config.BidderInfos{"z": enabledCore, "a": enabledCore},
+			givenRequestAliases: nil,
+			expected:            `["a","z"]`,
+		},
+		{
+			name:                "alias-one",
+			givenBidders:        config.BidderInfos{"a": enabledAlias},
+			givenRequestAliases: nil,
+			expected:            `["a"]`,
+		},
+		{
+			name:                "alias-mixed",
+			givenBidders:        config.BidderInfos{"a": disabledCore, "b": disabledAlias, "c": enabledCore, "d": enabledAlias},
+			givenRequestAliases: nil,
+			expected:            `["c","d"]`,
+		},
+		{
+			name:                "alias-mixed-sorted",
+			givenBidders:        config.BidderInfos{"z": enabledAlias, "a": enabledCore},
+			givenRequestAliases: nil,
+			expected:            `["a","z"]`,
+		},
+		{
+			name:                "defaultrequest-one",
+			givenBidders:        config.BidderInfos{"a": enabledCore},
+			givenRequestAliases: map[string]string{"b": "a"},
+			expected:            `["a","b"]`,
+		},
+		{
+			name:                "defaultrequest-mixed",
+			givenBidders:        config.BidderInfos{"a": enabledCore, "b": disabledCore},
+			givenRequestAliases: map[string]string{"x": "a", "y": "b"},
+			expected:            `["a","x"]`,
+		},
+		{
+			name:                "defaultrequest-mixed-sorted",
+			givenBidders:        config.BidderInfos{"z": enabledCore},
+			givenRequestAliases: map[string]string{"a": "z"},
+			expected:            `["a","z"]`,
+		},
+		{
+			name:                "mixed",
+			givenBidders:        config.BidderInfos{"a": disabledCore, "b": disabledAlias, "c": enabledCore, "d": enabledAlias},
+			givenRequestAliases: map[string]string{"z": "a"},
+			expected:            `["c","d"]`,
 		},
 	}
 
 	for _, test := range testCases {
-		result, err := prepareBiddersResponseAll(test.givenBidders, test.givenAliases)
-
-		assert.NoError(t, err, test.description)
-		assert.Equal(t, []byte(test.expected), result, test.description)
+		t.Run(test.name, func(t *testing.T) {
+			result, err := prepareBiddersResponseEnabledOnly(test.givenBidders, test.givenRequestAliases)
+			assert.NoError(t, err)
+			assert.Equal(t, []byte(test.expected), result)
+		})
 	}
 }
 
-func TestPrepareBiddersResponseEnabledOnly(t *testing.T) {
+func TestPrepareBiddersResponseEnabledOnlyBaseOnly(t *testing.T) {
 	var (
-		enabled  = config.BidderInfo{Disabled: false}
-		disabled = config.BidderInfo{Disabled: true}
+		enabledCore   = config.BidderInfo{Disabled: false}
+		enabledAlias  = config.BidderInfo{Disabled: false, AliasOf: "something"}
+		disabledCore  = config.BidderInfo{Disabled: true}
+		disabledAlias = config.BidderInfo{Disabled: true, AliasOf: "something"}
 	)
 
 	testCases := []struct {
-		description  string
+		name         string
 		givenBidders config.BidderInfos
-		givenAliases map[string]string
 		expected     string
 	}{
 		{
-			description:  "None",
+			name:         "none",
 			givenBidders: config.BidderInfos{},
-			givenAliases: nil,
 			expected:     `[]`,
 		},
 		{
-			description:  "Core Bidders Only - One - Enabled",
-			givenBidders: config.BidderInfos{"a": enabled},
-			givenAliases: nil,
+			name:         "core-one-enabled",
+			givenBidders: config.BidderInfos{"a": enabledCore},
 			expected:     `["a"]`,
 		},
 		{
-			description:  "Core Bidders Only - One - Disabled",
-			givenBidders: config.BidderInfos{"a": disabled},
-			givenAliases: nil,
+			name:         "core-one-disabled",
+			givenBidders: config.BidderInfos{"a": disabledCore},
 			expected:     `[]`,
 		},
 		{
-			description:  "Core Bidders Only - Many",
-			givenBidders: config.BidderInfos{"a": enabled, "b": enabled},
-			givenAliases: nil,
-			expected:     `["a","b"]`,
-		},
-		{
-			description:  "Core Bidders Only - Many - Mixed",
-			givenBidders: config.BidderInfos{"a": disabled, "b": enabled},
-			givenAliases: nil,
+			name:         "core-one-mixed",
+			givenBidders: config.BidderInfos{"a": disabledCore, "b": enabledCore},
 			expected:     `["b"]`,
 		},
 		{
-			description:  "Core Bidders Only - Many - Sorted",
-			givenBidders: config.BidderInfos{"b": enabled, "a": enabled},
-			givenAliases: nil,
-			expected:     `["a","b"]`,
+			name:         "core-one-mixed-sorted",
+			givenBidders: config.BidderInfos{"z": enabledCore, "a": enabledCore},
+			expected:     `["a","z"]`,
 		},
 		{
-			description:  "With Aliases - One",
-			givenBidders: config.BidderInfos{"a": enabled},
-			givenAliases: map[string]string{"b": "a"},
-			expected:     `["a","b"]`,
+			name:         "alias-one",
+			givenBidders: config.BidderInfos{"a": enabledAlias},
+			expected:     `[]`,
 		},
 		{
-			description:  "With Aliases - Many",
-			givenBidders: config.BidderInfos{"a": enabled, "b": disabled},
-			givenAliases: map[string]string{"x": "a", "y": "b"},
-			expected:     `["a","x"]`,
+			name:         "alias-many",
+			givenBidders: config.BidderInfos{"a": enabledAlias, "b": enabledAlias},
+			expected:     `[]`,
 		},
 		{
-			description:  "With Aliases - Sorted",
-			givenBidders: config.BidderInfos{"z": enabled},
-			givenAliases: map[string]string{"a": "z"},
-			expected:     `["a","z"]`,
+			name:         "mixed",
+			givenBidders: config.BidderInfos{"a": disabledCore, "b": disabledAlias, "c": enabledCore, "d": enabledAlias},
+			expected:     `["c"]`,
 		},
 	}
 
 	for _, test := range testCases {
-		result, err := prepareBiddersResponseEnabledOnly(test.givenBidders, test.givenAliases)
-
-		assert.NoError(t, err, test.description)
-		assert.Equal(t, []byte(test.expected), result, test.description)
+		t.Run(test.name, func(t *testing.T) {
+			result, err := prepareBiddersResponseEnabledOnlyBaseOnly(test.givenBidders)
+			assert.NoError(t, err)
+			assert.Equal(t, []byte(test.expected), result)
+		})
 	}
 }
 
 func TestBiddersHandler(t *testing.T) {
 	var (
-		enabled  = config.BidderInfo{Disabled: false}
-		disabled = config.BidderInfo{Disabled: true}
+		enabledCore   = config.BidderInfo{Disabled: false}
+		enabledAlias  = config.BidderInfo{Disabled: false, AliasOf: "something"}
+		disabledCore  = config.BidderInfo{Disabled: true}
+		disabledAlias = config.BidderInfo{Disabled: true, AliasOf: "something"}
 	)
 
-	bidders := config.BidderInfos{"a": enabled, "b": disabled}
-	aliases := map[string]string{"x": "a", "y": "b"}
+	bidders := config.BidderInfos{"a": enabledCore, "b": enabledAlias, "c": disabledCore, "d": disabledAlias}
+	aliases := map[string]string{"x": "a", "y": "c"}
 
 	testCases := []struct {
-		description     string
+		name            string
 		givenURL        string
 		expectedStatus  int
 		expectedBody    string
 		expectedHeaders http.Header
 	}{
 		{
-			description:     "No Query Parameters - Backwards Compatibility",
+			name:            "simple",
 			givenURL:        "/info/bidders",
 			expectedStatus:  http.StatusOK,
-			expectedBody:    `["a","b","x","y"]`,
+			expectedBody:    `["a","b","c","d","x","y"]`,
 			expectedHeaders: http.Header{"Content-Type": []string{"application/json"}},
 		},
 		{
-			description:     "Enabled Only - False",
+			name:            "enabledonly-false",
 			givenURL:        "/info/bidders?enabledonly=false",
 			expectedStatus:  http.StatusOK,
-			expectedBody:    `["a","b","x","y"]`,
+			expectedBody:    `["a","b","c","d","x","y"]`,
 			expectedHeaders: http.Header{"Content-Type": []string{"application/json"}},
 		},
 		{
-			description:     "Enabled Only - False - Case Insensitive",
+			name:            "enabledonly-false-caseinsensitive",
 			givenURL:        "/info/bidders?enabledonly=fAlSe",
 			expectedStatus:  http.StatusOK,
-			expectedBody:    `["a","b","x","y"]`,
+			expectedBody:    `["a","b","c","d","x","y"]`,
 			expectedHeaders: http.Header{"Content-Type": []string{"application/json"}},
 		},
 		{
-			description:     "Enabled Only - True",
+			name:            "enabledonly-true",
 			givenURL:        "/info/bidders?enabledonly=true",
 			expectedStatus:  http.StatusOK,
-			expectedBody:    `["a","x"]`,
+			expectedBody:    `["a","b","x"]`,
 			expectedHeaders: http.Header{"Content-Type": []string{"application/json"}},
 		},
 		{
-			description:     "Enabled Only - True - Case Insensitive",
+			name:            "enabledonly-true-caseinsensitive",
 			givenURL:        "/info/bidders?enabledonly=TrUe",
 			expectedStatus:  http.StatusOK,
-			expectedBody:    `["a","x"]`,
+			expectedBody:    `["a","b","x"]`,
 			expectedHeaders: http.Header{"Content-Type": []string{"application/json"}},
 		},
 		{
-			description:     "Enabled Only - Invalid",
+			name:            "enabledonly-invalid",
 			givenURL:        "/info/bidders?enabledonly=foo",
 			expectedStatus:  http.StatusBadRequest,
 			expectedBody:    `Invalid value for 'enabledonly' query param, must be of boolean type`,
 			expectedHeaders: http.Header{},
 		},
 		{
-			description:     "Enabled Only - Missing Value",
+			name:            "enabledonly-missing",
 			givenURL:        "/info/bidders?enabledonly=",
 			expectedStatus:  http.StatusBadRequest,
 			expectedBody:    `Invalid value for 'enabledonly' query param, must be of boolean type`,
 			expectedHeaders: http.Header{},
 		},
+		{
+			name:            "baseonly-false",
+			givenURL:        "/info/bidders?baseadaptersonly=false",
+			expectedStatus:  http.StatusOK,
+			expectedBody:    `["a","b","c","d","x","y"]`,
+			expectedHeaders: http.Header{"Content-Type": []string{"application/json"}},
+		},
+		{
+			name:            "baseonly-false-caseinsensitive",
+			givenURL:        "/info/bidders?baseadaptersonly=fAlSe",
+			expectedStatus:  http.StatusOK,
+			expectedBody:    `["a","b","c","d","x","y"]`,
+			expectedHeaders: http.Header{"Content-Type": []string{"application/json"}},
+		},
+		{
+			name:            "baseonly-true",
+			givenURL:        "/info/bidders?baseadaptersonly=true",
+			expectedStatus:  http.StatusOK,
+			expectedBody:    `["a","c"]`,
+			expectedHeaders: http.Header{"Content-Type": []string{"application/json"}},
+		},
+		{
+			name:            "baseonly-true-caseinsensitive",
+			givenURL:        "/info/bidders?baseadaptersonly=TrUe",
+			expectedStatus:  http.StatusOK,
+			expectedBody:    `["a","c"]`,
+			expectedHeaders: http.Header{"Content-Type": []string{"application/json"}},
+		},
+		{
+			name:            "baseonly-invalid",
+			givenURL:        "/info/bidders?baseadaptersonly=foo",
+			expectedStatus:  http.StatusBadRequest,
+			expectedBody:    `Invalid value for 'baseadaptersonly' query param, must be of boolean type`,
+			expectedHeaders: http.Header{},
+		},
+		{
+			name:            "baseonly-missing",
+			givenURL:        "/info/bidders?baseadaptersonly=",
+			expectedStatus:  http.StatusBadRequest,
+			expectedBody:    `Invalid value for 'baseadaptersonly' query param, must be of boolean type`,
+			expectedHeaders: http.Header{},
+		},
+		{
+			name:            "enabledonly-true-baseonly-false",
+			givenURL:        "/info/bidders?enabledonly=true&baseadaptersonly=false",
+			expectedStatus:  http.StatusOK,
+			expectedBody:    `["a","b","x"]`,
+			expectedHeaders: http.Header{"Content-Type": []string{"application/json"}},
+		},
+		{
+			name:            "enabledonly-false-baseonly-true",
+			givenURL:        "/info/bidders?enabledonly=false&baseadaptersonly=true",
+			expectedStatus:  http.StatusOK,
+			expectedBody:    `["a","c"]`,
+			expectedHeaders: http.Header{"Content-Type": []string{"application/json"}},
+		},
+		{
+			name:            "enabledonly-true-baseonly-true",
+			givenURL:        "/info/bidders?enabledonly=true&baseadaptersonly=true",
+			expectedStatus:  http.StatusOK,
+			expectedBody:    `["a"]`,
+			expectedHeaders: http.Header{"Content-Type": []string{"application/json"}},
+		},
 	}
 
 	for _, test := range testCases {
-		handler := NewBiddersEndpoint(bidders, aliases)
+		t.Run(test.name, func(t *testing.T) {
+			handler := NewBiddersEndpoint(bidders, aliases)
 
-		request := httptest.NewRequest("GET", test.givenURL, nil)
+			request := httptest.NewRequest("GET", test.givenURL, nil)
 
-		responseRecorder := httptest.NewRecorder()
-		handler(responseRecorder, request, nil)
+			responseRecorder := httptest.NewRecorder()
+			handler(responseRecorder, request, nil)
 
-		result := responseRecorder.Result()
-		assert.Equal(t, result.StatusCode, test.expectedStatus)
+			result := responseRecorder.Result()
+			assert.Equal(t, result.StatusCode, test.expectedStatus)
 
-		resultBody, _ := io.ReadAll(result.Body)
-		assert.Equal(t, []byte(test.expectedBody), resultBody)
+			resultBody, _ := io.ReadAll(result.Body)
+			assert.Equal(t, []byte(test.expectedBody), resultBody)
 
-		resultHeaders := result.Header
-		assert.Equal(t, test.expectedHeaders, resultHeaders)
+			resultHeaders := result.Header
+			assert.Equal(t, test.expectedHeaders, resultHeaders)
+		})
 	}
 }
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index 84e5cf4b..f60fd98e 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -205,7 +205,7 @@ func runEndToEndTest(t *testing.T, auctionEndpointHandler httprouter.Handle, tes
 		err = json.Unmarshal(test.ExpectedBidResponse, &expectedBidResponse)
 		if assert.NoError(t, err, "Could not unmarshal expected bidResponse taken from test file.\n Test file: %s\n Error:%s\n", testFile, err) {
 			err = json.Unmarshal([]byte(actualJsonBidResponse), &actualBidResponse)
-			if assert.NoError(t, err, "Could not unmarshal actual bidResponse from auction.\n Test file: %s\n Error:%s\n", testFile, err) {
+			if assert.NoError(t, err, "Could not unmarshal actual bidResponse from auction.\n Test file: %s\n Error:%s\n actualJsonBidResponse: %s", testFile, err, actualJsonBidResponse) {
 				assertBidResponseEqual(t, testFile, expectedBidResponse, actualBidResponse)
 			}
 		}
@@ -1541,6 +1541,81 @@ func TestMergeBidderParamsImpExtPrebid(t *testing.T) {
 	}
 }
 
+func TestValidateExactlyOneInventoryType(t *testing.T) {
+
+	testCases := []struct {
+		description         string
+		givenRequestWrapper *openrtb_ext.RequestWrapper
+		expectedError       error
+	}{
+		{
+			description:         "None provided - invalid",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}},
+			expectedError:       errors.New("One of request.site or request.app or request.dooh must be defined"),
+		},
+		{
+			description: "Only site provided",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{
+				Site: &openrtb2.Site{},
+			}},
+			expectedError: nil,
+		},
+		{
+			description: "Only app provided",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{
+				App: &openrtb2.App{},
+			}},
+			expectedError: nil,
+		},
+		{
+			description: "Only dooh provided",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{
+				DOOH: &openrtb2.DOOH{},
+			}},
+			expectedError: nil,
+		},
+		{
+			description: "Two provided (site+app) - invalid",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{
+				Site: &openrtb2.Site{},
+				App:  &openrtb2.App{},
+			}},
+			expectedError: errors.New("No more than one of request.site or request.app or request.dooh can be defined"),
+		},
+		{
+			description: "Two provided (site+dooh) - invalid",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{
+				Site: &openrtb2.Site{},
+				DOOH: &openrtb2.DOOH{},
+			}},
+			expectedError: errors.New("No more than one of request.site or request.app or request.dooh can be defined"),
+		},
+		{
+			description: "Two provided (app+dooh) - invalid",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{
+				App:  &openrtb2.App{},
+				DOOH: &openrtb2.DOOH{},
+			}},
+			expectedError: errors.New("No more than one of request.site or request.app or request.dooh can be defined"),
+		},
+		{
+			description: "Three provided - invalid",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{
+				Site: &openrtb2.Site{},
+				App:  &openrtb2.App{},
+				DOOH: &openrtb2.DOOH{},
+			}},
+			expectedError: errors.New("No more than one of request.site or request.app or request.dooh can be defined"),
+		},
+	}
+
+	for _, test := range testCases {
+		error := validateExactlyOneInventoryType(test.givenRequestWrapper)
+		assert.Equalf(t, test.expectedError, error, "Error doesn't match: %s\n", test.description)
+	}
+
+}
+
 func TestValidateRequest(t *testing.T) {
 	deps := &endpointDeps{
 		fakeUUIDGenerator{},
@@ -1725,6 +1800,66 @@ func TestValidateRequest(t *testing.T) {
 			expectedErrorList:     []error{},
 			expectedChannelObject: &openrtb_ext.ExtRequestPrebidChannel{Name: appChannel, Version: ""},
 		},
+		{
+			description: "Minimum required site attributes missing",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					ID:   "Some-ID",
+					Site: &openrtb2.Site{},
+					Imp: []openrtb2.Imp{
+						{
+							ID: "Some-Imp-ID",
+							Banner: &openrtb2.Banner{
+								Format: []openrtb2.Format{
+									{
+										W: 600,
+										H: 500,
+									},
+									{
+										W: 300,
+										H: 600,
+									},
+								},
+							},
+							Ext: []byte(`{"appnexus":{"placementId": 12345678}}`),
+						},
+					},
+				},
+			},
+			expectedErrorList: []error{
+				errors.New("request.site should include at least one of request.site.id or request.site.page."),
+			},
+		},
+		{
+			description: "Minimum required DOOH attributes missing",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					ID:   "Some-ID",
+					DOOH: &openrtb2.DOOH{},
+					Imp: []openrtb2.Imp{
+						{
+							ID: "Some-Imp-ID",
+							Banner: &openrtb2.Banner{
+								Format: []openrtb2.Format{
+									{
+										W: 600,
+										H: 500,
+									},
+									{
+										W: 300,
+										H: 600,
+									},
+								},
+							},
+							Ext: []byte(`{"appnexus":{"placementId": 12345678}}`),
+						},
+					},
+				},
+			},
+			expectedErrorList: []error{
+				errors.New("request.dooh should include at least one of request.dooh.id or request.dooh.venuetype."),
+			},
+		},
 	}
 
 	for _, test := range testCases {
@@ -3237,6 +3372,78 @@ func TestMapSChains(t *testing.T) {
 	}
 }
 
+func TestSearchAccountID(t *testing.T) {
+	// Correctness for lookup within Publisher object left to TestGetAccountID
+	// This however tests the expected lookup paths in outer site, app and dooh
+	testCases := []struct {
+		description       string
+		request           []byte
+		expectedAccID     string
+		expectedError     error
+		expectedIsAppReq  bool
+		expectedIsSiteReq bool
+		expectedIsDOOHReq bool
+	}{
+		{
+			description:       "No publisher available",
+			request:           []byte(`{}`),
+			expectedAccID:     "",
+			expectedError:     nil,
+			expectedIsAppReq:  false,
+			expectedIsDOOHReq: false,
+		},
+		{
+			description:       "Publisher.ID doesn't exist",
+			request:           []byte(`{"site":{"publisher":{}}}`),
+			expectedAccID:     "",
+			expectedError:     nil,
+			expectedIsAppReq:  false,
+			expectedIsDOOHReq: false,
+		},
+		{
+			description:       "Publisher.ID not a string",
+			request:           []byte(`{"site":{"publisher":{"id":42}}}`),
+			expectedAccID:     "",
+			expectedError:     errors.New("site.publisher.id must be a string"),
+			expectedIsAppReq:  false,
+			expectedIsDOOHReq: false,
+		},
+		{
+			description:       "Publisher available in request.site",
+			request:           []byte(`{"site":{"publisher":{"id":"42"}}}`),
+			expectedAccID:     "42",
+			expectedError:     nil,
+			expectedIsAppReq:  false,
+			expectedIsDOOHReq: false,
+		},
+		{
+			description:       "Publisher available in request.app",
+			request:           []byte(`{"app":{"publisher":{"id":"42"}}}`),
+			expectedAccID:     "42",
+			expectedError:     nil,
+			expectedIsAppReq:  true,
+			expectedIsDOOHReq: false,
+		},
+		{
+			description:       "Publisher available in request.dooh",
+			request:           []byte(`{"dooh":{"publisher":{"id":"42"}}}`),
+			expectedAccID:     "42",
+			expectedError:     nil,
+			expectedIsAppReq:  false,
+			expectedIsDOOHReq: true,
+		},
+	}
+
+	for _, test := range testCases {
+		accountId, isAppReq, isDOOHReq, err := searchAccountId(test.request)
+		assert.Equal(t, test.expectedAccID, accountId, "searchAccountID should return expected account ID for test case: %s", test.description)
+		assert.Equal(t, test.expectedIsAppReq, isAppReq, "searchAccountID should return expected isAppReq for test case: %s", test.description)
+		assert.Equal(t, test.expectedIsDOOHReq, isDOOHReq, "searchAccountID should return expected isDOOHReq for test case: %s", test.description)
+		assert.Equal(t, test.expectedError, err, "searchAccountID should return expected error for test case: %s", test.description)
+	}
+
+}
+
 func TestGetAccountID(t *testing.T) {
 	testPubID := "test-pub"
 	testParentAccount := "test-account"
diff --git a/endpoints/setuid_test.go b/endpoints/setuid_test.go
index 4f37eb77..897538c3 100644
--- a/endpoints/setuid_test.go
+++ b/endpoints/setuid_test.go
@@ -8,6 +8,7 @@ import (
 	"net/http/httptest"
 	"net/url"
 	"regexp"
+	"strings"
 	"testing"
 	"time"
 
@@ -332,7 +333,7 @@ func TestSetUIDEndpoint(t *testing.T) {
 
 	for _, test := range testCases {
 		response := doRequest(makeRequest(test.uri, test.existingSyncs), analytics, metrics,
-			test.syncersBidderNameToKey, test.gdprAllowsHostCookies, test.gdprReturnsError, test.gdprMalformed, false)
+			test.syncersBidderNameToKey, test.gdprAllowsHostCookies, test.gdprReturnsError, test.gdprMalformed, false, 0, nil)
 		assert.Equal(t, test.expectedStatusCode, response.Code, "Test Case: %s. /setuid returned unexpected error code", test.description)
 
 		if test.expectedSyncs != nil {
@@ -359,6 +360,135 @@ func TestSetUIDEndpoint(t *testing.T) {
 	}
 }
 
+func TestSetUIDPriorityEjection(t *testing.T) {
+	decoder := usersync.Base64Decoder{}
+	analytics := analyticsConf.NewPBSAnalytics(&config.Analytics{})
+	syncersByBidder := map[string]string{
+		"pubmatic":             "pubmatic",
+		"syncer1":              "syncer1",
+		"syncer2":              "syncer2",
+		"syncer3":              "syncer3",
+		"syncer4":              "syncer4",
+		"mismatchedBidderName": "syncer5",
+		"syncerToEject":        "syncerToEject",
+	}
+
+	testCases := []struct {
+		description           string
+		uri                   string
+		givenExistingSyncs    []string
+		givenPriorityGroups   [][]string
+		givenMaxCookieSize    int
+		expectedStatusCode    int
+		expectedSyncer        string
+		expectedUID           string
+		expectedNumOfElements int
+		expectedWarning       string
+	}{
+		{
+			description:           "Cookie empty, expect bidder to be synced, no ejection",
+			uri:                   "/setuid?bidder=pubmatic&uid=123",
+			givenPriorityGroups:   [][]string{},
+			givenMaxCookieSize:    500,
+			expectedSyncer:        "pubmatic",
+			expectedUID:           "123",
+			expectedNumOfElements: 1,
+			expectedStatusCode:    http.StatusOK,
+		},
+		{
+			description:           "Cookie full, no priority groups, one ejection",
+			uri:                   "/setuid?bidder=pubmatic&uid=123",
+			givenExistingSyncs:    []string{"syncer1", "syncer2", "syncer3", "syncer4"},
+			givenPriorityGroups:   [][]string{},
+			givenMaxCookieSize:    500,
+			expectedUID:           "123",
+			expectedSyncer:        "pubmatic",
+			expectedNumOfElements: 4,
+			expectedStatusCode:    http.StatusOK,
+		},
+		{
+			description:           "Cookie full, eject lowest priority element",
+			uri:                   "/setuid?bidder=pubmatic&uid=123",
+			givenExistingSyncs:    []string{"syncer2", "syncer3", "syncer4", "syncerToEject"},
+			givenPriorityGroups:   [][]string{{"pubmatic", "syncer2", "syncer3", "syncer4"}, {"syncerToEject"}},
+			givenMaxCookieSize:    500,
+			expectedUID:           "123",
+			expectedSyncer:        "pubmatic",
+			expectedNumOfElements: 4,
+			expectedStatusCode:    http.StatusOK,
+		},
+		{
+			description:           "Cookie full, all elements same priority, one ejection",
+			uri:                   "/setuid?bidder=pubmatic&uid=123",
+			givenExistingSyncs:    []string{"syncer1", "syncer2", "syncer3", "syncer5"},
+			givenPriorityGroups:   [][]string{{"pubmatic", "syncer1", "syncer2", "syncer3", "mismatchedBidderName"}},
+			givenMaxCookieSize:    500,
+			expectedUID:           "123",
+			expectedSyncer:        "pubmatic",
+			expectedNumOfElements: 4,
+			expectedStatusCode:    http.StatusOK,
+		},
+		{
+			description:         "There are only priority elements left, but the bidder being synced isn't one",
+			uri:                 "/setuid?bidder=pubmatic&uid=123",
+			givenExistingSyncs:  []string{"syncer1", "syncer2", "syncer3", "syncer4"},
+			givenPriorityGroups: [][]string{{"syncer1", "syncer2", "syncer3", "syncer4"}},
+			givenMaxCookieSize:  500,
+			expectedStatusCode:  http.StatusOK,
+			expectedWarning:     "Warning: syncer key is not a priority, and there are only priority elements left, cookie not updated",
+		},
+		{
+			description:        "Uid that's trying to be synced is bigger than MaxCookieSize",
+			uri:                "/setuid?bidder=pubmatic&uid=123",
+			givenMaxCookieSize: 1,
+			expectedStatusCode: http.StatusBadRequest,
+		},
+	}
+	for _, test := range testCases {
+		request := httptest.NewRequest("GET", test.uri, nil)
+
+		// Cookie Set Up
+		cookie := usersync.NewCookie()
+		for _, key := range test.givenExistingSyncs {
+			cookie.Sync(key, "111")
+		}
+		httpCookie, err := ToHTTPCookie(cookie)
+		assert.NoError(t, err)
+		request.AddCookie(httpCookie)
+
+		// Make Request to /setuid
+		response := doRequest(request, analytics, &metricsConf.NilMetricsEngine{}, syncersByBidder, true, false, false, false, test.givenMaxCookieSize, test.givenPriorityGroups)
+
+		if test.expectedWarning != "" {
+			assert.Equal(t, test.expectedWarning, response.Body.String(), test.description)
+		} else if test.expectedSyncer != "" {
+			// Get Cookie From Header
+			var cookieHeader string
+			for k, v := range response.Result().Header {
+				if k == "Set-Cookie" {
+					cookieHeader = v[0]
+				}
+			}
+			encodedCookieValue := getUIDFromHeader(cookieHeader)
+
+			// Check That Bidder On Request was Synced, it's UID matches, and that the right number of elements are present after ejection
+			decodedCookie := decoder.Decode(encodedCookieValue)
+			decodedCookieUIDs := decodedCookie.GetUIDs()
+
+			assert.Equal(t, test.expectedUID, decodedCookieUIDs[test.expectedSyncer], test.description)
+			assert.Equal(t, test.expectedNumOfElements, len(decodedCookieUIDs), test.description)
+
+			// Specific test case handling where we eject the lowest priority element
+			if len(test.givenPriorityGroups) == 2 {
+				syncer := test.givenPriorityGroups[len(test.givenPriorityGroups)-1][0]
+				_, syncerExists := decodedCookieUIDs[syncer]
+				assert.False(t, syncerExists, test.description)
+			}
+		}
+		assert.Equal(t, test.expectedStatusCode, response.Result().StatusCode, test.description)
+	}
+}
+
 func TestParseSignalFromGPPSID(t *testing.T) {
 	type testOutput struct {
 		signal gdpr.Signal
@@ -1214,7 +1344,7 @@ func TestSetUIDEndpointMetrics(t *testing.T) {
 		for _, v := range test.cookies {
 			addCookie(req, v)
 		}
-		response := doRequest(req, analyticsEngine, metricsEngine, test.syncersBidderNameToKey, test.gdprAllowsHostCookies, false, false, test.cfgAccountRequired)
+		response := doRequest(req, analyticsEngine, metricsEngine, test.syncersBidderNameToKey, test.gdprAllowsHostCookies, false, false, test.cfgAccountRequired, 0, nil)
 
 		assert.Equal(t, test.expectedResponseCode, response.Code, test.description)
 		analyticsEngine.AssertExpectations(t)
@@ -1230,7 +1360,7 @@ func TestOptedOut(t *testing.T) {
 	syncersBidderNameToKey := map[string]string{"pubmatic": "pubmatic"}
 	analytics := analyticsConf.NewPBSAnalytics(&config.Analytics{})
 	metrics := &metricsConf.NilMetricsEngine{}
-	response := doRequest(request, analytics, metrics, syncersBidderNameToKey, true, false, false, false)
+	response := doRequest(request, analytics, metrics, syncersBidderNameToKey, true, false, false, false, 0, nil)
 
 	assert.Equal(t, http.StatusUnauthorized, response.Code)
 }
@@ -1341,6 +1471,56 @@ func TestGetResponseFormat(t *testing.T) {
 	}
 }
 
+func TestIsSyncerPriority(t *testing.T) {
+	testCases := []struct {
+		name                           string
+		givenBidderNameFromSyncerQuery string
+		givenPriorityGroups            [][]string
+		expected                       bool
+	}{
+		{
+			name:                           "bidder-name-is-priority",
+			givenBidderNameFromSyncerQuery: "priorityBidder",
+			givenPriorityGroups: [][]string{
+				{"priorityBidder"},
+				{"2", "3"},
+			},
+			expected: true,
+		},
+		{
+			name:                           "bidder-name-is-not-priority",
+			givenBidderNameFromSyncerQuery: "notPriorityBidderName",
+			givenPriorityGroups: [][]string{
+				{"1"},
+				{"2", "3"},
+			},
+			expected: false,
+		},
+		{
+			name:                           "no-bidder-name-given",
+			givenBidderNameFromSyncerQuery: "",
+			givenPriorityGroups: [][]string{
+				{"1"},
+				{"2", "3"},
+			},
+			expected: false,
+		},
+		{
+			name:                           "no-priority-groups-given",
+			givenBidderNameFromSyncerQuery: "bidderName",
+			givenPriorityGroups:            [][]string{},
+			expected:                       false,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			isPriority := isSyncerPriority(test.givenBidderNameFromSyncerQuery, test.givenPriorityGroups)
+			assert.Equal(t, test.expected, isPriority)
+		})
+	}
+}
+
 func assertHasSyncs(t *testing.T, testCase string, resp *httptest.ResponseRecorder, syncs map[string]string) {
 	t.Helper()
 	cookie := parseCookieString(t, resp)
@@ -1366,13 +1546,19 @@ func makeRequest(uri string, existingSyncs map[string]string) *http.Request {
 	return request
 }
 
-func doRequest(req *http.Request, analytics analytics.PBSAnalyticsModule, metrics metrics.MetricsEngine, syncersBidderNameToKey map[string]string, gdprAllowsHostCookies, gdprReturnsError, gdprReturnsMalformedError, cfgAccountRequired bool) *httptest.ResponseRecorder {
+func doRequest(req *http.Request, analytics analytics.PBSAnalyticsModule, metrics metrics.MetricsEngine, syncersBidderNameToKey map[string]string, gdprAllowsHostCookies, gdprReturnsError, gdprReturnsMalformedError, cfgAccountRequired bool, maxCookieSize int, priorityGroups [][]string) *httptest.ResponseRecorder {
 	cfg := config.Configuration{
 		AccountRequired: cfgAccountRequired,
 		BlacklistedAcctMap: map[string]bool{
 			"blocked_acct": true,
 		},
 		AccountDefaults: config.Account{},
+		UserSync: config.UserSync{
+			PriorityGroups: priorityGroups,
+		},
+		HostCookie: config.HostCookie{
+			MaxCookieSizeBytes: maxCookieSize,
+		},
 	}
 	cfg.MarshalAccountDefaults()
 
@@ -1395,6 +1581,10 @@ func doRequest(req *http.Request, analytics analytics.PBSAnalyticsModule, metric
 	syncersByBidder := make(map[string]usersync.Syncer)
 	for bidderName, syncerKey := range syncersBidderNameToKey {
 		syncersByBidder[bidderName] = fakeSyncer{key: syncerKey, defaultSyncType: usersync.SyncTypeIFrame}
+		if priorityGroups == nil {
+			cfg.UserSync.PriorityGroups = [][]string{{}}
+			cfg.UserSync.PriorityGroups[0] = append(cfg.UserSync.PriorityGroups[0], bidderName)
+		}
 	}
 
 	fakeAccountsFetcher := FakeAccountsFetcher{AccountData: map[string]json.RawMessage{
@@ -1513,3 +1703,17 @@ func ToHTTPCookie(cookie *usersync.Cookie) (*http.Cookie, error) {
 		Path:    "/",
 	}, nil
 }
+
+func getUIDFromHeader(setCookieHeader string) string {
+	cookies := strings.Split(setCookieHeader, ";")
+	for _, cookie := range cookies {
+		trimmedCookie := strings.TrimSpace(cookie)
+		if strings.HasPrefix(trimmedCookie, "uids=") {
+			parts := strings.SplitN(trimmedCookie, "=", 2)
+			if len(parts) == 2 {
+				return parts[1]
+			}
+		}
+	}
+	return ""
+}
diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go
index e2d476c0..65f6b795 100644
--- a/exchange/bidder_test.go
+++ b/exchange/bidder_test.go
@@ -3221,7 +3221,7 @@ func TestDoRequestImplWithTmax(t *testing.T) {
 			ctxDeadline:     time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
 			description:     "returns-tmax-timeout-error",
 			tmaxAdjustments: &TmaxAdjustmentsPreprocessed{IsEnforced: true, PBSResponsePreparationDuration: 100, BidderNetworkLatencyBuffer: 10, BidderResponseDurationMin: 5000},
-			assertFn:        func(err error) { assert.Equal(t, errTmaxTimeout, err) },
+			assertFn:        func(err error) { assert.Equal(t, &errortypes.TmaxTimeout{Message: "exceeded tmax duration"}, err) },
 		},
 		{
 			ctxDeadline:     time.Now().Add(5 * time.Second),
@@ -3296,7 +3296,7 @@ func TestDoRequestImplWithTmaxTimeout(t *testing.T) {
 			ctxDeadline:     time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC),
 			description:     "returns-tmax-timeout-error",
 			tmaxAdjustments: &TmaxAdjustmentsPreprocessed{IsEnforced: true, PBSResponsePreparationDuration: 100, BidderNetworkLatencyBuffer: 10, BidderResponseDurationMin: 5000},
-			assertFn:        func(err error) { assert.Equal(t, errTmaxTimeout, err) },
+			assertFn:        func(err error) { assert.Equal(t, &errortypes.TmaxTimeout{Message: "exceeded tmax duration"}, err) },
 		},
 	}
 	for _, test := range tests {
diff --git a/openrtb_ext/request_wrapper_test.go b/openrtb_ext/request_wrapper_test.go
index e02dd741..afa047e6 100644
--- a/openrtb_ext/request_wrapper_test.go
+++ b/openrtb_ext/request_wrapper_test.go
@@ -41,6 +41,7 @@ func TestCloneRequestWrapper(t *testing.T) {
 				appExt:    &AppExt{prebidDirty: true},
 				regExt:    &RegExt{usPrivacy: "foo"},
 				siteExt:   &SiteExt{amp: ptrutil.ToPtr[int8](1)},
+				doohExt:   &DOOHExt{},
 				sourceExt: &SourceExt{schainDirty: true},
 			},
 			reqWrapCopy: &RequestWrapper{
@@ -60,6 +61,7 @@ func TestCloneRequestWrapper(t *testing.T) {
 				appExt:    &AppExt{prebidDirty: true},
 				regExt:    &RegExt{usPrivacy: "foo"},
 				siteExt:   &SiteExt{amp: ptrutil.ToPtr[int8](1)},
+				doohExt:   &DOOHExt{},
 				sourceExt: &SourceExt{schainDirty: true},
 			},
 			mutator: func(t *testing.T, reqWrap *RequestWrapper) {},
@@ -83,6 +85,7 @@ func TestCloneRequestWrapper(t *testing.T) {
 				appExt:    &AppExt{prebidDirty: true},
 				regExt:    &RegExt{usPrivacy: "foo"},
 				siteExt:   &SiteExt{amp: ptrutil.ToPtr[int8](1)},
+				doohExt:   &DOOHExt{},
 				sourceExt: &SourceExt{schainDirty: true},
 			},
 			reqWrapCopy: &RequestWrapper{
@@ -102,6 +105,7 @@ func TestCloneRequestWrapper(t *testing.T) {
 				appExt:    &AppExt{prebidDirty: true},
 				regExt:    &RegExt{usPrivacy: "foo"},
 				siteExt:   &SiteExt{amp: ptrutil.ToPtr[int8](1)},
+				doohExt:   &DOOHExt{},
 				sourceExt: &SourceExt{schainDirty: true},
 			},
 			mutator: func(t *testing.T, reqWrap *RequestWrapper) {
@@ -115,6 +119,7 @@ func TestCloneRequestWrapper(t *testing.T) {
 				reqWrap.appExt = nil
 				reqWrap.regExt = nil
 				reqWrap.siteExt = nil
+				reqWrap.doohExt = nil
 				reqWrap.sourceExt = nil
 			},
 		},
@@ -1206,6 +1211,141 @@ func TestCloneAppExt(t *testing.T) {
 	}
 }
 
+func TestRebuildDOOHExt(t *testing.T) {
+	// These permutations look a bit wonky
+	// Since DOOHExt currently exists for consistency but there isn't a single field
+	// expected - hence unable to test dirty and variations
+	// Once one is established, updated the permutations below similar to TestRebuildAppExt example
+	testCases := []struct {
+		description           string
+		request               openrtb2.BidRequest
+		requestDOOHExtWrapper DOOHExt
+		expectedRequest       openrtb2.BidRequest
+	}{
+		{
+			description:           "Nil - Not Dirty",
+			request:               openrtb2.BidRequest{},
+			requestDOOHExtWrapper: DOOHExt{},
+			expectedRequest:       openrtb2.BidRequest{},
+		},
+		{
+			description:           "Nil - Dirty",
+			request:               openrtb2.BidRequest{},
+			requestDOOHExtWrapper: DOOHExt{},
+			expectedRequest:       openrtb2.BidRequest{DOOH: nil},
+		},
+		{
+			description:           "Nil - Dirty - No Change",
+			request:               openrtb2.BidRequest{},
+			requestDOOHExtWrapper: DOOHExt{},
+			expectedRequest:       openrtb2.BidRequest{},
+		},
+		{
+			description:           "Empty - Not Dirty",
+			request:               openrtb2.BidRequest{DOOH: &openrtb2.DOOH{}},
+			requestDOOHExtWrapper: DOOHExt{},
+			expectedRequest:       openrtb2.BidRequest{DOOH: &openrtb2.DOOH{}},
+		},
+		{
+			description:           "Empty - Dirty",
+			request:               openrtb2.BidRequest{DOOH: &openrtb2.DOOH{}},
+			requestDOOHExtWrapper: DOOHExt{},
+			expectedRequest:       openrtb2.BidRequest{DOOH: &openrtb2.DOOH{}},
+		},
+		{
+			description:           "Empty - Dirty - No Change",
+			request:               openrtb2.BidRequest{DOOH: &openrtb2.DOOH{}},
+			requestDOOHExtWrapper: DOOHExt{},
+			expectedRequest:       openrtb2.BidRequest{DOOH: &openrtb2.DOOH{}},
+		},
+		{
+			description:           "Populated - Not Dirty",
+			request:               openrtb2.BidRequest{DOOH: &openrtb2.DOOH{Ext: json.RawMessage(`{}`)}},
+			requestDOOHExtWrapper: DOOHExt{},
+			expectedRequest:       openrtb2.BidRequest{DOOH: &openrtb2.DOOH{Ext: json.RawMessage(`{}`)}},
+		},
+		{
+			description:           "Populated - Dirty",
+			request:               openrtb2.BidRequest{DOOH: &openrtb2.DOOH{Ext: json.RawMessage(`{}`)}},
+			requestDOOHExtWrapper: DOOHExt{},
+			expectedRequest:       openrtb2.BidRequest{DOOH: &openrtb2.DOOH{Ext: json.RawMessage(`{}`)}},
+		},
+		{
+			description:           "Populated - Dirty - No Change",
+			request:               openrtb2.BidRequest{DOOH: &openrtb2.DOOH{Ext: json.RawMessage(`{}`)}},
+			requestDOOHExtWrapper: DOOHExt{},
+			expectedRequest:       openrtb2.BidRequest{DOOH: &openrtb2.DOOH{Ext: json.RawMessage(`{}`)}},
+		},
+		{
+			description:           "Populated - Dirty - Cleared",
+			request:               openrtb2.BidRequest{DOOH: &openrtb2.DOOH{Ext: json.RawMessage(`{}`)}},
+			requestDOOHExtWrapper: DOOHExt{},
+			expectedRequest:       openrtb2.BidRequest{DOOH: &openrtb2.DOOH{Ext: json.RawMessage(`{}`)}},
+		},
+	}
+
+	for _, test := range testCases {
+		// create required filed in the test loop to keep test declaration easier to read
+		test.requestDOOHExtWrapper.ext = make(map[string]json.RawMessage)
+
+		w := RequestWrapper{BidRequest: &test.request, doohExt: &test.requestDOOHExtWrapper}
+		w.RebuildRequest()
+		assert.Equal(t, test.expectedRequest, *w.BidRequest, test.description)
+	}
+}
+
+func TestCloneDOOHExt(t *testing.T) {
+	testCases := []struct {
+		name        string
+		DOOHExt     *DOOHExt
+		DOOHExtCopy *DOOHExt                             // manual copy of above ext object to verify against
+		mutator     func(t *testing.T, DOOHExt *DOOHExt) // function to modify the Ext object
+	}{
+		{
+			name:        "Nil", // Verify the nil case
+			DOOHExt:     nil,
+			DOOHExtCopy: nil,
+			mutator:     func(t *testing.T, DOOHExt *DOOHExt) {},
+		},
+		{
+			name: "NoMutate",
+			DOOHExt: &DOOHExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+			},
+			DOOHExtCopy: &DOOHExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+			},
+			mutator: func(t *testing.T, DOOHExt *DOOHExt) {},
+		},
+		{
+			name: "General",
+			DOOHExt: &DOOHExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+			},
+			DOOHExtCopy: &DOOHExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+			},
+			mutator: func(t *testing.T, DOOHExt *DOOHExt) {
+				DOOHExt.ext["A"] = json.RawMessage(`"string"`)
+				DOOHExt.ext["C"] = json.RawMessage(`{}`)
+				DOOHExt.extDirty = false
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			clone := test.DOOHExt.Clone()
+			test.mutator(t, test.DOOHExt)
+			assert.Equal(t, test.DOOHExtCopy, clone)
+		})
+	}
+}
+
 func TestCloneRegExt(t *testing.T) {
 	testCases := []struct {
 		name       string
diff --git a/usersync/cookie_test.go b/usersync/cookie_test.go
index 5b86df54..34006976 100644
--- a/usersync/cookie_test.go
+++ b/usersync/cookie_test.go
@@ -380,82 +380,155 @@ func TestWriteCookieUserAgent(t *testing.T) {
 func TestPrepareCookieForWrite(t *testing.T) {
 	encoder := Base64Encoder{}
 	decoder := Base64Decoder{}
-	cookieToSend := &Cookie{
+
+	mainCookie := &Cookie{
+		uids: map[string]UIDEntry{
+			"mainUID": newTempId("1234567890123456789012345678901234567890123456", 7),
+			"2":       newTempId("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 6),
+			"3":       newTempId("123456789012345678901234567896123456789012345678", 5),
+			"4":       newTempId("aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ", 4),
+			"5":       newTempId("12345678901234567890123456789012345678901234567890", 3),
+			"6":       newTempId("abcdefghij", 2),
+			"7":       newTempId("abcdefghijklmnopqrstuvwxy", 1),
+		},
+		optOut: false,
+	}
+
+	errorCookie := &Cookie{
 		uids: map[string]UIDEntry{
-			"1": newTempId("1234567890123456789012345678901234567890123456", 7),
-			"7": newTempId("abcdefghijklmnopqrstuvwxy", 1),
-			"2": newTempId("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 6),
-			"3": newTempId("123456789012345678901234567896123456789012345678", 5),
-			"4": newTempId("aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ", 4),
-			"5": newTempId("12345678901234567890123456789012345678901234567890", 3),
-			"6": newTempId("abcdefghij", 2),
+			"syncerNotPriority": newTempId("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 7),
+			"2":                 newTempId("1234567890123456789012345678901234567890123456", 7), // Priority Element
 		},
 		optOut: false,
 	}
 
+	ejector := &PriorityBidderEjector{
+		PriorityGroups: [][]string{
+			{"mainUID"},
+			{"2", "3"},
+			{"4", "5", "6"},
+			{"7"},
+		},
+		SyncersByBidder: map[string]Syncer{
+			"mainUID": fakeSyncer{
+				key: "mainUID",
+			},
+			"2": fakeSyncer{
+				key: "2",
+			},
+			"3": fakeSyncer{
+				key: "3",
+			},
+			"4": fakeSyncer{
+				key: "4",
+			},
+			"5": fakeSyncer{
+				key: "5",
+			},
+			"6": fakeSyncer{
+				key: "6",
+			},
+			"mistmatchedBidder": fakeSyncer{
+				key: "7",
+			},
+		},
+		TieEjector: &OldestEjector{},
+	}
+
 	testCases := []struct {
 		name                     string
 		givenMaxCookieSize       int
+		givenCookieToSend        *Cookie
+		givenIsSyncerPriority    bool
 		expectedRemainingUidKeys []string
+		expectedError            error
 	}{
 		{
-			name:               "no-uids-ejected",
-			givenMaxCookieSize: 2000,
+			name:                  "no-uids-ejected",
+			givenMaxCookieSize:    2000,
+			givenCookieToSend:     mainCookie,
+			givenIsSyncerPriority: true,
 			expectedRemainingUidKeys: []string{
-				"1", "2", "3", "4", "5", "6", "7",
+				"mainUID", "2", "3", "4", "5", "6", "7",
 			},
 		},
 		{
-			name:               "no-uids-ejected-2",
-			givenMaxCookieSize: 0,
+			name:               "invalid-max-size",
+			givenMaxCookieSize: -100,
+			givenCookieToSend:  mainCookie,
 			expectedRemainingUidKeys: []string{
-				"1", "2", "3", "4", "5", "6", "7",
+				"mainUID", "2", "3", "4", "5", "6", "7",
 			},
 		},
 		{
-			name:               "one-uid-ejected",
-			givenMaxCookieSize: 900,
+			name:                  "syncer-is-not-priority",
+			givenMaxCookieSize:    100,
+			givenCookieToSend:     errorCookie,
+			givenIsSyncerPriority: false,
+			expectedError:         errors.New("syncer key is not a priority, and there are only priority elements left"),
+		},
+		{
+			name:                  "no-uids-ejected-2",
+			givenMaxCookieSize:    0,
+			givenCookieToSend:     mainCookie,
+			givenIsSyncerPriority: true,
 			expectedRemainingUidKeys: []string{
-				"1", "2", "3", "4", "5", "6",
+				"mainUID", "2", "3", "4", "5", "6", "7",
 			},
 		},
 		{
-			name:               "four-uids-ejected",
-			givenMaxCookieSize: 500,
+			name:                  "one-uid-ejected",
+			givenMaxCookieSize:    900,
+			givenCookieToSend:     mainCookie,
+			givenIsSyncerPriority: true,
 			expectedRemainingUidKeys: []string{
-				"1", "2", "3",
+				"mainUID", "2", "3", "4", "5", "6",
 			},
 		},
 		{
-			name:               "all-but-one-uids-ejected",
-			givenMaxCookieSize: 300,
+			name:                  "four-uids-ejected",
+			givenMaxCookieSize:    500,
+			givenCookieToSend:     mainCookie,
+			givenIsSyncerPriority: true,
 			expectedRemainingUidKeys: []string{
-				"1",
+				"mainUID", "2", "3",
 			},
 		},
 		{
-			name:                     "all-uids-ejected",
-			givenMaxCookieSize:       100,
-			expectedRemainingUidKeys: []string{},
+			name:                  "all-but-one-uids-ejected",
+			givenMaxCookieSize:    300,
+			givenCookieToSend:     mainCookie,
+			givenIsSyncerPriority: true,
+			expectedRemainingUidKeys: []string{
+				"mainUID",
+			},
 		},
 		{
-			name:                     "invalid-max-size",
-			givenMaxCookieSize:       -100,
+			name:                     "only-main-uid-left",
+			givenMaxCookieSize:       100,
+			givenCookieToSend:        mainCookie,
+			expectedError:            errors.New("uid that's trying to be synced is bigger than MaxCookieSize"),
 			expectedRemainingUidKeys: []string{},
 		},
 	}
 
 	for _, test := range testCases {
 		t.Run(test.name, func(t *testing.T) {
-			encodedCookie, err := cookieToSend.PrepareCookieForWrite(&config.HostCookie{MaxCookieSizeBytes: test.givenMaxCookieSize}, encoder)
-			assert.NoError(t, err)
-			decodedCookie := decoder.Decode(encodedCookie)
+			ejector.IsSyncerPriority = test.givenIsSyncerPriority
+			encodedCookie, err := test.givenCookieToSend.PrepareCookieForWrite(&config.HostCookie{MaxCookieSizeBytes: test.givenMaxCookieSize}, encoder, ejector)
+
+			if test.expectedError != nil {
+				assert.Equal(t, test.expectedError, err)
+			} else {
+				assert.NoError(t, err)
+				decodedCookie := decoder.Decode(encodedCookie)
 
-			for _, key := range test.expectedRemainingUidKeys {
-				_, ok := decodedCookie.uids[key]
-				assert.Equal(t, true, ok)
+				for _, key := range test.expectedRemainingUidKeys {
+					_, ok := decodedCookie.uids[key]
+					assert.Equal(t, true, ok)
+				}
+				assert.Equal(t, len(decodedCookie.uids), len(test.expectedRemainingUidKeys))
 			}
-			assert.Equal(t, len(decodedCookie.uids), len(test.expectedRemainingUidKeys))
 		})
 	}
 }
diff --git a/usersync/ejector_test.go b/usersync/ejector_test.go
new file mode 100644
index 00000000..d13e7484
--- /dev/null
+++ b/usersync/ejector_test.go
@@ -0,0 +1,444 @@
+package usersync
+
+import (
+	"errors"
+	"reflect"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestPriorityEjector(t *testing.T) {
+	testCases := []struct {
+		name          string
+		givenUids     map[string]UIDEntry
+		givenEjector  Ejector
+		expected      string
+		expectedError error
+	}{
+		{
+			name: "one-lowest-priority-element",
+			givenUids: map[string]UIDEntry{
+				"highestPrioritySyncer": {
+					UID:     "123",
+					Expires: time.Now().Add((90 * 24 * time.Hour)),
+				},
+				"lowestPriority": {
+					UID:     "456",
+					Expires: time.Now(),
+				},
+			},
+			givenEjector: &PriorityBidderEjector{
+				PriorityGroups: [][]string{
+					{"highestPriorityBidder"},
+					{"lowestPriority"},
+				},
+				SyncersByBidder: map[string]Syncer{
+					"highestPriorityBidder": fakeSyncer{
+						key: "highestPrioritySyncer",
+					},
+					"lowestPriority": fakeSyncer{
+						key: "lowestPriority",
+					},
+				},
+				IsSyncerPriority: true,
+			},
+			expected: "lowestPriority",
+		},
+		{
+			name: "multiple-uids-same-priority",
+			givenUids: map[string]UIDEntry{
+				"newerButSamePriority": {
+					UID:     "123",
+					Expires: time.Now().Add((90 * 24 * time.Hour)),
+				},
+				"olderButSamePriority": {
+					UID:     "456",
+					Expires: time.Now(),
+				},
+			},
+			givenEjector: &PriorityBidderEjector{
+				PriorityGroups: [][]string{
+					{"newerButSamePriority", "olderButSamePriority"},
+				},
+				SyncersByBidder: map[string]Syncer{
+					"newerButSamePriority": fakeSyncer{
+						key: "newerButSamePriority",
+					},
+					"olderButSamePriority": fakeSyncer{
+						key: "olderButSamePriority",
+					},
+				},
+				IsSyncerPriority: true,
+				TieEjector:       &OldestEjector{},
+			},
+			expected: "olderButSamePriority",
+		},
+		{
+			name: "non-priority-uids-present",
+			givenUids: map[string]UIDEntry{
+				"higherPriority": {
+					UID:     "123",
+					Expires: time.Now().Add((90 * 24 * time.Hour)),
+				},
+				"lowestPriority": {
+					UID:     "456",
+					Expires: time.Now(),
+				},
+				"oldestNonPriority": {
+					UID:     "456",
+					Expires: time.Now(),
+				},
+				"newestNonPriority": {
+					UID:     "123",
+					Expires: time.Now().Add((90 * 24 * time.Hour)),
+				},
+			},
+			givenEjector: &PriorityBidderEjector{
+				PriorityGroups: [][]string{
+					{"higherPriority"},
+					{"lowestPriority"},
+				},
+				SyncersByBidder: map[string]Syncer{
+					"higherPriority": fakeSyncer{
+						key: "higherPriority",
+					},
+					"lowestPriority": fakeSyncer{
+						key: "lowestPriority",
+					},
+					"oldestNonPriority": fakeSyncer{
+						key: "oldestNonPriority",
+					},
+					"newestNonPriority": fakeSyncer{
+						key: "newestNonPriority",
+					},
+				},
+				IsSyncerPriority: true,
+				TieEjector:       &OldestEjector{},
+			},
+			expected: "oldestNonPriority",
+		},
+		{
+			name: "empty-priority-groups",
+			givenUids: map[string]UIDEntry{
+				"oldestNonPriority": {
+					UID:     "456",
+					Expires: time.Now(),
+				},
+				"newestNonPriority": {
+					UID:     "123",
+					Expires: time.Now().Add((90 * 24 * time.Hour)),
+				},
+			},
+			givenEjector: &PriorityBidderEjector{
+				SyncersByBidder: map[string]Syncer{
+					"oldestNonPriority": fakeSyncer{
+						key: "oldestNonPriority",
+					},
+					"newestNonPriority": fakeSyncer{
+						key: "newestNonPriority",
+					},
+				},
+				IsSyncerPriority: false,
+				TieEjector:       &OldestEjector{},
+			},
+			expected: "oldestNonPriority",
+		},
+		{
+			name: "one-priority-element",
+			givenUids: map[string]UIDEntry{
+				"onlyPriorityElement": {
+					UID:     "123",
+					Expires: time.Now().Add((90 * 24 * time.Hour)),
+				},
+			},
+			givenEjector: &PriorityBidderEjector{
+				PriorityGroups: [][]string{
+					{"onlyPriorityElement"},
+				},
+				SyncersByBidder: map[string]Syncer{
+					"onlyPriorityElement": fakeSyncer{
+						key: "onlyPriorityElement",
+					},
+				},
+				IsSyncerPriority: true,
+			},
+			expected: "onlyPriorityElement",
+		},
+		{
+			name: "syncer-is-not-priority",
+			givenUids: map[string]UIDEntry{
+				"onlyPriorityElement": {
+					UID:     "123",
+					Expires: time.Now().Add((90 * 24 * time.Hour)),
+				},
+				"syncer": {
+					UID:     "456",
+					Expires: time.Now().Add((90 * 24 * time.Hour)),
+				},
+			},
+			givenEjector: &PriorityBidderEjector{
+				PriorityGroups: [][]string{
+					{"onlyPriorityElement"},
+				},
+				SyncersByBidder: map[string]Syncer{
+					"onlyPriorityElement": fakeSyncer{
+						key: "onlyPriorityElement",
+					},
+				},
+				IsSyncerPriority: false,
+			},
+			expectedError: errors.New("syncer key is not a priority, and there are only priority elements left"),
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			uidToDelete, err := test.givenEjector.Choose(test.givenUids)
+			if test.expectedError != nil {
+				assert.Equal(t, test.expectedError, err)
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, test.expected, uidToDelete)
+			}
+		})
+	}
+}
+
+func TestOldestEjector(t *testing.T) {
+	testCases := []struct {
+		name      string
+		givenUids map[string]UIDEntry
+		expected  string
+	}{
+		{
+			name: "multiple-elements",
+			givenUids: map[string]UIDEntry{
+				"newestElement": {
+					UID:     "123",
+					Expires: time.Now().Add((90 * 24 * time.Hour)),
+				},
+				"oldestElement": {
+					UID:     "456",
+					Expires: time.Now(),
+				},
+			},
+			expected: "oldestElement",
+		},
+		{
+			name: "one-element",
+			givenUids: map[string]UIDEntry{
+				"onlyElement": {
+					UID:     "123",
+					Expires: time.Now().Add((90 * 24 * time.Hour)),
+				},
+			},
+			expected: "onlyElement",
+		},
+		{
+			name:      "no-elements",
+			givenUids: map[string]UIDEntry{},
+			expected:  "",
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			ejector := OldestEjector{}
+			oldestElement, err := ejector.Choose(test.givenUids)
+			assert.NoError(t, err)
+			assert.Equal(t, test.expected, oldestElement)
+		})
+	}
+}
+
+func TestGetNonPriorityUids(t *testing.T) {
+	syncersByBidder := map[string]Syncer{
+		"syncerKey1": fakeSyncer{
+			key: "syncerKey1",
+		},
+		"syncerKey2": fakeSyncer{
+			key: "syncerKey2",
+		},
+		"syncerKey3": fakeSyncer{
+			key: "syncerKey3",
+		},
+	}
+
+	testCases := []struct {
+		name                string
+		givenUids           map[string]UIDEntry
+		givenPriorityGroups [][]string
+		expected            map[string]UIDEntry
+	}{
+		{
+			name: "one-priority-group",
+			givenUids: map[string]UIDEntry{
+				"syncerKey1": {
+					UID: "123",
+				},
+				"syncerKey2": {
+					UID: "456",
+				},
+				"syncerKey3": {
+					UID: "789",
+				},
+			},
+			givenPriorityGroups: [][]string{
+				{"syncerKey1"},
+			},
+			expected: map[string]UIDEntry{
+				"syncerKey2": {
+					UID: "456",
+				},
+				"syncerKey3": {
+					UID: "789",
+				},
+			},
+		},
+		{
+			name: "multiple-priority-groups",
+			givenUids: map[string]UIDEntry{
+				"syncerKey1": {
+					UID: "123",
+				},
+				"syncerKey2": {
+					UID: "456",
+				},
+				"syncerKey3": {
+					UID: "789",
+				},
+			},
+			givenPriorityGroups: [][]string{
+				{"syncerKey1"},
+				{"syncerKey2"},
+			},
+			expected: map[string]UIDEntry{
+				"syncerKey3": {
+					UID: "789",
+				},
+			},
+		},
+		{
+			name: "no-priority-groups",
+			givenUids: map[string]UIDEntry{
+				"syncerKey1": {
+					UID: "123",
+				},
+				"syncerKey2": {
+					UID: "456",
+				},
+				"syncerKey3": {
+					UID: "789",
+				},
+			},
+			expected: map[string]UIDEntry{
+				"syncerKey1": {
+					UID: "123",
+				},
+				"syncerKey2": {
+					UID: "456",
+				},
+				"syncerKey3": {
+					UID: "789",
+				},
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			uids := getNonPriorityUids(test.givenUids, test.givenPriorityGroups, syncersByBidder)
+			assert.Equal(t, true, reflect.DeepEqual(test.expected, uids))
+		})
+	}
+}
+
+func TestGetPriorityUids(t *testing.T) {
+	syncersByBidder := map[string]Syncer{
+		"syncerKey1": fakeSyncer{
+			key: "syncerKey1",
+		},
+		"syncerKey2": fakeSyncer{
+			key: "syncerKey2",
+		},
+		"syncerKey3": fakeSyncer{
+			key: "syncerKey3",
+		},
+	}
+
+	testCases := []struct {
+		name                     string
+		givenUids                map[string]UIDEntry
+		givenLowestPriorityGroup []string
+		expected                 map[string]UIDEntry
+	}{
+		{
+			name: "one-priority-element",
+			givenUids: map[string]UIDEntry{
+				"syncerKey1": {
+					UID: "123",
+				},
+				"syncerKey2": {
+					UID: "456",
+				},
+				"syncerKey3": {
+					UID: "789",
+				},
+			},
+			givenLowestPriorityGroup: []string{"syncerKey1"},
+			expected: map[string]UIDEntry{
+				"syncerKey1": {
+					UID: "123",
+				},
+			},
+		},
+		{
+			name: "multiple-priority-elements",
+			givenUids: map[string]UIDEntry{
+				"syncerKey1": {
+					UID: "123",
+				},
+				"syncerKey2": {
+					UID: "456",
+				},
+				"syncerKey3": {
+					UID: "789",
+				},
+			},
+			givenLowestPriorityGroup: []string{"syncerKey1", "syncerKey2"},
+			expected: map[string]UIDEntry{
+				"syncerKey1": {
+					UID: "123",
+				},
+				"syncerKey2": {
+					UID: "456",
+				},
+			},
+		},
+		{
+			name: "no-priority-elements",
+			givenUids: map[string]UIDEntry{
+				"syncerKey1": {
+					UID: "123",
+				},
+				"syncerKey2": {
+					UID: "456",
+				},
+				"syncerKey3": {
+					UID: "789",
+				},
+			},
+			givenLowestPriorityGroup: []string{},
+			expected:                 map[string]UIDEntry{},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			uids := getPriorityUids(test.givenLowestPriorityGroup, test.givenUids, syncersByBidder)
+			assert.Equal(t, true, reflect.DeepEqual(test.expected, uids))
+		})
+	}
+}