Repository: prebid/prebid-server
Pull request #1183 (2020-02-03 23:12:47)

Adding support for deal prefixes
Opening PR to get an early start on review comments. We have a couple questions remaining on our end. One major change that may need to be added is implementing the `pmp` object for targeting deals in the impressions. I will update this PR with any action items regarding this.

camrice: Relevant issue for this PR with general documentation: #1174 
camrice: Update to my initial comment:

The `pmp` object is not required for supporting deals, so this change is ready for review. There aren't any other major additions needed at this time.

Patch:
diff --git a/adapters/adoppler/adoppler.go b/adapters/adoppler/adoppler.go
new file mode 100644
index 00000000..c604bfea
--- /dev/null
+++ b/adapters/adoppler/adoppler.go
@@ -0,0 +1,210 @@
+package adoppler
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+	"net/url"
+
+	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+var bidHeaders http.Header = map[string][]string{
+	"Accept":            {"application/json"},
+	"Content-Type":      {"application/json;charset=utf-8"},
+	"X-OpenRTB-Version": {"2.5"},
+}
+
+type adsVideoExt struct {
+	Duration int `json:"duration"`
+}
+
+type adsImpExt struct {
+	Video *adsVideoExt `json:"video"`
+}
+
+type AdopplerAdapter struct {
+	endpoint string
+}
+
+func NewAdopplerBidder(endpoint string) *AdopplerAdapter {
+	return &AdopplerAdapter{endpoint}
+}
+
+func (ads *AdopplerAdapter) MakeRequests(
+	req *openrtb.BidRequest,
+	info *adapters.ExtraRequestInfo,
+) (
+	[]*adapters.RequestData,
+	[]error,
+) {
+	if len(req.Imp) == 0 {
+		return nil, nil
+	}
+
+	var datas []*adapters.RequestData
+	var errs []error
+	for _, imp := range req.Imp {
+		ext, err := unmarshalExt(imp.Ext)
+		if err != nil {
+			errs = append(errs, &errortypes.BadInput{err.Error()})
+			continue
+		}
+
+		var r openrtb.BidRequest = *req
+		r.ID = req.ID + "-" + ext.AdUnit
+		r.Imp = []openrtb.Imp{imp}
+
+		body, err := json.Marshal(r)
+		if err != nil {
+			errs = append(errs, err)
+			continue
+		}
+
+		uri := fmt.Sprintf("%s/processHeaderBid/%s",
+			ads.endpoint, url.PathEscape(ext.AdUnit))
+		data := &adapters.RequestData{
+			Method:  "POST",
+			Uri:     uri,
+			Body:    body,
+			Headers: bidHeaders,
+		}
+		datas = append(datas, data)
+	}
+
+	return datas, errs
+}
+
+func (ads *AdopplerAdapter) MakeBids(
+	intReq *openrtb.BidRequest,
+	extReq *adapters.RequestData,
+	resp *adapters.ResponseData,
+) (
+	*adapters.BidderResponse,
+	[]error,
+) {
+	if resp.StatusCode == http.StatusNoContent {
+		return nil, nil
+	}
+	if resp.StatusCode == http.StatusBadRequest {
+		return nil, []error{&errortypes.BadInput{"bad request"}}
+	}
+	if resp.StatusCode != http.StatusOK {
+		err := &errortypes.BadServerResponse{
+			fmt.Sprintf("unexpected status: %d", resp.StatusCode),
+		}
+		return nil, []error{err}
+	}
+
+	var bidResp openrtb.BidResponse
+	err := json.Unmarshal(resp.Body, &bidResp)
+	if err != nil {
+		err := &errortypes.BadServerResponse{
+			fmt.Sprintf("invalid body: %s", err.Error()),
+		}
+		return nil, []error{err}
+	}
+
+	impTypes := make(map[string]openrtb_ext.BidType)
+	for _, imp := range intReq.Imp {
+		if _, ok := impTypes[imp.ID]; ok {
+			return nil, []error{&errortypes.BadInput{
+				fmt.Sprintf("duplicate $.imp.id %s", imp.ID),
+			}}
+		}
+		if imp.Banner != nil {
+			impTypes[imp.ID] = openrtb_ext.BidTypeBanner
+		} else if imp.Video != nil {
+			impTypes[imp.ID] = openrtb_ext.BidTypeVideo
+		} else if imp.Audio != nil {
+			impTypes[imp.ID] = openrtb_ext.BidTypeAudio
+		} else if imp.Native != nil {
+			impTypes[imp.ID] = openrtb_ext.BidTypeNative
+		} else {
+			return nil, []error{&errortypes.BadInput{
+				"one of $.imp.banner, $.imp.video, $.imp.audio and $.imp.native field required",
+			}}
+		}
+	}
+
+	var bids []*adapters.TypedBid
+	for _, seatBid := range bidResp.SeatBid {
+		for _, bid := range seatBid.Bid {
+			tp, ok := impTypes[bid.ImpID]
+			if !ok {
+				err := &errortypes.BadServerResponse{
+					fmt.Sprintf("unknown impid: %s", bid.ImpID),
+				}
+				return nil, []error{err}
+			}
+
+			var bidVideo *openrtb_ext.ExtBidPrebidVideo
+			if tp == openrtb_ext.BidTypeVideo {
+				adsExt, err := unmarshalAdsExt(bid.Ext)
+				if err != nil {
+					return nil, []error{&errortypes.BadServerResponse{err.Error()}}
+				}
+				if adsExt == nil || adsExt.Video == nil {
+					return nil, []error{&errortypes.BadServerResponse{
+						"$.seatbid.bid.ext.ads.video required",
+					}}
+				}
+				bidVideo = &openrtb_ext.ExtBidPrebidVideo{
+					Duration:        adsExt.Video.Duration,
+					PrimaryCategory: head(bid.Cat),
+				}
+			}
+			bids = append(bids, &adapters.TypedBid{
+				Bid:      &bid,
+				BidType:  tp,
+				BidVideo: bidVideo,
+			})
+		}
+	}
+
+	adsResp := adapters.NewBidderResponseWithBidsCapacity(len(bids))
+	adsResp.Bids = bids
+
+	return adsResp, nil
+}
+
+func unmarshalExt(ext json.RawMessage) (*openrtb_ext.ExtImpAdoppler, error) {
+	var bext adapters.ExtImpBidder
+	err := json.Unmarshal(ext, &bext)
+	if err != nil {
+		return nil, err
+	}
+
+	var adsExt openrtb_ext.ExtImpAdoppler
+	err = json.Unmarshal(bext.Bidder, &adsExt)
+	if err != nil {
+		return nil, err
+	}
+
+	if adsExt.AdUnit == "" {
+		return nil, errors.New("$.imp.ext.adoppler.adunit required")
+	}
+
+	return &adsExt, nil
+}
+
+func unmarshalAdsExt(ext json.RawMessage) (*adsImpExt, error) {
+	var e struct {
+		Ads *adsImpExt `json:"ads"`
+	}
+	err := json.Unmarshal(ext, &e)
+
+	return e.Ads, err
+}
+
+func head(s []string) string {
+	if len(s) == 0 {
+		return ""
+	}
+
+	return s[0]
+}
diff --git a/adapters/adpone/adpone.go b/adapters/adpone/adpone.go
index 345a4988..b1822a0a 100644
--- a/adapters/adpone/adpone.go
+++ b/adapters/adpone/adpone.go
@@ -3,9 +3,10 @@ package adpone
 import (
 	"encoding/json"
 	"fmt"
-	"github.com/prebid/prebid-server/openrtb_ext"
 	"net/http"
 
+	"github.com/prebid/prebid-server/openrtb_ext"
+
 	"github.com/mxmCherry/openrtb"
 	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/errortypes"
diff --git a/adapters/appnexus/appnexus.go b/adapters/appnexus/appnexus.go
index 3986bfd4..9bec9bf1 100644
--- a/adapters/appnexus/appnexus.go
+++ b/adapters/appnexus/appnexus.go
@@ -87,6 +87,7 @@ type appnexusBidExtAppnexus struct {
 	BrandId       int                    `json:"brand_id"`
 	BrandCategory int                    `json:"brand_category_id"`
 	CreativeInfo  appnexusBidExtCreative `json:"creative_info"`
+	DealPriority  int                    `json:"deal_priority"`
 }
 
 type appnexusBidExt struct {
@@ -543,9 +544,10 @@ func (a *AppNexusAdapter) MakeBids(internalRequest *openrtb.BidRequest, external
 					}
 
 					bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
-						Bid:      &bid,
-						BidType:  bidType,
-						BidVideo: impVideo,
+						Bid:          &bid,
+						BidType:      bidType,
+						BidVideo:     impVideo,
+						DealPriority: bidExt.Appnexus.DealPriority,
 					})
 				} else {
 					errs = append(errs, err)
diff --git a/adapters/audienceNetwork/facebook.go b/adapters/audienceNetwork/facebook.go
index 706673cb..3ece7bb9 100644
--- a/adapters/audienceNetwork/facebook.go
+++ b/adapters/audienceNetwork/facebook.go
@@ -447,3 +447,20 @@ func NewFacebookBidder(client *http.Client, platformID string, appSecret string)
 		appSecret:    appSecret,
 	}
 }
+
+func (fa *FacebookAdapter) MakeTimeoutNotification(req *adapters.RequestData) (*adapters.RequestData, []error) {
+	// Note, facebook creates one request per imp, so all these requests will only have one imp in them
+	auction_id, err := jsonparser.GetString(req.Body, "imp", "[0]", "id")
+	if err != nil {
+		return &adapters.RequestData{}, []error{err}
+	}
+
+	uri := fmt.Sprintf("https://www.facebook.com/audiencenetwork/nurl/?partner=%s&app=%s&auction=%s&ortb_loss_code=2", fa.platformID, fa.platformID, auction_id)
+	timeoutReq := adapters.RequestData{
+		Method:  "GET",
+		Uri:     uri,
+		Body:    nil,
+		Headers: http.Header{},
+	}
+	return &timeoutReq, nil
+}
diff --git a/adapters/bidder.go b/adapters/bidder.go
index 9d3ffb75..627caf67 100644
--- a/adapters/bidder.go
+++ b/adapters/bidder.go
@@ -39,6 +39,19 @@ type Bidder interface {
 	MakeBids(internalRequest *openrtb.BidRequest, externalRequest *RequestData, response *ResponseData) (*BidderResponse, []error)
 }
 
+// TimeoutBidder is used to identify bidders that support timeout notifications.
+type TimeoutBidder interface {
+	Bidder
+
+	// MakeTimeoutNotice functions much the same as MakeRequests, except it is fed the bidder request that timed out,
+	// and expects that only one notification "request" will be generated. A use case for multiple timeout notifications
+	// has not been anticipated.
+	//
+	// Do note that if MakeRequests returns multiple requests, and more than one of these times out, MakeTimeoutNotice will be called
+	// once for each timed out request.
+	MakeTimeoutNotification(req *RequestData) (*RequestData, []error)
+}
+
 type MisconfiguredBidder struct {
 	Name  string
 	Error error
@@ -95,10 +108,12 @@ func NewBidderResponse() *BidderResponse {
 // TypedBid.Bid.Ext will become "response.seatbid[i].bid.ext.bidder" in the final OpenRTB response.
 // TypedBid.BidType will become "response.seatbid[i].bid.ext.prebid.type" in the final OpenRTB response.
 // TypedBid.BidVideo will become "response.seatbid[i].bid.ext.prebid.video" in the final OpenRTB response.
+// TypedBid.DealPriority will become "response.seatbid[i].bid.dealPriority" in the final OpenRTB response.
 type TypedBid struct {
-	Bid      *openrtb.Bid
-	BidType  openrtb_ext.BidType
-	BidVideo *openrtb_ext.ExtBidPrebidVideo
+	Bid          *openrtb.Bid
+	BidType      openrtb_ext.BidType
+	BidVideo     *openrtb_ext.ExtBidPrebidVideo
+	DealPriority int
 }
 
 // RequestData and ResponseData exist so that prebid-server core code can implement its "debug" functionality
diff --git a/config/config.go b/config/config.go
index 943d18a9..52686422 100644
--- a/config/config.go
+++ b/config/config.go
@@ -673,6 +673,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.adform.endpoint", "http://adx.adform.net/adx")
 	v.SetDefault("adapters.adkernel.endpoint", "http://{{.Host}}/hb?zone={{.ZoneID}}")
 	v.SetDefault("adapters.adkerneladn.endpoint", "http://{{.Host}}/rtbpub?account={{.PublisherID}}")
+	v.SetDefault("adapters.adoppler.endpoint", "http://app.trustedmarketplace.io/ads")
 	v.SetDefault("adapters.adpone.endpoint", "http://rtb.adpone.com/bid-request?src=prebid_server")
 	v.SetDefault("adapters.adtelligent.endpoint", "http://hb.adtelligent.com/auction")
 	v.SetDefault("adapters.advangelists.endpoint", "http://nep.advangelists.com/xp/get?pubid={{.PublisherID}}")
diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go
index b8b21b76..7c9651af 100644
--- a/endpoints/openrtb2/video_auction.go
+++ b/endpoints/openrtb2/video_auction.go
@@ -550,8 +550,9 @@ func createBidExtension(videoRequest *openrtb_ext.BidRequestVideo) ([]byte, erro
 	}
 
 	prebid := openrtb_ext.ExtRequestPrebid{
-		Cache:     &cache,
-		Targeting: &targeting,
+		Cache:        &cache,
+		Targeting:    &targeting,
+		SupportDeals: videoRequest.SupportDeals,
 	}
 	extReq := openrtb_ext.ExtRequest{Prebid: prebid}
 
diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go
index 95f5b7f5..d169c120 100644
--- a/exchange/adapter_map.go
+++ b/exchange/adapter_map.go
@@ -12,6 +12,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/adform"
 	"github.com/prebid/prebid-server/adapters/adkernel"
 	"github.com/prebid/prebid-server/adapters/adkernelAdn"
+	"github.com/prebid/prebid-server/adapters/adoppler"
 	"github.com/prebid/prebid-server/adapters/adpone"
 	"github.com/prebid/prebid-server/adapters/adtelligent"
 	"github.com/prebid/prebid-server/adapters/advangelists"
@@ -71,6 +72,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter
 		openrtb_ext.BidderAdform:       adform.NewAdformBidder(client, cfg.Adapters[string(openrtb_ext.BidderAdform)].Endpoint),
 		openrtb_ext.BidderAdkernel:     adkernel.NewAdkernelAdapter(cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderAdkernel))].Endpoint),
 		openrtb_ext.BidderAdkernelAdn:  adkernelAdn.NewAdkernelAdnAdapter(cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderAdkernelAdn))].Endpoint),
+		openrtb_ext.BidderAdoppler:     adoppler.NewAdopplerBidder(cfg.Adapters[string(openrtb_ext.BidderAdoppler)].Endpoint),
 		openrtb_ext.BidderAdpone:       adpone.NewAdponeBidder(cfg.Adapters[string(openrtb_ext.BidderAdpone)].Endpoint),
 		openrtb_ext.BidderAdtelligent:  adtelligent.NewAdtelligentBidder(cfg.Adapters[string(openrtb_ext.BidderAdtelligent)].Endpoint),
 		openrtb_ext.BidderAdvangelists: advangelists.NewAdvangelistsBidder(cfg.Adapters[string(openrtb_ext.BidderAdvangelists)].Endpoint),
diff --git a/exchange/bidder.go b/exchange/bidder.go
index 57086600..97f64e74 100644
--- a/exchange/bidder.go
+++ b/exchange/bidder.go
@@ -8,6 +8,7 @@ import (
 	"fmt"
 	"io/ioutil"
 	"net/http"
+	"time"
 
 	"github.com/mxmCherry/openrtb"
 	nativeRequests "github.com/mxmCherry/openrtb/native/request"
@@ -50,11 +51,13 @@ type adaptedBidder interface {
 // pbsOrtbBid.bidType will become "response.seatbid[i].bid.ext.prebid.type" in the final OpenRTB response.
 // pbsOrtbBid.bidTargets does not need to be filled out by the Bidder. It will be set later by the exchange.
 // pbsOrtbBid.bidVideo is optional but should be filled out by the Bidder if bidType is video.
+// pbsOrtbBid.dealPriority will become "response.seatbid[i].bid.dealPriority" in the final OpenRTB response.
 type pbsOrtbBid struct {
-	bid        *openrtb.Bid
-	bidType    openrtb_ext.BidType
-	bidTargets map[string]string
-	bidVideo   *openrtb_ext.ExtBidPrebidVideo
+	bid          *openrtb.Bid
+	bidType      openrtb_ext.BidType
+	bidTargets   map[string]string
+	bidVideo     *openrtb_ext.ExtBidPrebidVideo
+	dealPriority int
 }
 
 // pbsOrtbSeatBid is a SeatBid returned by an adaptedBidder.
@@ -182,9 +185,10 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, request *openrtb.Bi
 							bidResponse.Bids[i].Bid.Price = bidResponse.Bids[i].Bid.Price * bidAdjustment * conversionRate
 						}
 						seatBid.bids = append(seatBid.bids, &pbsOrtbBid{
-							bid:      bidResponse.Bids[i].Bid,
-							bidType:  bidResponse.Bids[i].BidType,
-							bidVideo: bidResponse.Bids[i].BidVideo,
+							bid:          bidResponse.Bids[i].Bid,
+							bidType:      bidResponse.Bids[i].BidType,
+							bidVideo:     bidResponse.Bids[i].BidVideo,
+							dealPriority: bidResponse.Bids[i].DealPriority,
 						})
 					}
 				} else {
@@ -295,6 +299,14 @@ func (bidder *bidderAdapter) doRequest(ctx context.Context, req *adapters.Reques
 	if err != nil {
 		if err == context.DeadlineExceeded {
 			err = &errortypes.Timeout{Message: err.Error()}
+			if tb, ok := bidder.Bidder.(adapters.TimeoutBidder); ok {
+				// Toss the timeout notification call into a go routine, as we are out of time'
+				// and cannot delay processing. We don't do anything result, as there is not much
+				// we can do about a timeout notification failure. We do not want to get stuck in
+				// a loop of trying to report timeouts to the timeout notifications.
+				go bidder.doTimeoutNotification(tb, req)
+			}
+
 		}
 		return &httpCallInfo{
 			request: req,
@@ -328,6 +340,21 @@ func (bidder *bidderAdapter) doRequest(ctx context.Context, req *adapters.Reques
 	}
 }
 
+func (bidder *bidderAdapter) doTimeoutNotification(timeoutBidder adapters.TimeoutBidder, req *adapters.RequestData) {
+	ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
+	defer cancel()
+	toReq, errL := timeoutBidder.MakeTimeoutNotification(req)
+	if toReq != nil && len(errL) == 0 {
+		httpReq, err := http.NewRequest(toReq.Method, toReq.Uri, bytes.NewBuffer(toReq.Body))
+		if err == nil {
+			httpReq.Header = req.Headers
+			ctxhttp.Do(ctx, bidder.Client, httpReq)
+			// No validation yet on sending notifications
+		}
+	}
+
+}
+
 type httpCallInfo struct {
 	request  *adapters.RequestData
 	response *adapters.ResponseData
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 8c6cac4c..ef10180a 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -10,6 +10,7 @@ import (
 	"net/http"
 	"runtime/debug"
 	"sort"
+	"strings"
 	"time"
 
 	"github.com/prebid/prebid-server/stored_requests"
@@ -167,12 +168,93 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque
 			}
 			targData.setTargeting(auc, bidRequest.App != nil, bidCategory)
 		}
+
+		if requestExt.Prebid.SupportDeals {
+			dealErrs := applyDealSupport(bidRequest, auc)
+			errs = append(errs, dealErrs...)
+		}
 	}
 
 	// Build the response
 	return e.buildBidResponse(ctx, liveAdapters, adapterBids, bidRequest, resolvedRequest, adapterExtra, auc, errs)
 }
 
+type DealTierInfo struct {
+	Prefix      string `json:"prefix"`
+	MinDealTier int    `json:"minDealTier"`
+}
+
+type DealTier struct {
+	Info *DealTierInfo `json:"dealTier,omitempty"`
+}
+
+type BidderDealTier struct {
+	DealInfo map[string]*DealTier
+}
+
+// applyDealSupport updates targeting keys with deal prefixes if minimum deal tier exceeded
+func applyDealSupport(bidRequest *openrtb.BidRequest, auc *auction) []error {
+	errs := []error{}
+	impDealMap := getDealTiers(bidRequest)
+
+	for impID, topBidsPerImp := range auc.winningBidsByBidder {
+		impDeal := impDealMap[impID].DealInfo
+		for bidder, topBidPerBidder := range topBidsPerImp {
+			bidderString := bidder.String()
+
+			if topBidPerBidder.dealPriority > 0 {
+				if validateAndNormalizeDealTier(impDeal[bidderString]) {
+					updateHbPbCatDur(topBidPerBidder, impDeal[bidderString].Info)
+				} else {
+					errs = append(errs, fmt.Errorf("dealTier configuration invalid for bidder '%s', imp ID '%s'", bidderString, impID))
+				}
+			}
+		}
+	}
+
+	return errs
+}
+
+// getDealTiers creates map of impression to bidder deal tier configuration
+func getDealTiers(bidRequest *openrtb.BidRequest) map[string]*BidderDealTier {
+	impDealMap := make(map[string]*BidderDealTier)
+
+	for _, imp := range bidRequest.Imp {
+		var bidderDealTier BidderDealTier
+		err := json.Unmarshal(imp.Ext, &bidderDealTier.DealInfo)
+		if err != nil {
+			continue
+		}
+
+		impDealMap[imp.ID] = &bidderDealTier
+	}
+
+	return impDealMap
+}
+
+func validateAndNormalizeDealTier(impDeal *DealTier) bool {
+	if impDeal == nil || impDeal.Info == nil {
+		return false
+	}
+	// Remove whitespace from prefix before checking if it can be used
+	impDeal.Info.Prefix = strings.ReplaceAll(impDeal.Info.Prefix, " ", "")
+	return len(impDeal.Info.Prefix) > 0 && impDeal.Info.MinDealTier > 0
+}
+
+func updateHbPbCatDur(bid *pbsOrtbBid, dealTierInfo *DealTierInfo) {
+	if bid.dealPriority >= dealTierInfo.MinDealTier {
+		prefixTier := fmt.Sprintf("%s%d_", dealTierInfo.Prefix, bid.dealPriority)
+
+		if oldCatDur, ok := bid.bidTargets["hb_pb_cat_dur"]; ok {
+			oldCatDurSplit := strings.SplitAfterN(oldCatDur, "_", 2)
+			oldCatDurSplit[0] = prefixTier
+
+			newCatDur := strings.Join(oldCatDurSplit, "")
+			bid.bidTargets["hb_pb_cat_dur"] = newCatDur
+		}
+	}
+}
+
 func (e *exchange) makeAuctionContext(ctx context.Context, needsCache bool) (auctionCtx context.Context, cancel context.CancelFunc) {
 	auctionCtx = ctx
 	cancel = func() {}
diff --git a/openrtb_ext/bid_request_video.go b/openrtb_ext/bid_request_video.go
index 45447685..f7ddf203 100644
--- a/openrtb_ext/bid_request_video.go
+++ b/openrtb_ext/bid_request_video.go
@@ -136,6 +136,14 @@ type BidRequestVideo struct {
 	// Description:
 	//   Contains the OpenRTB Regs object to be passed to OpenRTB request
 	Regs *openrtb.Regs `json:"regs,omitempty"`
+
+	// Attribute:
+	//   supportdeals
+	// Type:
+	//   bool; optional
+	// Description:
+	//   Indicates that the response should update key to include prefix and tier
+	SupportDeals bool `json:"supportdeals,omitempty"`
 }
 
 type PodConfig struct {
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 7a3f24eb..6e70ef4b 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -29,6 +29,7 @@ const (
 	BidderAdvangelists     BidderName = "advangelists"
 	BidderApplogy          BidderName = "applogy"
 	BidderAppnexus         BidderName = "appnexus"
+	BidderAdoppler         BidderName = "adoppler"
 	BidderBeachfront       BidderName = "beachfront"
 	BidderBrightroll       BidderName = "brightroll"
 	BidderConsumable       BidderName = "consumable"
@@ -84,6 +85,7 @@ var BidderMap = map[string]BidderName{
 	"advangelists":      BidderAdvangelists,
 	"applogy":           BidderApplogy,
 	"appnexus":          BidderAppnexus,
+	"adoppler":          BidderAdoppler,
 	"beachfront":        BidderBeachfront,
 	"brightroll":        BidderBrightroll,
 	"consumable":        BidderConsumable,
diff --git a/openrtb_ext/imp_adoppler.go b/openrtb_ext/imp_adoppler.go
new file mode 100644
index 00000000..4b3ba97c
--- /dev/null
+++ b/openrtb_ext/imp_adoppler.go
@@ -0,0 +1,5 @@
+package openrtb_ext
+
+type ExtImpAdoppler struct {
+	AdUnit string `json:"adunit"`
+}
diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go
index 9226ff29..ee1a0cd0 100644
--- a/openrtb_ext/request.go
+++ b/openrtb_ext/request.go
@@ -17,6 +17,7 @@ type ExtRequestPrebid struct {
 	Cache                *ExtRequestPrebidCache `json:"cache,omitempty"`
 	StoredRequest        *ExtStoredRequest      `json:"storedrequest,omitempty"`
 	Targeting            *ExtRequestTargeting   `json:"targeting,omitempty"`
+	SupportDeals         bool                   `json:"supportdeals,omitempty"`
 }
 
 // ExtRequestPrebidCache defines the contract for bidrequest.ext.prebid.cache
diff --git a/static/bidder-info/adoppler.yaml b/static/bidder-info/adoppler.yaml
new file mode 100644
index 00000000..7fa79eda
--- /dev/null
+++ b/static/bidder-info/adoppler.yaml
@@ -0,0 +1,11 @@
+maintainer:
+  email: info@adoppler.com
+capabilities:
+  app:
+    mediaTypes:
+      - banner
+      - video
+  site:
+    mediaTypes:
+      - banner
+      - video
diff --git a/static/bidder-info/visx.yaml b/static/bidder-info/visx.yaml
index dd4f6c66..f404a013 100644
--- a/static/bidder-info/visx.yaml
+++ b/static/bidder-info/visx.yaml
@@ -4,3 +4,6 @@ capabilities:
   site:
     mediaTypes:
       - banner
+  app:
+    mediaTypes:
+      - banner
diff --git a/static/bidder-params/adoppler.json b/static/bidder-params/adoppler.json
new file mode 100644
index 00000000..c2bdde4f
--- /dev/null
+++ b/static/bidder-params/adoppler.json
@@ -0,0 +1,13 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "Adoppler Adapter Params",
+  "description": "A schema which validates params accepted by the Adoppler adapter",
+  "type": "object",
+  "properties": {
+      "adunit": {
+        "type": "string",
+        "description": "AdUnit to bid against to."
+      }
+  },
+  "required": ["adunit"]
+}


Test patch:
diff --git a/adapters/adoppler/adoppler_test.go b/adapters/adoppler/adoppler_test.go
new file mode 100644
index 00000000..e7d908df
--- /dev/null
+++ b/adapters/adoppler/adoppler_test.go
@@ -0,0 +1,12 @@
+package adoppler
+
+import (
+	"testing"
+
+	"github.com/prebid/prebid-server/adapters/adapterstest"
+)
+
+func TestJsonSamples(t *testing.T) {
+	bidder := NewAdopplerBidder("http://adoppler.com")
+	adapterstest.RunJSONBidderTest(t, "adopplertest", bidder)
+}
diff --git a/adapters/adoppler/adopplertest/exemplary/multibid.json b/adapters/adoppler/adopplertest/exemplary/multibid.json
new file mode 100644
index 00000000..851f4c5b
--- /dev/null
+++ b/adapters/adoppler/adopplertest/exemplary/multibid.json
@@ -0,0 +1,60 @@
+{"mockBidRequest": {"id": "req1",
+                    "imp":[{"id": "imp1",
+                            "banner": {"w": 100,
+                                       "h": 200},
+                            "ext": {"bidder": {"adunit": "unit1"}}},
+                           {"id": "imp2",
+                            "video": {"minduration": 120,
+                                      "mimes": ["video/mp4"]},
+                            "ext": {"bidder": {"adunit": "unit2"}}},
+                           {"id": "imp3",
+                            "native": {"request": "{}"},
+                            "ext": {"bidder": {"adunit": "unit3"}}}]},
+ "httpCalls": [{"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit1",
+                                    "body": {"id": "req1-unit1",
+                                        "imp": [{"id": "imp1",
+                                                 "banner": {"w": 100, "h": 200},
+                                                 "ext": {"bidder": {"adunit": "unit1"}}}]}},
+                "mockResponse": {"status": 200,
+                                 "body": {"id": "req1-imp1-resp1",
+                                          "seatbid": [{"bid": [{"id": "req1-imp1-bid1",
+                                                                "impid": "imp1",
+                                                                "price": 0.12,
+                                                                "adm": "<b>a banner</b>"}]}],
+                                          "cur": "USD"}}},
+               {"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit2",
+                                    "body": {"id": "req1-unit2",
+                                        "imp": [{"id": "imp2",
+                                                 "video": {"minduration": 120,
+                                                           "mimes": ["video/mp4"]},
+                                                 "ext": {"bidder": {"adunit": "unit2"}}}]}},
+                "mockResponse": {"status": 200,
+                                 "body": {"id": "req1-imp2-resp2",
+                                          "seatbid": [{"bid": [{"id": "req1-imp2-bid1",
+                                                                "impid": "imp2",
+                                                                "price": 0.24,
+                                                                "adm": "<VAST />",
+                                                                "cat": ["IAB1", "IAB2"],
+                                                                "ext": {"ads": {"video": {"duration": 121}}}}]}],
+                                          "cur": "USD"}}},
+               {"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit3",
+                                    "body": {"id": "req1-unit3",
+                                        "imp": [{"id": "imp3",
+                                                 "native": {"request": "{}"},
+                                                 "ext": {"bidder": {"adunit": "unit3"}}}]}},
+                "mockResponse": {"status": 204,
+                                 "body": ""}}],
+ "expectedBidResponses": [{"currency": "USD",
+                           "bids": [{"bid": {"id": "req1-imp1-bid1",
+                                             "impid": "imp1",
+                                             "price": 0.12,
+                                             "adm": "<b>a banner</b>"},
+                                     "type": "banner"}]},
+                          {"currency": "USD",
+                           "bids": [{"bid": {"id": "req1-imp2-bid1",
+                                             "impid": "imp2",
+                                             "price": 0.24,
+                                             "adm": "<VAST />",
+                                             "cat": ["IAB1", "IAB2"],
+                                             "ext": {"ads": {"video": {"duration": 121}}}},
+                                     "type": "video"}]}]}
diff --git a/adapters/adoppler/adopplertest/exemplary/no-bid.json b/adapters/adoppler/adopplertest/exemplary/no-bid.json
new file mode 100644
index 00000000..0e0f1358
--- /dev/null
+++ b/adapters/adoppler/adopplertest/exemplary/no-bid.json
@@ -0,0 +1,13 @@
+{"mockBidRequest": {"id": "req1",
+                    "imp":[{"id": "imp1",
+                            "banner": {"w": 100,
+                                       "h": 200},
+                            "ext": {"bidder": {"adunit": "unit1"}}}]},
+ "httpCalls": [{"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit1",
+                                    "body": {"id": "req1-unit1",
+                                        "imp": [{"id": "imp1",
+                                                 "banner": {"w": 100, "h": 200},
+                                                 "ext": {"bidder": {"adunit": "unit1"}}}]}},
+                "mockResponse": {"status": 204,
+                                 "body": ""}}],
+ "expectedBidResponses": []}
diff --git a/adapters/adoppler/adopplertest/supplemental/bad-request.json b/adapters/adoppler/adopplertest/supplemental/bad-request.json
new file mode 100644
index 00000000..3bdd5a55
--- /dev/null
+++ b/adapters/adoppler/adopplertest/supplemental/bad-request.json
@@ -0,0 +1,15 @@
+{"mockBidRequest": {"id": "req1",
+                    "imp":[{"id": "imp1",
+                            "banner": {"w": 100,
+                                       "h": 200},
+                            "ext": {"bidder": {"adunit": "unit1"}}}]},
+ "httpCalls": [{"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit1",
+                                    "body": {"id": "req1-unit1",
+                                        "imp": [{"id": "imp1",
+                                                 "banner": {"w": 100, "h": 200},
+                                                 "ext": {"bidder": {"adunit": "unit1"}}}]}},
+                "mockResponse": {"status": 400,
+                                 "body": ""}}],
+ "expectedBidResponses": [],
+ "expectedMakeBidsErrors": [{"value": "bad request",
+                             "comparison": "literal"}]}
diff --git a/adapters/adoppler/adopplertest/supplemental/duplicate-imp.json b/adapters/adoppler/adopplertest/supplemental/duplicate-imp.json
new file mode 100644
index 00000000..4382e36c
--- /dev/null
+++ b/adapters/adoppler/adopplertest/supplemental/duplicate-imp.json
@@ -0,0 +1,38 @@
+{"mockBidRequest": {"id": "req1",
+                    "imp":[{"id": "imp1",
+                            "banner": {"w": 100,
+                                       "h": 200},
+                            "ext": {"bidder": {"adunit": "unit1"}}},
+                           {"id": "imp1",
+                            "banner": {"w": 100,
+                                       "h": 200},
+                            "ext": {"bidder": {"adunit": "unit2"}}}]},
+ "httpCalls": [{"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit1",
+                                    "body": {"id": "req1-unit1",
+                                        "imp": [{"id": "imp1",
+                                                 "banner": {"w": 100, "h": 200},
+                                                 "ext": {"bidder": {"adunit": "unit1"}}}]}},
+                "mockResponse": {"status": 200,
+                                 "body": {"id": "req1-imp1-resp1",
+                                          "seatbid": [{"bid": [{"id": "req1-imp1-bid1",
+                                                                "impid": "imp1",
+                                                                "price": 0.12,
+                                                                "adm": "<b>a banner</b>"}]}],
+                                          "cur": "USD"}}},
+               {"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit2",
+                                    "body": {"id": "req1-unit2",
+                                        "imp": [{"id": "imp1",
+                                                 "banner": {"w": 100, "h": 200},
+                                                 "ext": {"bidder": {"adunit": "unit2"}}}]}},
+                "mockResponse": {"status": 200,
+                                 "body": {"id": "req1-imp1-resp1",
+                                          "seatbid": [{"bid": [{"id": "req1-imp1-bid1",
+                                                                "impid": "imp1",
+                                                                "price": 0.12,
+                                                                "adm": "<b>a banner</b>"}]}],
+                                          "cur": "USD"}}}],
+ "expectedBidResponses": [],
+ "expectedMakeBidsErrors": [{"value": "duplicate $.imp.id imp1",
+                             "comparison": "literal"},
+                            {"value": "duplicate $.imp.id imp1",
+                             "comparison": "literal"}]}
diff --git a/adapters/adoppler/adopplertest/supplemental/invalid-impid.json b/adapters/adoppler/adopplertest/supplemental/invalid-impid.json
new file mode 100644
index 00000000..2e6ecf4a
--- /dev/null
+++ b/adapters/adoppler/adopplertest/supplemental/invalid-impid.json
@@ -0,0 +1,20 @@
+{"mockBidRequest": {"id": "req1",
+                    "imp":[{"id": "imp1",
+                            "banner": {"w": 100,
+                                       "h": 200},
+                            "ext": {"bidder": {"adunit": "unit1"}}}]},
+ "httpCalls": [{"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit1",
+                                    "body": {"id": "req1-unit1",
+                                        "imp": [{"id": "imp1",
+                                                 "banner": {"w": 100, "h": 200},
+                                                 "ext": {"bidder": {"adunit": "unit1"}}}]}},
+                "mockResponse": {"status": 200,
+                                 "body": {"id": "req1-imp1-resp1",
+                                          "seatbid": [{"bid": [{"id": "req1-imp1-bid1",
+                                                                "impid": "invalid",
+                                                                "price": 0.12,
+                                                                "adm": "<b>a banner</b>"}]}],
+                                          "cur": "USD"}}}],
+ "expectedBidResponses": [],
+ "expectedMakeBidsErrors": [{"value": "unknown impid: invalid",
+                             "comparison": "literal"}]}
diff --git a/adapters/adoppler/adopplertest/supplemental/invalid-response.json b/adapters/adoppler/adopplertest/supplemental/invalid-response.json
new file mode 100644
index 00000000..72420881
--- /dev/null
+++ b/adapters/adoppler/adopplertest/supplemental/invalid-response.json
@@ -0,0 +1,15 @@
+{"mockBidRequest": {"id": "req1",
+                    "imp":[{"id": "imp1",
+                            "banner": {"w": 100,
+                                       "h": 200},
+                            "ext": {"bidder": {"adunit": "unit1"}}}]},
+ "httpCalls": [{"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit1",
+                                    "body": {"id": "req1-unit1",
+                                        "imp": [{"id": "imp1",
+                                                 "banner": {"w": 100, "h": 200},
+                                                 "ext": {"bidder": {"adunit": "unit1"}}}]}},
+                "mockResponse": {"status": 200,
+                                 "body": "invalid-json"}}],
+ "expectedBidResponses": [],
+ "expectedMakeBidsErrors": [{"value": "invalid body: json: cannot unmarshal string into Go value of type openrtb.BidResponse",
+                             "comparison": "literal"}]}
diff --git a/adapters/adoppler/adopplertest/supplemental/invalid-video-ext.json b/adapters/adoppler/adopplertest/supplemental/invalid-video-ext.json
new file mode 100644
index 00000000..d9cb6daa
--- /dev/null
+++ b/adapters/adoppler/adopplertest/supplemental/invalid-video-ext.json
@@ -0,0 +1,43 @@
+{"mockBidRequest": {"id": "req1",
+                    "imp":[{"id": "imp1",
+                            "video": {"minduration": 120,
+                                      "mimes": ["video/mp4"]},
+                            "ext": {"bidder": {"adunit": "unit1"}}},
+                          {"id": "imp2",
+                           "video": {"minduration": 120,
+                                     "mimes": ["video/mp4"]},
+                           "ext": {"bidder": {"adunit": "unit2"}}}]},
+ "httpCalls": [{"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit1",
+                                    "body": {"id": "req1-unit1",
+                                        "imp": [{"id": "imp1",
+                                                 "video": {"minduration": 120,
+                                                           "mimes": ["video/mp4"]},
+                                                 "ext": {"bidder": {"adunit": "unit1"}}}]}},
+                "mockResponse": {"status": 200,
+                                 "body": {"id": "req1-imp1-resp1",
+                                          "seatbid": [{"bid": [{"id": "req1-imp1-bid1",
+                                                                "impid": "imp1",
+                                                                "price": 0.24,
+                                                                "adm": "<VAST />",
+                                                                "cat": ["IAB1", "IAB2"],
+                                                                "ext": {}}]}],
+                                          "cur": "USD"}}},
+               {"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit2",
+                                    "body": {"id": "req1-unit2",
+                                        "imp": [{"id": "imp2",
+                                                 "video": {"minduration": 120,
+                                                           "mimes": ["video/mp4"]},
+                                                 "ext": {"bidder": {"adunit": "unit2"}}}]}},
+                "mockResponse": {"status": 200,
+                                 "body": {"id": "req1-imp2-resp2",
+                                          "seatbid": [{"bid": [{"id": "req1-imp2-bid2",
+                                                                "impid": "imp2",
+                                                                "price": 0.24,
+                                                                "adm": "<VAST />",
+                                                                "cat": ["IAB1", "IAB2"],
+                                                                "ext": ""}]}],
+                                          "cur": "USD"}}}],
+ "expectedMakeBidsErrors": [{"value": "$.seatbid.bid.ext.ads.video required",
+                             "comparison": "literal"},
+                            {"value": "json: cannot unmarshal string into Go value of type struct { Ads *adoppler.adsImpExt \"json:\\\"ads\\\"\" }",
+                             "comparison": "literal"}]}
diff --git a/adapters/adoppler/adopplertest/supplemental/missing-adunit.json b/adapters/adoppler/adopplertest/supplemental/missing-adunit.json
new file mode 100644
index 00000000..82a6a95e
--- /dev/null
+++ b/adapters/adoppler/adopplertest/supplemental/missing-adunit.json
@@ -0,0 +1,9 @@
+{"mockBidRequest": {"id": "req1",
+                    "imp":[{"id": "imp1",
+                            "banner": {"w": 100,
+                                       "h": 200},
+                            "ext": {"bidder": {}}}]},
+ "httpCalls": [],
+ "expectedBidResponses": [],
+ "expectedMakeRequestsErrors": [{"value": "$.imp.ext.adoppler.adunit required",
+                                 "comparison": "literal"}]}
diff --git a/adapters/adoppler/adopplertest/supplemental/server-error.json b/adapters/adoppler/adopplertest/supplemental/server-error.json
new file mode 100644
index 00000000..df23bac0
--- /dev/null
+++ b/adapters/adoppler/adopplertest/supplemental/server-error.json
@@ -0,0 +1,15 @@
+{"mockBidRequest": {"id": "req1",
+                    "imp":[{"id": "imp1",
+                            "banner": {"w": 100,
+                                       "h": 200},
+                            "ext": {"bidder": {"adunit": "unit1"}}}]},
+ "httpCalls": [{"expectedRequest": {"uri": "http://adoppler.com/processHeaderBid/unit1",
+                                    "body": {"id": "req1-unit1",
+                                        "imp": [{"id": "imp1",
+                                                 "banner": {"w": 100, "h": 200},
+                                                 "ext": {"bidder": {"adunit": "unit1"}}}]}},
+                "mockResponse": {"status": 500,
+                                 "body": ""}}],
+ "expectedBidResponses": [],
+ "expectedMakeBidsErrors": [{"value": "unexpected status: 500",
+                             "comparison": "literal"}]}
diff --git a/adapters/appnexus/appnexusplatformtest/exemplary/simple-auction.json b/adapters/appnexus/appnexusplatformtest/exemplary/simple-auction.json
index 03c3f4c5..e0c0435f 100644
--- a/adapters/appnexus/appnexusplatformtest/exemplary/simple-auction.json
+++ b/adapters/appnexus/appnexusplatformtest/exemplary/simple-auction.json
@@ -80,7 +80,8 @@
 					  "auction_id": 8189378542222915032,
 					  "bid_ad_type": 1,
 					  "bidder_id": 2,
-					  "ranking_price": 0.000000
+					  "ranking_price": 0.000000,
+					  "deal_priority": 5
 					}
 				  }
 				}]
@@ -118,7 +119,8 @@
 				  "auction_id": 8189378542222915032,
 				  "bid_ad_type": 1,
 				  "bidder_id": 2,
-				  "ranking_price": 0.000000
+				  "ranking_price": 0.000000,
+				  "deal_priority": 5
 				}
 			  }
 			},
diff --git a/adapters/appnexus/appnexusplatformtest/video/simple-video.json b/adapters/appnexus/appnexusplatformtest/video/simple-video.json
index 85960427..7ee192be 100644
--- a/adapters/appnexus/appnexusplatformtest/video/simple-video.json
+++ b/adapters/appnexus/appnexusplatformtest/video/simple-video.json
@@ -80,7 +80,8 @@
 					  "auction_id": 8189378542222915032,
 					  "bid_ad_type": 1,
 					  "bidder_id": 2,
-					  "ranking_price": 0.000000
+					  "ranking_price": 0.000000,
+					  "deal_priority": 5
 					}
 				  }
 				}]
@@ -118,7 +119,8 @@
 				  "auction_id": 8189378542222915032,
 				  "bid_ad_type": 1,
 				  "bidder_id": 2,
-				  "ranking_price": 0.000000
+				  "ranking_price": 0.000000,
+				  "deal_priority": 5
 				}
 			  }
 			},
diff --git a/adapters/appnexus/appnexustest/amp/simple-banner.json b/adapters/appnexus/appnexustest/amp/simple-banner.json
index 646359b4..54e6a143 100644
--- a/adapters/appnexus/appnexustest/amp/simple-banner.json
+++ b/adapters/appnexus/appnexustest/amp/simple-banner.json
@@ -91,7 +91,8 @@
                     "auction_id": 8189378542222915032,
                     "bid_ad_type": 0,
                     "bidder_id": 2,
-                    "ranking_price": 0.000000
+                    "ranking_price": 0.000000,
+                    "deal_priority": 5
                   }
                 }
               }]
@@ -129,7 +130,8 @@
                 "auction_id": 8189378542222915032,
                 "bid_ad_type": 0,
                 "bidder_id": 2,
-                "ranking_price": 0.000000
+                "ranking_price": 0.000000,
+                "deal_priority": 5
               }
             }
           },
diff --git a/adapters/appnexus/appnexustest/amp/simple-video.json b/adapters/appnexus/appnexustest/amp/simple-video.json
index a6f96be3..061d5c94 100644
--- a/adapters/appnexus/appnexustest/amp/simple-video.json
+++ b/adapters/appnexus/appnexustest/amp/simple-video.json
@@ -82,7 +82,8 @@
 					  "auction_id": 8189378542222915032,
 					  "bid_ad_type": 1,
 					  "bidder_id": 2,
-					  "ranking_price": 0.000000
+					  "ranking_price": 0.000000,
+					  "deal_priority": 5
 					}
 				  }
 				}]
@@ -120,7 +121,8 @@
 				  "auction_id": 8189378542222915032,
 				  "bid_ad_type": 1,
 				  "bidder_id": 2,
-				  "ranking_price": 0.000000
+				  "ranking_price": 0.000000,
+				  "deal_priority": 5
 				}
 			  }
 			},
diff --git a/adapters/appnexus/appnexustest/exemplary/native-1.1.json b/adapters/appnexus/appnexustest/exemplary/native-1.1.json
index 86b75505..189304fd 100644
--- a/adapters/appnexus/appnexustest/exemplary/native-1.1.json
+++ b/adapters/appnexus/appnexustest/exemplary/native-1.1.json
@@ -96,7 +96,8 @@
                       "brand_category_id": 350,
                       "auction_id": 5607483846416358664,
                       "bidder_id": 2,
-                      "bid_ad_type": 3
+                      "bid_ad_type": 3,
+                      "deal_priority": 5
                     }
                   }
                 }
@@ -136,7 +137,8 @@
                 "brand_category_id": 350,
                 "auction_id": 5607483846416358664,
                 "bidder_id": 2,
-                "bid_ad_type": 3
+                "bid_ad_type": 3,
+                "deal_priority": 5
               }
             }
           },
diff --git a/adapters/appnexus/appnexustest/exemplary/simple-banner.json b/adapters/appnexus/appnexustest/exemplary/simple-banner.json
index e5bd3116..59931fb6 100644
--- a/adapters/appnexus/appnexustest/exemplary/simple-banner.json
+++ b/adapters/appnexus/appnexustest/exemplary/simple-banner.json
@@ -89,7 +89,8 @@
                     "auction_id": 8189378542222915032,
                     "bid_ad_type": 0,
                     "bidder_id": 2,
-                    "ranking_price": 0.000000
+                    "ranking_price": 0.000000,
+                    "deal_priority": 5
                   }
                 }
               }]
@@ -127,7 +128,8 @@
                 "auction_id": 8189378542222915032,
                 "bid_ad_type": 0,
                 "bidder_id": 2,
-                "ranking_price": 0.000000
+                "ranking_price": 0.000000,
+                "deal_priority": 5
               }
             }
           },
diff --git a/adapters/appnexus/appnexustest/exemplary/simple-video.json b/adapters/appnexus/appnexustest/exemplary/simple-video.json
index 15755c7d..ced90c39 100644
--- a/adapters/appnexus/appnexustest/exemplary/simple-video.json
+++ b/adapters/appnexus/appnexustest/exemplary/simple-video.json
@@ -80,7 +80,8 @@
 					  "auction_id": 8189378542222915032,
 					  "bid_ad_type": 1,
 					  "bidder_id": 2,
-					  "ranking_price": 0.000000
+					  "ranking_price": 0.000000,
+					  "deal_priority": 5
 					}
 				  }
 				}]
@@ -118,7 +119,8 @@
 				  "auction_id": 8189378542222915032,
 				  "bid_ad_type": 1,
 				  "bidder_id": 2,
-				  "ranking_price": 0.000000
+				  "ranking_price": 0.000000,
+				  "deal_priority": 5
 				}
 			  }
 			},
diff --git a/adapters/appnexus/appnexustest/exemplary/video-invalid-category.json b/adapters/appnexus/appnexustest/exemplary/video-invalid-category.json
index d3686af0..257905c8 100644
--- a/adapters/appnexus/appnexustest/exemplary/video-invalid-category.json
+++ b/adapters/appnexus/appnexustest/exemplary/video-invalid-category.json
@@ -79,7 +79,8 @@
 					  "auction_id": 8189378542222915032,
 					  "bid_ad_type": 1,
 					  "bidder_id": 2,
-					  "ranking_price": 0.000000
+					  "ranking_price": 0.000000,
+					  "deal_priority": 5
 					}
 				  }
 				}]
@@ -116,7 +117,8 @@
 				  "auction_id": 8189378542222915032,
 				  "bid_ad_type": 1,
 				  "bidder_id": 2,
-				  "ranking_price": 0.000000
+				  "ranking_price": 0.000000,
+				  "deal_priority": 5
 				}
 			  }
 			},
diff --git a/adapters/appnexus/appnexustest/supplemental/displaymanager-test.json b/adapters/appnexus/appnexustest/supplemental/displaymanager-test.json
index d5c981c6..c6ad330e 100644
--- a/adapters/appnexus/appnexustest/supplemental/displaymanager-test.json
+++ b/adapters/appnexus/appnexustest/supplemental/displaymanager-test.json
@@ -106,7 +106,8 @@
                       "auction_id": 8189378542222915032,
                       "bid_ad_type": 0,
                       "bidder_id": 2,
-                      "ranking_price": 0.000000
+                      "ranking_price": 0.000000,
+                      "deal_priority": 5
                     }
                   }
                 }]
@@ -144,7 +145,8 @@
                   "auction_id": 8189378542222915032,
                   "bid_ad_type": 0,
                   "bidder_id": 2,
-                  "ranking_price": 0.000000
+                  "ranking_price": 0.000000,
+                  "deal_priority": 5
                 }
               }
             },
diff --git a/adapters/appnexus/appnexustest/supplemental/multi-bid.json b/adapters/appnexus/appnexustest/supplemental/multi-bid.json
index 7234551e..9e63bdce 100644
--- a/adapters/appnexus/appnexustest/supplemental/multi-bid.json
+++ b/adapters/appnexus/appnexustest/supplemental/multi-bid.json
@@ -89,7 +89,8 @@
                     "auction_id": 8189378542222915032,
                     "bid_ad_type": 0,
                     "bidder_id": 2,
-                    "ranking_price": 0.000000
+                    "ranking_price": 0.000000,
+                    "deal_priority": 4
                   }
                 }
               },
@@ -112,7 +113,8 @@
                     "auction_id": 8189378542222915032,
                     "bid_ad_type": 0,
                     "bidder_id": 2,
-                    "ranking_price": 0.000000
+                    "ranking_price": 0.000000,
+                    "deal_priority": 5
                   }
                 }
               }]
@@ -150,7 +152,8 @@
                 "auction_id": 8189378542222915032,
                 "bid_ad_type": 0,
                 "bidder_id": 2,
-                "ranking_price": 0.000000
+                "ranking_price": 0.000000,
+                "deal_priority": 4
               }
             }
           },
@@ -177,7 +180,8 @@
                 "auction_id": 8189378542222915032,
                 "bid_ad_type": 0,
                 "bidder_id": 2,
-                "ranking_price": 0.000000
+                "ranking_price": 0.000000,
+                "deal_priority": 5
               }
             }
           },
diff --git a/adapters/audienceNetwork/facebook_test.go b/adapters/audienceNetwork/facebook_test.go
index 2ce0ef3b..1edaabd4 100644
--- a/adapters/audienceNetwork/facebook_test.go
+++ b/adapters/audienceNetwork/facebook_test.go
@@ -4,7 +4,9 @@ import (
 	"testing"
 	"time"
 
+	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/adapters/adapterstest"
+	"github.com/stretchr/testify/assert"
 )
 
 type tagInfo struct {
@@ -40,3 +42,38 @@ type FacebookExt struct {
 func TestJsonSamples(t *testing.T) {
 	adapterstest.RunJSONBidderTest(t, "audienceNetworktest", NewFacebookBidder(nil, "test-platform-id", "test-app-secret"))
 }
+
+func TestMakeTimeoutNotice(t *testing.T) {
+	req := adapters.RequestData{
+		Body: []byte(`{"imp":[{"id":"1234"}]}}`),
+	}
+	fba := NewFacebookBidder(nil, "test-platform-id", "test-app-secret")
+
+	tb, ok := fba.(adapters.TimeoutBidder)
+	if !ok {
+		t.Error("Facebook adapter is not a TimeoutAdapter")
+	}
+
+	toReq, err := tb.MakeTimeoutNotification(&req)
+	assert.Nil(t, err, "Facebook MakeTimeoutNotification() return an error %v", err)
+	expectedUri := "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=test-platform-id&auction=1234&ortb_loss_code=2"
+	assert.Equal(t, expectedUri, toReq.Uri, "Facebook timeout notification not returning the expected URI.")
+
+}
+
+func TestMakeTimeoutNoticeBadRequest(t *testing.T) {
+	req := adapters.RequestData{
+		Body: []byte(`{"imp":[{{"id":"1234"}}`),
+	}
+	fba := NewFacebookBidder(nil, "test-platform-id", "test-app-secret")
+
+	tb, ok := fba.(adapters.TimeoutBidder)
+	if !ok {
+		t.Error("Facebook adapter is not a TimeoutAdapter")
+	}
+
+	toReq, err := tb.MakeTimeoutNotification(&req)
+	assert.Empty(t, toReq.Uri, "Facebook MakeTimeoutNotification() did not return nil", err)
+	assert.NotNil(t, err, "Facebook MakeTimeoutNotification() did not return an error")
+
+}
diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go
index 173bc37e..46f63cc6 100644
--- a/exchange/bidder_test.go
+++ b/exchange/bidder_test.go
@@ -39,13 +39,15 @@ func TestSingleBidder(t *testing.T) {
 				Bid: &openrtb.Bid{
 					Price: firstInitialPrice,
 				},
-				BidType: openrtb_ext.BidTypeBanner,
+				BidType:      openrtb_ext.BidTypeBanner,
+				DealPriority: 4,
 			},
 			{
 				Bid: &openrtb.Bid{
 					Price: secondInitialPrice,
 				},
-				BidType: openrtb_ext.BidTypeVideo,
+				BidType:      openrtb_ext.BidTypeVideo,
+				DealPriority: 5,
 			},
 		},
 	}
@@ -88,6 +90,9 @@ func TestSingleBidder(t *testing.T) {
 		if typedBid.BidType != seatBid.bids[index].bidType {
 			t.Errorf("Bid %d did not have the right type. Expected %s, got %s", index, typedBid.BidType, seatBid.bids[index].bidType)
 		}
+		if typedBid.DealPriority != seatBid.bids[index].dealPriority {
+			t.Errorf("Bid %d did not have the right deal priority. Expected %s, got %s", index, typedBid.BidType, seatBid.bids[index].bidType)
+		}
 	}
 	if mockBidderResponse.Bids[0].Bid.Price != bidAdjustment*firstInitialPrice {
 		t.Errorf("Bid[0].Price was not adjusted properly. Expected %f, got %f", bidAdjustment*firstInitialPrice, mockBidderResponse.Bids[0].Bid.Price)
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index 7e199d4b..0a64bce0 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -914,10 +914,10 @@ func TestCategoryMapping(t *testing.T) {
 	bid3 := openrtb.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1}
 	bid4 := openrtb.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 40.0000, Cat: cats4, W: 1, H: 1}
 
-	bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}}
-	bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}}
-	bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}}
-	bid1_4 := pbsOrtbBid{&bid4, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}}
+	bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0}
+	bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, 0}
+	bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, 0}
+	bid1_4 := pbsOrtbBid{&bid4, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0}
 
 	innerBids := []*pbsOrtbBid{
 		&bid1_1,
@@ -969,10 +969,10 @@ func TestCategoryMappingNoIncludeBrandCategory(t *testing.T) {
 	bid3 := openrtb.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1}
 	bid4 := openrtb.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 40.0000, Cat: cats4, W: 1, H: 1}
 
-	bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}}
-	bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}}
-	bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}}
-	bid1_4 := pbsOrtbBid{&bid4, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}}
+	bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0}
+	bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, 0}
+	bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, 0}
+	bid1_4 := pbsOrtbBid{&bid4, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, 0}
 
 	innerBids := []*pbsOrtbBid{
 		&bid1_1,
@@ -1023,9 +1023,9 @@ func TestCategoryMappingTranslateCategoriesNil(t *testing.T) {
 	bid2 := openrtb.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 20.0000, Cat: cats2, W: 1, H: 1}
 	bid3 := openrtb.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1}
 
-	bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}}
-	bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}}
-	bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}}
+	bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0}
+	bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, 0}
+	bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0}
 
 	innerBids := []*pbsOrtbBid{
 		&bid1_1,
@@ -1105,9 +1105,9 @@ func TestCategoryMappingTranslateCategoriesFalse(t *testing.T) {
 	bid2 := openrtb.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 20.0000, Cat: cats2, W: 1, H: 1}
 	bid3 := openrtb.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1}
 
-	bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}}
-	bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}}
-	bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}}
+	bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0}
+	bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, 0}
+	bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0}
 
 	innerBids := []*pbsOrtbBid{
 		&bid1_1,
@@ -1156,10 +1156,10 @@ func TestCategoryDedupe(t *testing.T) {
 	bid3 := openrtb.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 10.0000, Cat: cats1, W: 1, H: 1}
 	bid4 := openrtb.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 20.0000, Cat: cats4, W: 1, H: 1}
 
-	bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}}
-	bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}}
-	bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}}
-	bid1_4 := pbsOrtbBid{&bid4, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}}
+	bid1_1 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0}
+	bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, 0}
+	bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0}
+	bid1_4 := pbsOrtbBid{&bid4, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0}
 
 	selectedBids := make(map[string]int)
 	expectedCategories := map[string]string{
@@ -1288,7 +1288,7 @@ func TestBidRejectionErrors(t *testing.T) {
 		innerBids := []*pbsOrtbBid{}
 		for _, bid := range test.bids {
 			currentBid := pbsOrtbBid{
-				bid, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: test.duration},
+				bid, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: test.duration}, 0,
 			}
 			innerBids = append(innerBids, &currentBid)
 		}
@@ -1325,6 +1325,264 @@ func TestUpdateRejections(t *testing.T) {
 	assert.Containsf(t, rejections, "bid rejected [bid ID: bid_id2] reason: some reason 2", "Rejection message did not match expected")
 }
 
+func TestApplyDealSupport(t *testing.T) {
+	testCases := []struct {
+		description        string
+		dealPriority       int
+		impExt             json.RawMessage
+		targ               map[string]string
+		expectedHbPbCatDur string
+		expectedDealErr    string
+	}{
+		{
+			description:  "hb_pb_cat_dur should be modified",
+			dealPriority: 5,
+			impExt:       json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`),
+			targ: map[string]string{
+				"hb_pb_cat_dur": "12.00_movies_30s",
+			},
+			expectedHbPbCatDur: "tier5_movies_30s",
+			expectedDealErr:    "",
+		},
+		{
+			description:  "hb_pb_cat_dur should not be modified due to priority not exceeding min",
+			dealPriority: 9,
+			impExt:       json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 10, "prefix": "tier"}, "placementId": 10433394}}`),
+			targ: map[string]string{
+				"hb_pb_cat_dur": "12.00_medicine_30s",
+			},
+			expectedHbPbCatDur: "12.00_medicine_30s",
+			expectedDealErr:    "",
+		},
+		{
+			description:  "hb_pb_cat_dur should not be modified due to invalid config",
+			dealPriority: 5,
+			impExt:       json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": ""}, "placementId": 10433394}}`),
+			targ: map[string]string{
+				"hb_pb_cat_dur": "12.00_games_30s",
+			},
+			expectedHbPbCatDur: "12.00_games_30s",
+			expectedDealErr:    "dealTier configuration invalid for bidder 'appnexus', imp ID 'imp_id1'",
+		},
+		{
+			description:  "hb_pb_cat_dur should not be modified due to deal priority of 0",
+			dealPriority: 0,
+			impExt:       json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`),
+			targ: map[string]string{
+				"hb_pb_cat_dur": "12.00_auto_30s",
+			},
+			expectedHbPbCatDur: "12.00_auto_30s",
+			expectedDealErr:    "",
+		},
+	}
+
+	bidderName := openrtb_ext.BidderName("appnexus")
+	for _, test := range testCases {
+		bidRequest := &openrtb.BidRequest{
+			ID: "some-request-id",
+			Imp: []openrtb.Imp{
+				{
+					ID:  "imp_id1",
+					Ext: test.impExt,
+				},
+			},
+		}
+
+		bid := pbsOrtbBid{&openrtb.Bid{}, "video", test.targ, &openrtb_ext.ExtBidPrebidVideo{}, test.dealPriority}
+
+		auc := &auction{
+			winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{
+				"imp_id1": {
+					bidderName: &bid,
+				},
+			},
+		}
+
+		dealErrs := applyDealSupport(bidRequest, auc)
+
+		assert.Equal(t, test.expectedHbPbCatDur, auc.winningBidsByBidder["imp_id1"][bidderName].bidTargets["hb_pb_cat_dur"], test.description)
+		if len(test.expectedDealErr) > 0 {
+			assert.Containsf(t, dealErrs, errors.New(test.expectedDealErr), "Expected error message not found in deal errors")
+		}
+	}
+}
+
+func TestGetDealTiers(t *testing.T) {
+	testCases := []struct {
+		impExt       json.RawMessage
+		bidderResult map[string]bool // true indicates bidder had valid config, false indicates invalid
+	}{
+		{
+			impExt: json.RawMessage(`{"validbase": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`),
+			bidderResult: map[string]bool{
+				"validbase": true,
+			},
+		},
+		{
+			impExt: json.RawMessage(`{"validmultiple1": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}, "validmultiple2": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`),
+			bidderResult: map[string]bool{
+				"validmultiple1": true,
+				"validmultiple2": true,
+			},
+		},
+		{
+			impExt: json.RawMessage(`{"nodealtier": {"placementId": 10433394}}`),
+			bidderResult: map[string]bool{
+				"nodealtier": false,
+			},
+		},
+		{
+			impExt: json.RawMessage(`{"validbase": {"placementId": 10433394}, "onedealTier2": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`),
+			bidderResult: map[string]bool{
+				"onedealTier2": true,
+				"validbase":    false,
+			},
+		},
+	}
+
+	filledDealTier := DealTier{
+		Info: &DealTierInfo{
+			Prefix:      "tier",
+			MinDealTier: 5,
+		},
+	}
+	emptyDealTier := DealTier{}
+
+	for _, test := range testCases {
+		bidRequest := &openrtb.BidRequest{
+			ID: "some-request-id",
+			Imp: []openrtb.Imp{
+				{
+					ID:  "imp_id1",
+					Ext: test.impExt,
+				},
+			},
+		}
+
+		impDealMap := getDealTiers(bidRequest)
+
+		for bidder, valid := range test.bidderResult {
+			if valid {
+				assert.Equal(t, &filledDealTier, impDealMap["imp_id1"].DealInfo[bidder], "DealTier should be filled with config data")
+			} else {
+				assert.Equal(t, &emptyDealTier, impDealMap["imp_id1"].DealInfo[bidder], "DealTier should be empty")
+			}
+		}
+	}
+}
+
+func TestValidateAndNormalizeDealTier(t *testing.T) {
+	testCases := []struct {
+		description    string
+		params         json.RawMessage
+		expectedResult bool
+	}{
+		{
+			description:    "BidderDealTier should be valid",
+			params:         json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`),
+			expectedResult: true,
+		},
+		{
+			description:    "BidderDealTier should be invalid due to empty prefix",
+			params:         json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": ""}, "placementId": 10433394}}`),
+			expectedResult: false,
+		},
+		{
+			description:    "BidderDealTier should be invalid due to empty dealTier",
+			params:         json.RawMessage(`{"appnexus": {"dealTier": {}, "placementId": 10433394}}`),
+			expectedResult: false,
+		},
+		{
+			description:    "BidderDealTier should be invalid due to missing minDealTier",
+			params:         json.RawMessage(`{"appnexus": {"dealTier": {"prefix": "tier"}, "placementId": 10433394}}`),
+			expectedResult: false,
+		},
+		{
+			description:    "BidderDealTier should be invalid due to missing dealTier",
+			params:         json.RawMessage(`{"appnexus": {"placementId": 10433394}}`),
+			expectedResult: false,
+		},
+		{
+			description:    "BidderDealTier should be invalid due to prefix containing all whitespace",
+			params:         json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "    "}, "placementId": 10433394}}`),
+			expectedResult: false,
+		},
+		{
+			description:    "BidderDealTier should be valid after removing whitespace",
+			params:         json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "    prefixwith  sp aces "}, "placementId": 10433394}}`),
+			expectedResult: true,
+		},
+	}
+
+	for _, test := range testCases {
+		var bidderDealTier BidderDealTier
+		err := json.Unmarshal(test.params, &bidderDealTier.DealInfo)
+		if err != nil {
+			assert.Fail(t, "Unable to unmarshal JSON data for testing BidderDealTier")
+		}
+
+		assert.Equal(t, test.expectedResult, validateAndNormalizeDealTier(bidderDealTier.DealInfo["appnexus"]), test.description)
+	}
+}
+
+func TestUpdateHbPbCatDur(t *testing.T) {
+	testCases := []struct {
+		description        string
+		targ               map[string]string
+		dealTier           *DealTierInfo
+		dealPriority       int
+		expectedHbPbCatDur string
+	}{
+		{
+			description: "hb_pb_cat_dur should be updated with prefix and tier",
+			targ: map[string]string{
+				"hb_pb":         "12.00",
+				"hb_pb_cat_dur": "12.00_movies_30s",
+			},
+			dealTier: &DealTierInfo{
+				Prefix:      "tier",
+				MinDealTier: 5,
+			},
+			dealPriority:       5,
+			expectedHbPbCatDur: "tier5_movies_30s",
+		},
+		{
+			description: "hb_pb_cat_dur should not be updated due to bid priority",
+			targ: map[string]string{
+				"hb_pb":         "12.00",
+				"hb_pb_cat_dur": "12.00_auto_30s",
+			},
+			dealTier: &DealTierInfo{
+				Prefix:      "tier",
+				MinDealTier: 10,
+			},
+			dealPriority:       6,
+			expectedHbPbCatDur: "12.00_auto_30s",
+		},
+		{
+			description: "hb_pb_cat_dur should be updated with prefix and tier",
+			targ: map[string]string{
+				"hb_pb":         "12.00",
+				"hb_pb_cat_dur": "12.00_medicine_30s",
+			},
+			dealTier: &DealTierInfo{
+				Prefix:      "tier",
+				MinDealTier: 1,
+			},
+			dealPriority:       7,
+			expectedHbPbCatDur: "tier7_medicine_30s",
+		},
+	}
+
+	for _, test := range testCases {
+		bid := pbsOrtbBid{&openrtb.Bid{}, "video", test.targ, &openrtb_ext.ExtBidPrebidVideo{}, test.dealPriority}
+
+		updateHbPbCatDur(&bid, test.dealTier)
+
+		assert.Equal(t, test.expectedHbPbCatDur, bid.bidTargets["hb_pb_cat_dur"], test.description)
+	}
+}
+
 type exchangeSpec struct {
 	IncomingRequest  exchangeRequest        `json:"incomingRequest"`
 	OutgoingRequests map[string]*bidderSpec `json:"outgoingRequests"`
diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go
index ded8fd2b..87a9caeb 100644
--- a/usersync/usersyncers/syncer_test.go
+++ b/usersync/usersyncers/syncer_test.go
@@ -65,6 +65,7 @@ func TestNewSyncerMap(t *testing.T) {
 	}
 
 	adaptersWithoutSyncers := map[openrtb_ext.BidderName]bool{
+		openrtb_ext.BidderAdoppler:  true,
 		openrtb_ext.BidderApplogy:   true,
 		openrtb_ext.BidderTappx:     true,
 		openrtb_ext.BidderKubient:   true,