Repository: prebid/prebid-server
Pull request #2611 (2023-02-27 10:35:05)

Criteo: add video capability

vraybaud: Hi @gargcreation1992 , thanks a lot for your comments. We didn't take into account this case in the pull request as it was. I did some changes in the pull request. We decided to set directly the seatbid.bid.ext.prebid.type in our bidder so we don't add any additional logic regarding this in the adapter. Does it sounds fine for you ? I have updated the tests accordingly.
vraybaud: Hi @gargcreation1992 , thanks a lot for your comments. We didn't take into account the multi type case in this pull request. I did some changes to handle it. We decided to set directly the seatbid.bid.ext.prebid.type in our bidder so we don't add any additional logic regarding this in the adapter. Does it sound fine for you ? I have updated the tests accordingly.

Patch:
diff --git a/adapters/appnexus/appnexus.go b/adapters/appnexus/appnexus.go
index 7eadd757..de1d882f 100644
--- a/adapters/appnexus/appnexus.go
+++ b/adapters/appnexus/appnexus.go
@@ -6,7 +6,6 @@ import (
 	"fmt"
 	"math/rand"
 	"net/http"
-	"os"
 	"strconv"
 	"strings"
 
@@ -29,61 +28,6 @@ type adapter struct {
 	hbSource       int
 }
 
-type KeyVal struct {
-	Key    string   `json:"key,omitempty"`
-	Values []string `json:"value,omitempty"`
-}
-
-type appnexusAdapterOptions struct {
-	IabCategories map[string]string `json:"iab_categories"`
-}
-
-type appnexusImpExtAppnexus struct {
-	PlacementID       int             `json:"placement_id,omitempty"`
-	Keywords          string          `json:"keywords,omitempty"`
-	TrafficSourceCode string          `json:"traffic_source_code,omitempty"`
-	UsePmtRule        *bool           `json:"use_pmt_rule,omitempty"`
-	PrivateSizes      json.RawMessage `json:"private_sizes,omitempty"`
-}
-
-type appnexusImpExt struct {
-	Appnexus appnexusImpExtAppnexus `json:"appnexus"`
-}
-
-type appnexusBidExtVideo struct {
-	Duration int `json:"duration"`
-}
-
-type appnexusBidExtCreative struct {
-	Video appnexusBidExtVideo `json:"video"`
-}
-
-type appnexusBidExtAppnexus struct {
-	BidType       int                    `json:"bid_ad_type"`
-	BrandId       int                    `json:"brand_id"`
-	BrandCategory int                    `json:"brand_category_id"`
-	CreativeInfo  appnexusBidExtCreative `json:"creative_info"`
-	DealPriority  int                    `json:"deal_priority"`
-}
-
-type appnexusBidExt struct {
-	Appnexus appnexusBidExtAppnexus `json:"appnexus"`
-}
-
-type appnexusReqExtAppnexus struct {
-	IncludeBrandCategory    *bool  `json:"include_brand_category,omitempty"`
-	BrandCategoryUniqueness *bool  `json:"brand_category_uniqueness,omitempty"`
-	IsAMP                   int    `json:"is_amp,omitempty"`
-	HeaderBiddingSource     int    `json:"hb_source,omitempty"`
-	AdPodId                 string `json:"adpod_id,omitempty"`
-}
-
-// Full request extension including appnexus extension object
-type appnexusReqExt struct {
-	openrtb_ext.ExtRequest
-	Appnexus *appnexusReqExtAppnexus `json:"appnexus,omitempty"`
-}
-
 var maxImpsPerReq = 10
 
 func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
@@ -455,9 +399,12 @@ func appendMemberId(uri string, memberId string) string {
 // Builder builds a new instance of the AppNexus 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{
-		URI:            config.Endpoint,
-		iabCategoryMap: loadCategoryMapFromFileSystem(),
-		hbSource:       resolvePlatformID(config.PlatformID),
+		URI: config.Endpoint,
+		iabCategoryMap: map[string]string{
+			"1": "IAB20-3",
+			"9": "IAB5-3",
+		},
+		hbSource: resolvePlatformID(config.PlatformID),
 	}
 	return bidder, nil
 }
@@ -471,17 +418,3 @@ func resolvePlatformID(platformID string) int {
 
 	return defaultPlatformID
 }
-
-func loadCategoryMapFromFileSystem() map[string]string {
-	// Load custom options for our adapter (currently just a lookup table to convert appnexus => iab categories)
-	opts, err := os.ReadFile("./static/adapter/appnexus/opts.json")
-	if err == nil {
-		var adapterOptions appnexusAdapterOptions
-
-		if err := json.Unmarshal(opts, &adapterOptions); err == nil {
-			return adapterOptions.IabCategories
-		}
-	}
-
-	return nil
-}
diff --git a/adapters/appnexus/models.go b/adapters/appnexus/models.go
new file mode 100644
index 00000000..a3f6be99
--- /dev/null
+++ b/adapters/appnexus/models.go
@@ -0,0 +1,53 @@
+package appnexus
+
+import (
+	"encoding/json"
+
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+type appnexusImpExtAppnexus struct {
+	PlacementID       int             `json:"placement_id,omitempty"`
+	Keywords          string          `json:"keywords,omitempty"`
+	TrafficSourceCode string          `json:"traffic_source_code,omitempty"`
+	UsePmtRule        *bool           `json:"use_pmt_rule,omitempty"`
+	PrivateSizes      json.RawMessage `json:"private_sizes,omitempty"`
+}
+
+type appnexusImpExt struct {
+	Appnexus appnexusImpExtAppnexus `json:"appnexus"`
+}
+
+type appnexusBidExtVideo struct {
+	Duration int `json:"duration"`
+}
+
+type appnexusBidExtCreative struct {
+	Video appnexusBidExtVideo `json:"video"`
+}
+
+type appnexusBidExtAppnexus struct {
+	BidType       int                    `json:"bid_ad_type"`
+	BrandId       int                    `json:"brand_id"`
+	BrandCategory int                    `json:"brand_category_id"`
+	CreativeInfo  appnexusBidExtCreative `json:"creative_info"`
+	DealPriority  int                    `json:"deal_priority"`
+}
+
+type appnexusBidExt struct {
+	Appnexus appnexusBidExtAppnexus `json:"appnexus"`
+}
+
+type appnexusReqExtAppnexus struct {
+	IncludeBrandCategory    *bool  `json:"include_brand_category,omitempty"`
+	BrandCategoryUniqueness *bool  `json:"brand_category_uniqueness,omitempty"`
+	IsAMP                   int    `json:"is_amp,omitempty"`
+	HeaderBiddingSource     int    `json:"hb_source,omitempty"`
+	AdPodId                 string `json:"adpod_id,omitempty"`
+}
+
+// Full request extension including appnexus extension object
+type appnexusReqExt struct {
+	openrtb_ext.ExtRequest
+	Appnexus *appnexusReqExtAppnexus `json:"appnexus,omitempty"`
+}
diff --git a/adapters/appnexus/static/adapter/appnexus/opts.json b/adapters/appnexus/static/adapter/appnexus/opts.json
deleted file mode 100644
index 0050549e..00000000
--- a/adapters/appnexus/static/adapter/appnexus/opts.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-    "iab_categories": {
-        "1": "IAB20-3",
-        "9": "IAB5-3"
-    }
-}
diff --git a/adapters/criteo/criteo.go b/adapters/criteo/criteo.go
index 66e2a547..0ce7a14f 100644
--- a/adapters/criteo/criteo.go
+++ b/adapters/criteo/criteo.go
@@ -64,11 +64,19 @@ func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.R
 
 	bidResponse := adapters.NewBidderResponse()
 	bidResponse.Currency = response.Cur
+
 	for _, seatBid := range response.SeatBid {
 		for i := range seatBid.Bid {
+			var bidExt openrtb_ext.ExtBid
+			if err := json.Unmarshal(seatBid.Bid[i].Ext, &bidExt); err != nil {
+				return nil, []error{&errortypes.BadServerResponse{
+					Message: fmt.Sprintf("Missing ext.prebid.type in bid for impression : %s.", seatBid.Bid[i].ImpID),
+				}}
+			}
+
 			b := &adapters.TypedBid{
 				Bid:     &seatBid.Bid[i],
-				BidType: openrtb_ext.BidTypeBanner,
+				BidType: bidExt.Prebid.Type,
 			}
 			bidResponse.Bids = append(bidResponse.Bids, b)
 		}
diff --git a/config/accounts.go b/config/accounts.go
index b5c96dd3..2a9deb49 100644
--- a/config/accounts.go
+++ b/config/accounts.go
@@ -37,6 +37,7 @@ type Account struct {
 	Hooks                   AccountHooks                         `mapstructure:"hooks" json:"hooks"`
 	PriceFloors             AccountPriceFloors                   `mapstructure:"price_floors" json:"price_floors"`
 	Validations             Validations                          `mapstructure:"validations" json:"validations"`
+	DefaultBidLimit         int                                  `mapstructure:"default_bid_limit" json:"default_bid_limit"`
 }
 
 // CookieSync represents the account-level defaults for the cookie sync endpoint.
diff --git a/endpoints/cookie_sync.go b/endpoints/cookie_sync.go
index 365606a5..4b043bdd 100644
--- a/endpoints/cookie_sync.go
+++ b/endpoints/cookie_sync.go
@@ -100,7 +100,7 @@ func (c *cookieSyncEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ ht
 		c.handleResponse(w, request.SyncTypeFilter, cookie, privacyPolicies, nil)
 	case usersync.StatusOK:
 		c.metrics.RecordCookieSync(metrics.CookieSyncOK)
-		c.writeBidderMetrics(result.BiddersEvaluated)
+		c.writeSyncerMetrics(result.BiddersEvaluated)
 		c.handleResponse(w, request.SyncTypeFilter, cookie, privacyPolicies, result.SyncersChosen)
 	}
 }
@@ -323,7 +323,7 @@ func combineErrors(errs []error) error {
 	return errors.New(combinedErrors)
 }
 
-func (c *cookieSyncEndpoint) writeBidderMetrics(biddersEvaluated []usersync.BidderEvaluation) {
+func (c *cookieSyncEndpoint) writeSyncerMetrics(biddersEvaluated []usersync.BidderEvaluation) {
 	for _, bidder := range biddersEvaluated {
 		switch bidder.Status {
 		case usersync.StatusOK:
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index 493f927e..13b27898 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -716,8 +716,8 @@ func (deps *endpointDeps) validateRequest(req *openrtb_ext.RequestWrapper, isAmp
 		return append(errL, errors.New("request.site or request.app must be defined, but not both."))
 	}
 
-	if err := validateRequestExt(req); err != nil {
-		return append(errL, err)
+	if errs := validateRequestExt(req); len(errs) != 0 {
+		return append(errL, errs...)
 	}
 
 	if err := deps.validateSite(req); err != nil {
@@ -1497,14 +1497,13 @@ func (deps *endpointDeps) validateAliasesGVLIDs(aliasesGVLIDs map[string]uint16,
 	return nil
 }
 
-func validateRequestExt(req *openrtb_ext.RequestWrapper) error {
+func validateRequestExt(req *openrtb_ext.RequestWrapper) []error {
 	reqExt, err := req.GetRequestExt()
 	if err != nil {
-		return err
+		return []error{err}
 	}
 
 	prebid := reqExt.GetPrebid()
-
 	// exit early if there is no request.ext.prebid to validate
 	if prebid == nil {
 		return nil
@@ -1512,15 +1511,31 @@ func validateRequestExt(req *openrtb_ext.RequestWrapper) error {
 
 	if prebid.Cache != nil {
 		if prebid.Cache.Bids == nil && prebid.Cache.VastXML == nil {
-			return errors.New(`request.ext is invalid: request.ext.prebid.cache requires one of the "bids" or "vastxml" properties`)
+			return []error{errors.New(`request.ext is invalid: request.ext.prebid.cache requires one of the "bids" or "vastxml" properties`)}
 		}
 	}
 
 	if err := validateTargeting(prebid.Targeting); err != nil {
-		return err
+		return []error{err}
 	}
 
-	return nil
+	var errs []error
+	if prebid.MultiBid != nil {
+		validatedMultiBids, multBidErrs := openrtb_ext.ValidateAndBuildExtMultiBid(prebid)
+
+		for _, err := range multBidErrs {
+			errs = append(errs, &errortypes.Warning{
+				WarningCode: errortypes.MultiBidWarningCode,
+				Message:     err.Error(),
+			})
+		}
+
+		// update the downstream multibid to avoid passing unvalidated ext to bidders, etc.
+		prebid.MultiBid = validatedMultiBids
+		reqExt.SetPrebid(prebid)
+	}
+
+	return errs
 }
 
 func validateTargeting(t *openrtb_ext.ExtRequestTargeting) error {
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/multi-bid-error.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/multi-bid-error.json
new file mode 100644
index 00000000..c4add6f0
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/multi-bid-error.json
@@ -0,0 +1,111 @@
+{
+  "description": "Well formed amp request with valid Site field",
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "site": {
+      "page": "test.somepage.com"
+    },
+    "imp": [
+      {
+        "id": "my-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "pmp": {
+          "deals": [
+            {
+              "id": "some-deal-id"
+            }
+          ]
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          }
+        }
+      }
+    ],
+    "ext": {
+      "prebid": {
+        "multibid": [
+          {
+            "bidder": "appnexus"
+          },
+          {
+            "maxbids": -1,
+            "bidder": "rubicon",
+            "targetbiddercodeprefix": "rubN"
+          },
+          {
+            "maxbids": 10,
+            "bidder": "pubmatic",
+            "targetbiddercodeprefix": "pm"
+          },
+          {
+            "maxbids": 4,
+            "bidder": "pubmatic",
+            "targetbiddercodeprefix": "pubM"
+          },
+          {
+            "maxbids": 5,
+            "bidder": "groupm",
+            "bidders": [
+              "someBidder"
+            ],
+            "targetbiddercodeprefix": "gm"
+          },
+          {
+            "maxbids": 6,
+            "bidders": [
+              "groupm"
+            ]
+          },
+          {
+            "maxbids": 7,
+            "bidders": [
+              "33across"
+            ],
+            "targetbiddercodeprefix": "abc"
+          },
+          {
+            "maxbids": 8,
+            "targetbiddercodeprefix": "xyz"
+          }
+        ]
+      }
+    }
+  },
+  "expectedReqExt": {
+    "prebid": {
+      "multibid": [
+        {
+          "maxbids": 1,
+          "bidder": "rubicon",
+          "targetbiddercodeprefix": "rubN"
+        },
+        {
+          "maxbids": 9,
+          "bidder": "pubmatic",
+          "targetbiddercodeprefix": "pm"
+        },
+        {
+          "maxbids": 5,
+          "bidder": "groupm",
+          "targetbiddercodeprefix": "gm"
+        },
+        {
+          "maxbids": 7,
+          "bidders": [
+            "33across"
+          ]
+        }
+      ]
+    }
+  },
+  "expectedReturnCode": 200
+}
diff --git a/errortypes/code.go b/errortypes/code.go
index d6e081d2..e796a05d 100644
--- a/errortypes/code.go
+++ b/errortypes/code.go
@@ -24,6 +24,7 @@ const (
 	BidderLevelDebugDisabledWarningCode
 	DisabledCurrencyConversionWarningCode
 	AlternateBidderCodeWarningCode
+	MultiBidWarningCode
 )
 
 // Coder provides an error or warning code with severity.
diff --git a/exchange/auction.go b/exchange/auction.go
index 3c6f28f9..4cdbcca1 100644
--- a/exchange/auction.go
+++ b/exchange/auction.go
@@ -7,6 +7,7 @@ import (
 	"errors"
 	"fmt"
 	"regexp"
+	"sort"
 	"strings"
 	"time"
 
@@ -107,24 +108,22 @@ func (d *DebugLog) PutDebugLogError(cache prebid_cache_client.Client, timeout in
 
 func newAuction(seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, numImps int, preferDeals bool) *auction {
 	winningBids := make(map[string]*entities.PbsOrtbBid, numImps)
-	winningBidsByBidder := make(map[string]map[openrtb_ext.BidderName]*entities.PbsOrtbBid, numImps)
+	winningBidsByBidder := make(map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid, numImps)
 
 	for bidderName, seatBid := range seatBids {
 		if seatBid != nil {
 			for _, bid := range seatBid.Bids {
-				cpm := bid.Bid.Price
 				wbid, ok := winningBids[bid.Bid.ImpID]
 				if !ok || isNewWinningBid(bid.Bid, wbid.Bid, preferDeals) {
 					winningBids[bid.Bid.ImpID] = bid
 				}
+
 				if bidMap, ok := winningBidsByBidder[bid.Bid.ImpID]; ok {
-					bestSoFar, ok := bidMap[bidderName]
-					if !ok || cpm > bestSoFar.Bid.Price {
-						bidMap[bidderName] = bid
-					}
+					bidMap[bidderName] = append(bidMap[bidderName], bid)
 				} else {
-					winningBidsByBidder[bid.Bid.ImpID] = make(map[openrtb_ext.BidderName]*entities.PbsOrtbBid)
-					winningBidsByBidder[bid.Bid.ImpID][bidderName] = bid
+					winningBidsByBidder[bid.Bid.ImpID] = map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+						bidderName: {bid},
+					}
 				}
 			}
 		}
@@ -149,11 +148,50 @@ func isNewWinningBid(bid, wbid *openrtb2.Bid, preferDeals bool) bool {
 	return bid.Price > wbid.Price
 }
 
+func (a *auction) validateAndUpdateMultiBid(adapterBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, preferDeals bool, accountDefaultBidLimit int) {
+	bidsSnipped := false
+	// sort bids for multibid targeting
+	for _, topBidsPerBidder := range a.winningBidsByBidder {
+		for bidder, topBids := range topBidsPerBidder {
+			sort.Slice(topBids, func(i, j int) bool {
+				return isNewWinningBid(topBids[i].Bid, topBids[j].Bid, preferDeals)
+			})
+
+			// assert hard limit on bids count per imp, per adapter.
+			if accountDefaultBidLimit != 0 && len(topBids) > accountDefaultBidLimit {
+				for i := accountDefaultBidLimit; i < len(topBids); i++ {
+					topBids[i].Bid = nil
+					topBids[i] = nil
+					bidsSnipped = true
+				}
+
+				topBidsPerBidder[bidder] = topBids[:accountDefaultBidLimit]
+			}
+		}
+	}
+
+	if bidsSnipped { // remove the marked bids from original references
+		for _, seatBid := range adapterBids {
+			if seatBid != nil {
+				bids := make([]*entities.PbsOrtbBid, 0, accountDefaultBidLimit)
+				for i := 0; i < len(seatBid.Bids); i++ {
+					if seatBid.Bids[i].Bid != nil {
+						bids = append(bids, seatBid.Bids[i])
+					}
+				}
+				seatBid.Bids = bids
+			}
+		}
+	}
+}
+
 func (a *auction) setRoundedPrices(priceGranularity openrtb_ext.PriceGranularity) {
 	roundedPrices := make(map[*entities.PbsOrtbBid]string, 5*len(a.winningBids))
 	for _, topBidsPerImp := range a.winningBidsByBidder {
-		for _, topBidPerBidder := range topBidsPerImp {
-			roundedPrices[topBidPerBidder] = GetPriceBucket(topBidPerBidder.Bid.Price, priceGranularity)
+		for _, topBidsPerBidder := range topBidsPerImp {
+			for _, topBid := range topBidsPerBidder {
+				roundedPrices[topBid] = GetPriceBucket(topBid.Bid.Price, priceGranularity)
+			}
 		}
 	}
 	a.roundedPrices = roundedPrices
@@ -187,64 +225,65 @@ func (a *auction) doCache(ctx context.Context, cache prebid_cache_client.Client,
 	for _, imp := range bidRequest.Imp {
 		expByImp[imp.ID] = imp.Exp
 	}
-	for _, topBidsPerImp := range a.winningBidsByBidder {
-		for bidderName, topBidPerBidder := range topBidsPerImp {
-			impID := topBidPerBidder.Bid.ImpID
-			isOverallWinner := a.winningBids[impID] == topBidPerBidder
-			if !includeBidderKeys && !isOverallWinner {
-				continue
-			}
-			var customCacheKey string
-			var catDur string
-			useCustomCacheKey := false
-			if competitiveExclusion && isOverallWinner || includeBidderKeys {
-				// set custom cache key for winning bid when competitive exclusion applies
-				catDur = bidCategory[topBidPerBidder.Bid.ID]
-				if len(catDur) > 0 {
-					customCacheKey = fmt.Sprintf("%s_%s", catDur, hbCacheID)
-					useCustomCacheKey = true
+	for impID, topBidsPerImp := range a.winningBidsByBidder {
+		for bidderName, topBidsPerBidder := range topBidsPerImp {
+			for _, topBid := range topBidsPerBidder {
+				isOverallWinner := a.winningBids[impID] == topBid
+				if !includeBidderKeys && !isOverallWinner {
+					continue
 				}
-			}
-			if bids {
-				if jsonBytes, err := json.Marshal(topBidPerBidder.Bid); err == nil {
-					jsonBytes, err = evTracking.modifyBidJSON(topBidPerBidder, bidderName, jsonBytes)
-					if err != nil {
-						errs = append(errs, err)
+				var customCacheKey string
+				var catDur string
+				useCustomCacheKey := false
+				if competitiveExclusion && isOverallWinner || includeBidderKeys {
+					// set custom cache key for winning bid when competitive exclusion applies
+					catDur = bidCategory[topBid.Bid.ID]
+					if len(catDur) > 0 {
+						customCacheKey = fmt.Sprintf("%s_%s", catDur, hbCacheID)
+						useCustomCacheKey = true
 					}
-					if useCustomCacheKey {
-						// not allowed if bids is true; log error and cache normally
-						errs = append(errs, errors.New("cannot use custom cache key for non-vast Bids"))
-					}
-					toCache = append(toCache, prebid_cache_client.Cacheable{
-						Type:       prebid_cache_client.TypeJSON,
-						Data:       jsonBytes,
-						TTLSeconds: cacheTTL(expByImp[impID], topBidPerBidder.Bid.Exp, defTTL(topBidPerBidder.BidType, defaultTTLs), ttlBuffer),
-					})
-					bidIndices[len(toCache)-1] = topBidPerBidder.Bid
-				} else {
-					errs = append(errs, err)
 				}
-			}
-			if vast && topBidPerBidder.BidType == openrtb_ext.BidTypeVideo {
-				vastXML := makeVAST(topBidPerBidder.Bid)
-				if jsonBytes, err := json.Marshal(vastXML); err == nil {
-					if useCustomCacheKey {
+				if bids {
+					if jsonBytes, err := json.Marshal(topBid.Bid); err == nil {
+						jsonBytes, err = evTracking.modifyBidJSON(topBid, bidderName, jsonBytes)
+						if err != nil {
+							errs = append(errs, err)
+						}
+						if useCustomCacheKey {
+							// not allowed if bids is true; log error and cache normally
+							errs = append(errs, errors.New("cannot use custom cache key for non-vast bids"))
+						}
 						toCache = append(toCache, prebid_cache_client.Cacheable{
-							Type:       prebid_cache_client.TypeXML,
+							Type:       prebid_cache_client.TypeJSON,
 							Data:       jsonBytes,
-							TTLSeconds: cacheTTL(expByImp[impID], topBidPerBidder.Bid.Exp, defTTL(topBidPerBidder.BidType, defaultTTLs), ttlBuffer),
-							Key:        customCacheKey,
+							TTLSeconds: cacheTTL(expByImp[impID], topBid.Bid.Exp, defTTL(topBid.BidType, defaultTTLs), ttlBuffer),
 						})
+						bidIndices[len(toCache)-1] = topBid.Bid
 					} else {
-						toCache = append(toCache, prebid_cache_client.Cacheable{
-							Type:       prebid_cache_client.TypeXML,
-							Data:       jsonBytes,
-							TTLSeconds: cacheTTL(expByImp[impID], topBidPerBidder.Bid.Exp, defTTL(topBidPerBidder.BidType, defaultTTLs), ttlBuffer),
-						})
+						errs = append(errs, err)
+					}
+				}
+				if vast && topBid.BidType == openrtb_ext.BidTypeVideo {
+					vastXML := makeVAST(topBid.Bid)
+					if jsonBytes, err := json.Marshal(vastXML); err == nil {
+						if useCustomCacheKey {
+							toCache = append(toCache, prebid_cache_client.Cacheable{
+								Type:       prebid_cache_client.TypeXML,
+								Data:       jsonBytes,
+								TTLSeconds: cacheTTL(expByImp[impID], topBid.Bid.Exp, defTTL(topBid.BidType, defaultTTLs), ttlBuffer),
+								Key:        customCacheKey,
+							})
+						} else {
+							toCache = append(toCache, prebid_cache_client.Cacheable{
+								Type:       prebid_cache_client.TypeXML,
+								Data:       jsonBytes,
+								TTLSeconds: cacheTTL(expByImp[impID], topBid.Bid.Exp, defTTL(topBid.BidType, defaultTTLs), ttlBuffer),
+							})
+						}
+						vastIndices[len(toCache)-1] = topBid.Bid
+					} else {
+						errs = append(errs, err)
 					}
-					vastIndices[len(toCache)-1] = topBidPerBidder.Bid
-				} else {
-					errs = append(errs, err)
 				}
 			}
 		}
@@ -356,7 +395,7 @@ type auction struct {
 	// winningBids is a map from imp.id to the highest overall CPM bid in that imp.
 	winningBids map[string]*entities.PbsOrtbBid
 	// winningBidsByBidder stores the highest bid on each imp by each bidder.
-	winningBidsByBidder map[string]map[openrtb_ext.BidderName]*entities.PbsOrtbBid
+	winningBidsByBidder map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid
 	// roundedPrices stores the price strings rounded for each bid according to the price granularity.
 	roundedPrices map[*entities.PbsOrtbBid]string
 	// cacheIds stores the UUIDs from Prebid Cache for fetching the full bid JSON.
diff --git a/exchange/entities/entities.go b/exchange/entities/entities.go
index d535af00..a0c39649 100644
--- a/exchange/entities/entities.go
+++ b/exchange/entities/entities.go
@@ -47,4 +47,5 @@ type PbsOrtbBid struct {
 	GeneratedBidID    string
 	OriginalBidCPM    float64
 	OriginalBidCur    string
+	TargetBidderCode  string
 }
diff --git a/exchange/exchange.go b/exchange/exchange.go
index d477701e..9438d71f 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -365,10 +365,11 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *
 		if targData != nil {
 			// A non-nil auction is only needed if targeting is active. (It is used below this block to extract cache keys)
 			auc = newAuction(adapterBids, len(r.BidRequestWrapper.Imp), targData.preferDeals)
+			auc.validateAndUpdateMultiBid(adapterBids, targData.preferDeals, r.Account.DefaultBidLimit)
 			auc.setRoundedPrices(targData.priceGranularity)
 
 			if requestExt.Prebid.SupportDeals {
-				dealErrs := applyDealSupport(r.BidRequestWrapper.BidRequest, auc, bidCategory)
+				dealErrs := applyDealSupport(r.BidRequestWrapper.BidRequest, auc, bidCategory, requestExt.Prebid.MultiBidMap)
 				errs = append(errs, dealErrs...)
 			}
 
@@ -387,8 +388,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *
 				errs = append(errs, cacheErrs...)
 			}
 
-			targData.setTargeting(auc, r.BidRequestWrapper.BidRequest.App != nil, bidCategory, r.Account.TruncateTargetAttribute)
-
+			targData.setTargeting(auc, r.BidRequestWrapper.BidRequest.App != nil, bidCategory, r.Account.TruncateTargetAttribute, requestExt.Prebid.MultiBidMap)
 		}
 		bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, r, responseDebugAllow, requestExt.Prebid.Passthrough, fledge, errs)
 	} else {
@@ -464,18 +464,25 @@ func recordImpMetrics(bidRequest *openrtb2.BidRequest, metricsEngine metrics.Met
 }
 
 // applyDealSupport updates targeting keys with deal prefixes if minimum deal tier exceeded
-func applyDealSupport(bidRequest *openrtb2.BidRequest, auc *auction, bidCategory map[string]string) []error {
+func applyDealSupport(bidRequest *openrtb2.BidRequest, auc *auction, bidCategory map[string]string, multiBid map[string]openrtb_ext.ExtMultiBid) []error {
 	errs := []error{}
 	impDealMap := getDealTiers(bidRequest)
 
 	for impID, topBidsPerImp := range auc.winningBidsByBidder {
 		impDeal := impDealMap[impID]
-		for bidder, topBidPerBidder := range topBidsPerImp {
-			if topBidPerBidder.DealPriority > 0 {
-				if validateDealTier(impDeal[bidder]) {
-					updateHbPbCatDur(topBidPerBidder, impDeal[bidder], bidCategory)
-				} else {
-					errs = append(errs, fmt.Errorf("dealTier configuration invalid for bidder '%s', imp ID '%s'", string(bidder), impID))
+		for bidder, topBidsPerBidder := range topBidsPerImp {
+			maxBid := bidsToUpdate(multiBid, bidder.String())
+
+			for i, topBid := range topBidsPerBidder {
+				if i == maxBid {
+					break
+				}
+				if topBid.DealPriority > 0 {
+					if validateDealTier(impDeal[bidder]) {
+						updateHbPbCatDur(topBid, impDeal[bidder], bidCategory)
+					} else {
+						errs = append(errs, fmt.Errorf("dealTier configuration invalid for bidder '%s', imp ID '%s'", string(bidder), impID))
+					}
 				}
 			}
 		}
@@ -484,6 +491,18 @@ func applyDealSupport(bidRequest *openrtb2.BidRequest, auc *auction, bidCategory
 	return errs
 }
 
+// By default, update 1 bid,
+// For 2nd and the following bids, updateHbPbCatDur only if this bidder's multibid config is fully defined.
+func bidsToUpdate(multiBid map[string]openrtb_ext.ExtMultiBid, bidder string) int {
+	if multiBid != nil {
+		if bidderMultiBid, ok := multiBid[bidder]; ok && bidderMultiBid.TargetBidderCodePrefix != "" {
+			return *bidderMultiBid.MaxBids
+		}
+	}
+
+	return openrtb_ext.DefaultBidLimit
+}
+
 // getDealTiers creates map of impression to bidder deal tier configuration
 func getDealTiers(bidRequest *openrtb2.BidRequest) map[string]openrtb_ext.DealTierBidderMap {
 	impDealMap := make(map[string]openrtb_ext.DealTierBidderMap)
@@ -1117,6 +1136,7 @@ func (e *exchange) makeBid(bids []*entities.PbsOrtbBid, auc *auction, returnCrea
 			Meta:              bid.BidMeta,
 			Video:             bid.BidVideo,
 			BidId:             bid.GeneratedBidID,
+			TargetBidderCode:  bid.TargetBidderCode,
 		}
 
 		if cacheInfo, found := e.getBidCacheInfo(bid, auc); found {
diff --git a/exchange/targeting.go b/exchange/targeting.go
index b636b2b9..cbf1152d 100644
--- a/exchange/targeting.go
+++ b/exchange/targeting.go
@@ -1,6 +1,7 @@
 package exchange
 
 import (
+	"fmt"
 	"strconv"
 
 	"github.com/prebid/openrtb/v17/openrtb2"
@@ -35,49 +36,67 @@ type targetData struct {
 // The one exception is the `hb_cache_id` key. Since our APIs explicitly document cache keys to be on a "best effort" basis,
 // it's ok if those stay in the auction. For now, this method implements a very naive cache strategy.
 // In the future, we should implement a more clever retry & backoff strategy to balance the success rate & performance.
-func (targData *targetData) setTargeting(auc *auction, isApp bool, categoryMapping map[string]string, truncateTargetAttr *int) {
+func (targData *targetData) setTargeting(auc *auction, isApp bool, categoryMapping map[string]string, truncateTargetAttr *int, multiBidMap map[string]openrtb_ext.ExtMultiBid) {
 	for impId, topBidsPerImp := range auc.winningBidsByBidder {
 		overallWinner := auc.winningBids[impId]
-		for bidderName, topBidPerBidder := range topBidsPerImp {
-			isOverallWinner := overallWinner == topBidPerBidder
+		for originalBidderName, topBidsPerBidder := range topBidsPerImp {
+			targetingBidderCode := originalBidderName
+			bidderCodePrefix, maxBids := getMultiBidMeta(multiBidMap, originalBidderName.String())
 
-			targets := make(map[string]string, 10)
-			if cpm, ok := auc.roundedPrices[topBidPerBidder]; ok {
-				targData.addKeys(targets, openrtb_ext.HbpbConstantKey, cpm, bidderName, isOverallWinner, truncateTargetAttr)
-			}
-			targData.addKeys(targets, openrtb_ext.HbBidderConstantKey, string(bidderName), bidderName, isOverallWinner, truncateTargetAttr)
-			if hbSize := makeHbSize(topBidPerBidder.Bid); hbSize != "" {
-				targData.addKeys(targets, openrtb_ext.HbSizeConstantKey, hbSize, bidderName, isOverallWinner, truncateTargetAttr)
-			}
-			if cacheID, ok := auc.cacheIds[topBidPerBidder.Bid]; ok {
-				targData.addKeys(targets, openrtb_ext.HbCacheKey, cacheID, bidderName, isOverallWinner, truncateTargetAttr)
-			}
-			if vastID, ok := auc.vastCacheIds[topBidPerBidder.Bid]; ok {
-				targData.addKeys(targets, openrtb_ext.HbVastCacheKey, vastID, bidderName, isOverallWinner, truncateTargetAttr)
-			}
-			if targData.includeFormat {
-				targData.addKeys(targets, openrtb_ext.HbFormatKey, string(topBidPerBidder.BidType), bidderName, isOverallWinner, truncateTargetAttr)
-			}
+			for i, topBid := range topBidsPerBidder {
+				// Limit targeting keys to maxBids (default 1 bid).
+				// And, do not apply targeting for more than 1 bid if bidderCodePrefix is not defined.
+				if i == maxBids || (i == 1 && bidderCodePrefix == "") {
+					break
+				}
 
-			if targData.cacheHost != "" {
-				targData.addKeys(targets, openrtb_ext.HbConstantCacheHostKey, targData.cacheHost, bidderName, isOverallWinner, truncateTargetAttr)
-			}
-			if targData.cachePath != "" {
-				targData.addKeys(targets, openrtb_ext.HbConstantCachePathKey, targData.cachePath, bidderName, isOverallWinner, truncateTargetAttr)
-			}
+				if i > 0 { // bidderCode is used for first bid, generated bidderCodePrefix for following bids
+					targetingBidderCode = openrtb_ext.BidderName(fmt.Sprintf("%s%d", bidderCodePrefix, i+1))
+				}
 
-			if deal := topBidPerBidder.Bid.DealID; len(deal) > 0 {
-				targData.addKeys(targets, openrtb_ext.HbDealIDConstantKey, deal, bidderName, isOverallWinner, truncateTargetAttr)
-			}
+				if maxBids > openrtb_ext.DefaultBidLimit { // add targetingbiddercode only if multibid is set for this bidder
+					topBid.TargetBidderCode = targetingBidderCode.String()
+				}
 
-			if isApp {
-				targData.addKeys(targets, openrtb_ext.HbEnvKey, openrtb_ext.HbEnvKeyApp, bidderName, isOverallWinner, truncateTargetAttr)
-			}
-			if len(categoryMapping) > 0 {
-				targData.addKeys(targets, openrtb_ext.HbCategoryDurationKey, categoryMapping[topBidPerBidder.Bid.ID], bidderName, isOverallWinner, truncateTargetAttr)
-			}
+				isOverallWinner := overallWinner == topBid
 
-			topBidPerBidder.BidTargets = targets
+				targets := make(map[string]string, 10)
+				if cpm, ok := auc.roundedPrices[topBid]; ok {
+					targData.addKeys(targets, openrtb_ext.HbpbConstantKey, cpm, targetingBidderCode, isOverallWinner, truncateTargetAttr)
+				}
+				targData.addKeys(targets, openrtb_ext.HbBidderConstantKey, string(targetingBidderCode), targetingBidderCode, isOverallWinner, truncateTargetAttr)
+				if hbSize := makeHbSize(topBid.Bid); hbSize != "" {
+					targData.addKeys(targets, openrtb_ext.HbSizeConstantKey, hbSize, targetingBidderCode, isOverallWinner, truncateTargetAttr)
+				}
+				if cacheID, ok := auc.cacheIds[topBid.Bid]; ok {
+					targData.addKeys(targets, openrtb_ext.HbCacheKey, cacheID, targetingBidderCode, isOverallWinner, truncateTargetAttr)
+				}
+				if vastID, ok := auc.vastCacheIds[topBid.Bid]; ok {
+					targData.addKeys(targets, openrtb_ext.HbVastCacheKey, vastID, targetingBidderCode, isOverallWinner, truncateTargetAttr)
+				}
+				if targData.includeFormat {
+					targData.addKeys(targets, openrtb_ext.HbFormatKey, string(topBid.BidType), targetingBidderCode, isOverallWinner, truncateTargetAttr)
+				}
+
+				if targData.cacheHost != "" {
+					targData.addKeys(targets, openrtb_ext.HbConstantCacheHostKey, targData.cacheHost, targetingBidderCode, isOverallWinner, truncateTargetAttr)
+				}
+				if targData.cachePath != "" {
+					targData.addKeys(targets, openrtb_ext.HbConstantCachePathKey, targData.cachePath, targetingBidderCode, isOverallWinner, truncateTargetAttr)
+				}
+
+				if deal := topBid.Bid.DealID; len(deal) > 0 {
+					targData.addKeys(targets, openrtb_ext.HbDealIDConstantKey, deal, targetingBidderCode, isOverallWinner, truncateTargetAttr)
+				}
+
+				if isApp {
+					targData.addKeys(targets, openrtb_ext.HbEnvKey, openrtb_ext.HbEnvKeyApp, targetingBidderCode, isOverallWinner, truncateTargetAttr)
+				}
+				if len(categoryMapping) > 0 {
+					targData.addKeys(targets, openrtb_ext.HbCategoryDurationKey, categoryMapping[topBid.Bid.ID], targetingBidderCode, isOverallWinner, truncateTargetAttr)
+				}
+				topBid.BidTargets = targets
+			}
 		}
 	}
 }
@@ -106,3 +125,13 @@ func makeHbSize(bid *openrtb2.Bid) string {
 	}
 	return ""
 }
+
+func getMultiBidMeta(multiBidMap map[string]openrtb_ext.ExtMultiBid, bidder string) (string, int) {
+	if multiBidMap != nil {
+		if multiBid, ok := multiBidMap[bidder]; ok {
+			return multiBid.TargetBidderCodePrefix, *multiBid.MaxBids
+		}
+	}
+
+	return "", openrtb_ext.DefaultBidLimit
+}
diff --git a/exchange/utils.go b/exchange/utils.go
index d9c1c022..e825c198 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -318,6 +318,7 @@ func buildRequestExtForBidder(bidder string, requestExt json.RawMessage, request
 		prebid.Channel = requestExtParsed.Prebid.Channel
 		prebid.Debug = requestExtParsed.Prebid.Debug
 		prebid.Server = requestExtParsed.Prebid.Server
+		prebid.MultiBid = buildRequestExtMultiBid(bidder, requestExtParsed.Prebid.MultiBid, alternateBidderCodes)
 	}
 
 	// Marshal New Prebid Object
@@ -366,6 +367,45 @@ func buildRequestExtAlternateBidderCodes(bidder string, accABC *openrtb_ext.ExtA
 	return nil
 }
 
+func buildRequestExtMultiBid(adapter string, reqMultiBid []*openrtb_ext.ExtMultiBid, adapterABC *openrtb_ext.ExtAlternateBidderCodes) []*openrtb_ext.ExtMultiBid {
+	adapterMultiBid := make([]*openrtb_ext.ExtMultiBid, 0)
+	for _, multiBid := range reqMultiBid {
+		if multiBid.Bidder != "" {
+			if multiBid.Bidder == adapter || isBidderInExtAlternateBidderCodes(adapter, multiBid.Bidder, adapterABC) {
+				adapterMultiBid = append(adapterMultiBid, multiBid)
+			}
+		} else {
+			for _, bidder := range multiBid.Bidders {
+				if bidder == adapter || isBidderInExtAlternateBidderCodes(adapter, bidder, adapterABC) {
+					adapterMultiBid = append(adapterMultiBid, &openrtb_ext.ExtMultiBid{
+						Bidders: []string{bidder},
+						MaxBids: multiBid.MaxBids,
+					})
+				}
+			}
+		}
+	}
+
+	if len(adapterMultiBid) > 0 {
+		return adapterMultiBid
+	}
+
+	return nil
+}
+
+func isBidderInExtAlternateBidderCodes(adapter, currentMultiBidBidder string, adapterABC *openrtb_ext.ExtAlternateBidderCodes) bool {
+	if adapterABC != nil {
+		if abc, ok := adapterABC.Bidders[adapter]; ok {
+			for _, bidder := range abc.AllowedBidderCodes {
+				if bidder == "*" || bidder == currentMultiBidBidder {
+					return true
+				}
+			}
+		}
+	}
+	return false
+}
+
 // extractBuyerUIDs parses the values from user.ext.prebid.buyeruids, and then deletes those values from the ext.
 // This prevents a Bidder from using these values to figure out who else is involved in the Auction.
 func extractBuyerUIDs(user *openrtb2.User) (map[string]string, error) {
@@ -693,6 +733,21 @@ func extractBidRequestExt(bidRequest *openrtb2.BidRequest) (*openrtb_ext.ExtRequ
 			return requestExt, fmt.Errorf("Error decoding Request.ext : %s", err.Error())
 		}
 	}
+
+	// validation already done in validateRequestExt(), directly build a map here for downstream processing
+	if requestExt.Prebid.MultiBid != nil {
+		requestExt.Prebid.MultiBidMap = make(map[string]openrtb_ext.ExtMultiBid)
+		for _, multiBid := range requestExt.Prebid.MultiBid {
+			if multiBid.Bidder != "" {
+				requestExt.Prebid.MultiBidMap[multiBid.Bidder] = *multiBid
+			} else {
+				for _, bidder := range multiBid.Bidders {
+					requestExt.Prebid.MultiBidMap[bidder] = *multiBid
+				}
+			}
+		}
+	}
+
 	return requestExt, nil
 }
 
diff --git a/openrtb_ext/bid.go b/openrtb_ext/bid.go
index 61c720fe..8bd2aca7 100644
--- a/openrtb_ext/bid.go
+++ b/openrtb_ext/bid.go
@@ -19,6 +19,7 @@ type ExtBidPrebid struct {
 	DealTierSatisfied bool                `json:"dealtiersatisfied,omitempty"`
 	Meta              *ExtBidPrebidMeta   `json:"meta,omitempty"`
 	Targeting         map[string]string   `json:"targeting,omitempty"`
+	TargetBidderCode  string              `json:"targetbiddercode,omitempty"`
 	Type              BidType             `json:"type"`
 	Video             *ExtBidPrebidVideo  `json:"video,omitempty"`
 	Events            *ExtBidPrebidEvents `json:"events,omitempty"`
diff --git a/openrtb_ext/multibid.go b/openrtb_ext/multibid.go
new file mode 100644
index 00000000..2e36931c
--- /dev/null
+++ b/openrtb_ext/multibid.go
@@ -0,0 +1,85 @@
+package openrtb_ext
+
+import "fmt"
+
+const DefaultBidLimit = 1
+const MaxBidLimit = 9
+
+func ValidateAndBuildExtMultiBid(prebid *ExtRequestPrebid) ([]*ExtMultiBid, []error) {
+	if prebid == nil || prebid.MultiBid == nil {
+		return nil, nil
+	}
+
+	var validationErrs, errs []error
+	var validatedMultiBids, newMultiBids []*ExtMultiBid //returning slice instead of map to keep the downstream req.Ext payload consistent
+	multiBidMap := make(map[string]struct{})            // map is needed temporarily for validate of duplicate entries, etc.
+	for _, multiBid := range prebid.MultiBid {
+		newMultiBids, errs = addMultiBid(multiBidMap, multiBid)
+		if len(errs) != 0 {
+			validationErrs = append(validationErrs, errs...)
+		}
+		if len(newMultiBids) != 0 {
+			validatedMultiBids = append(validatedMultiBids, newMultiBids...)
+		}
+	}
+
+	return validatedMultiBids, validationErrs
+}
+
+// Validate and add multiBid
+func addMultiBid(multiBidMap map[string]struct{}, multiBid *ExtMultiBid) ([]*ExtMultiBid, []error) {
+	errs := make([]error, 0)
+
+	if multiBid.MaxBids == nil {
+		errs = append(errs, fmt.Errorf("maxBids not defined for %v", *multiBid))
+		return nil, errs
+	}
+
+	if *multiBid.MaxBids < DefaultBidLimit {
+		errs = append(errs, fmt.Errorf("invalid maxBids value, using minimum %d limit for %v", DefaultBidLimit, *multiBid))
+		*multiBid.MaxBids = DefaultBidLimit
+	}
+
+	if *multiBid.MaxBids > MaxBidLimit {
+		errs = append(errs, fmt.Errorf("invalid maxBids value, using maximum %d limit for %v", MaxBidLimit, *multiBid))
+		*multiBid.MaxBids = MaxBidLimit
+	}
+
+	var validatedMultiBids []*ExtMultiBid
+	if multiBid.Bidder != "" {
+		if _, ok := multiBidMap[multiBid.Bidder]; ok {
+			errs = append(errs, fmt.Errorf("multiBid already defined for %s, ignoring this instance %v", multiBid.Bidder, *multiBid))
+			return nil, errs
+		}
+
+		if multiBid.Bidders != nil {
+			errs = append(errs, fmt.Errorf("ignoring bidders from %v", *multiBid))
+			multiBid.Bidders = nil
+		}
+		multiBidMap[multiBid.Bidder] = struct{}{}
+		validatedMultiBids = append(validatedMultiBids, multiBid)
+	} else if len(multiBid.Bidders) > 0 {
+		var bidders []string
+		for _, bidder := range multiBid.Bidders {
+			if _, ok := multiBidMap[bidder]; ok {
+				errs = append(errs, fmt.Errorf("multiBid already defined for %s, ignoring this instance %v", bidder, *multiBid))
+				continue
+			}
+			multiBidMap[bidder] = struct{}{}
+			bidders = append(bidders, bidder)
+		}
+		if multiBid.TargetBidderCodePrefix != "" {
+			errs = append(errs, fmt.Errorf("ignoring targetbiddercodeprefix for %v", *multiBid))
+			multiBid.TargetBidderCodePrefix = ""
+		}
+		if len(bidders) != 0 {
+			validatedMultiBids = append(validatedMultiBids, &ExtMultiBid{
+				MaxBids: multiBid.MaxBids,
+				Bidders: bidders,
+			})
+		}
+	} else {
+		errs = append(errs, fmt.Errorf("bidder(s) not specified for %v", *multiBid))
+	}
+	return validatedMultiBids, errs
+}
diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go
index bc42769e..2edabcb2 100644
--- a/openrtb_ext/request.go
+++ b/openrtb_ext/request.go
@@ -2,6 +2,7 @@ package openrtb_ext
 
 import (
 	"encoding/json"
+	"fmt"
 
 	"github.com/prebid/openrtb/v17/openrtb2"
 )
@@ -71,6 +72,9 @@ type ExtRequestPrebid struct {
 	// - basic: excludes debugmessages and analytic_tags from output
 	// any other value or an empty string disables trace output at all.
 	Trace string `json:"trace,omitempty"`
+
+	MultiBid    []*ExtMultiBid         `json:"multibid,omitempty"`
+	MultiBidMap map[string]ExtMultiBid `json:"-"`
 }
 
 // Experiment defines if experimental features are available for the request
@@ -287,3 +291,18 @@ type ExtRequestPrebidDataEidPermission struct {
 	Source  string   `json:"source"`
 	Bidders []string `json:"bidders"`
 }
+
+type ExtMultiBid struct {
+	Bidder                 string   `json:"bidder,omitempty"`
+	Bidders                []string `json:"bidders,omitempty"`
+	MaxBids                *int     `json:"maxbids,omitempty"`
+	TargetBidderCodePrefix string   `json:"targetbiddercodeprefix,omitempty"`
+}
+
+func (m ExtMultiBid) String() string {
+	maxBid := "<nil>"
+	if m.MaxBids != nil {
+		maxBid = fmt.Sprintf("%d", *m.MaxBids)
+	}
+	return fmt.Sprintf("{Bidder:%s, Bidders:%v, MaxBids:%s, TargetBidderCodePrefix:%s}", m.Bidder, m.Bidders, maxBid, m.TargetBidderCodePrefix)
+}
diff --git a/static/bidder-info/criteo.yaml b/static/bidder-info/criteo.yaml
index 9c085cd1..6d6b5b61 100644
--- a/static/bidder-info/criteo.yaml
+++ b/static/bidder-info/criteo.yaml
@@ -6,9 +6,11 @@ capabilities:
   app:
     mediaTypes:
       - banner
+      - video
   site:
     mediaTypes:
       - banner
+      - video
 userSync:
   # criteo supports user syncing, but requires configuration by the host. contact this
   # bidder directly at the email address in this file to ask about enabling user sync.
diff --git a/usersync/chooser.go b/usersync/chooser.go
index d3718463..97fa1471 100644
--- a/usersync/chooser.go
+++ b/usersync/chooser.go
@@ -134,30 +134,30 @@ func (c standardChooser) Choose(request Request, cookie *Cookie) Result {
 func (c standardChooser) evaluate(bidder string, syncersSeen map[string]struct{}, syncTypeFilter SyncTypeFilter, privacy Privacy, cookie *Cookie) (Syncer, BidderEvaluation) {
 	syncer, exists := c.bidderSyncerLookup[bidder]
 	if !exists {
-		return nil, BidderEvaluation{Bidder: bidder, Status: StatusUnknownBidder}
+		return nil, BidderEvaluation{Status: StatusUnknownBidder, Bidder: bidder}
 	}
 
 	_, seen := syncersSeen[syncer.Key()]
 	if seen {
-		return nil, BidderEvaluation{Bidder: bidder, Status: StatusDuplicate}
+		return nil, BidderEvaluation{Status: StatusDuplicate, Bidder: bidder, SyncerKey: syncer.Key()}
 	}
 	syncersSeen[syncer.Key()] = struct{}{}
 
 	if !syncer.SupportsType(syncTypeFilter.ForBidder(bidder)) {
-		return nil, BidderEvaluation{Bidder: bidder, Status: StatusTypeNotSupported}
+		return nil, BidderEvaluation{Status: StatusTypeNotSupported, Bidder: bidder, SyncerKey: syncer.Key()}
 	}
 
 	if cookie.HasLiveSync(syncer.Key()) {
-		return nil, BidderEvaluation{Bidder: bidder, Status: StatusAlreadySynced}
+		return nil, BidderEvaluation{Status: StatusAlreadySynced, Bidder: bidder, SyncerKey: syncer.Key()}
 	}
 
 	if !privacy.GDPRAllowsBidderSync(bidder) {
-		return nil, BidderEvaluation{Bidder: bidder, Status: StatusBlockedByGDPR}
+		return nil, BidderEvaluation{Status: StatusBlockedByGDPR, Bidder: bidder, SyncerKey: syncer.Key()}
 	}
 
 	if !privacy.CCPAAllowsBidderSync(bidder) {
-		return nil, BidderEvaluation{Bidder: bidder, Status: StatusBlockedByCCPA}
+		return nil, BidderEvaluation{Status: StatusBlockedByCCPA, Bidder: bidder, SyncerKey: syncer.Key()}
 	}
 
-	return syncer, BidderEvaluation{Bidder: bidder, Status: StatusOK}
+	return syncer, BidderEvaluation{Status: StatusOK, Bidder: bidder, SyncerKey: syncer.Key()}
 }


Test patch:
diff --git a/adapters/criteo/criteotest/exemplary/simple-banner-cookie-uid.json b/adapters/criteo/criteotest/exemplary/simple-banner-cookie-uid.json
index fd81f2dc..bc6098c5 100755
--- a/adapters/criteo/criteotest/exemplary/simple-banner-cookie-uid.json
+++ b/adapters/criteo/criteotest/exemplary/simple-banner-cookie-uid.json
@@ -88,7 +88,12 @@
                   "crid": "creative-123",
                   "adm": "<iframe id='789abc' name='789abc' src='http://creative-url.criteo.com'></iframe>",
                   "w": 300,
-                  "h": 250
+                  "h": 250,
+                  "ext": {
+                    "prebid": {
+                      "type": "banner"
+                    }
+                  }
                 }
               ]
             }
@@ -109,7 +114,12 @@
             "crid": "creative-123",
             "adm": "<iframe id='789abc' name='789abc' src='http://creative-url.criteo.com'></iframe>",
             "w": 300,
-            "h": 250
+            "h": 250,
+            "ext": {
+              "prebid": {
+                "type": "banner"
+              }
+            }
           },
           "type": "banner"
         }
diff --git a/adapters/criteo/criteotest/exemplary/simple-banner-inapp.json b/adapters/criteo/criteotest/exemplary/simple-banner-inapp.json
index b86cb1ed..38ed5c45 100755
--- a/adapters/criteo/criteotest/exemplary/simple-banner-inapp.json
+++ b/adapters/criteo/criteotest/exemplary/simple-banner-inapp.json
@@ -81,7 +81,12 @@
                   "crid": "creative-123",
                   "adm": "<iframe id='789abc' name='789abc' src='http://creative-url.criteo.com'></iframe>",
                   "w": 300,
-                  "h": 250
+                  "h": 250,
+                  "ext": {
+                    "prebid": {
+                      "type": "banner"
+                    }
+                  }
                 }
               ]
             }
@@ -102,7 +107,12 @@
             "crid": "creative-123",
             "adm": "<iframe id='789abc' name='789abc' src='http://creative-url.criteo.com'></iframe>",
             "w": 300,
-            "h": 250
+            "h": 250,
+            "ext": {
+              "prebid": {
+                "type": "banner"
+              }
+            }
           },
           "type": "banner"
         }
diff --git a/adapters/criteo/criteotest/exemplary/simple-banner-multiple-bids.json b/adapters/criteo/criteotest/exemplary/simple-banner-multiple-bids.json
index f7c5da2d..44405586 100644
--- a/adapters/criteo/criteotest/exemplary/simple-banner-multiple-bids.json
+++ b/adapters/criteo/criteotest/exemplary/simple-banner-multiple-bids.json
@@ -143,7 +143,12 @@
                   "crid": "creative-123",
                   "adm": "<iframe id='789abc' name='789abc' src='http://creative-url.criteo.com'></iframe>",
                   "w": 300,
-                  "h": 250
+                  "h": 250,
+                  "ext": {
+                    "prebid": {
+                      "type": "banner"
+                    }
+                  }
                 }
               ]
             },
@@ -156,7 +161,12 @@
                   "crid": "creative-123",
                   "adm": "<iframe id='789abc' name='789abc' src='http://creative-url.criteo.com'></iframe>",
                   "w": 300,
-                  "h": 250
+                  "h": 250,
+                  "ext": {
+                    "prebid": {
+                      "type": "banner"
+                    }
+                  }
                 }
               ]
             }
@@ -177,7 +187,12 @@
             "crid": "creative-123",
             "adm": "<iframe id='789abc' name='789abc' src='http://creative-url.criteo.com'></iframe>",
             "w": 300,
-            "h": 250
+            "h": 250,
+            "ext": {
+              "prebid": {
+                "type": "banner"
+              }
+            }
           },
           "type": "banner"
         },
@@ -189,7 +204,12 @@
             "crid": "creative-123",
             "adm": "<iframe id='789abc' name='789abc' src='http://creative-url.criteo.com'></iframe>",
             "w": 300,
-            "h": 250
+            "h": 250,
+            "ext": {
+              "prebid": {
+                "type": "banner"
+              }
+            }
           },
           "type": "banner"
         }
diff --git a/adapters/criteo/criteotest/exemplary/simple-banner-uid.json b/adapters/criteo/criteotest/exemplary/simple-banner-uid.json
index 1e5bb6a8..d1174c38 100755
--- a/adapters/criteo/criteotest/exemplary/simple-banner-uid.json
+++ b/adapters/criteo/criteotest/exemplary/simple-banner-uid.json
@@ -109,7 +109,12 @@
                   "crid": "creative-123",
                   "adm": "<iframe id='789abc' name='789abc' src='http://creative-url.criteo.com'></iframe>",
                   "w": 300,
-                  "h": 250
+                  "h": 250,
+                  "ext": {
+                    "prebid": {
+                      "type": "banner"
+                    }
+                  }
                 }
               ]
             }
@@ -130,7 +135,12 @@
             "crid": "creative-123",
             "adm": "<iframe id='789abc' name='789abc' src='http://creative-url.criteo.com'></iframe>",
             "w": 300,
-            "h": 250
+            "h": 250,
+            "ext": {
+              "prebid": {
+                "type": "banner"
+              }
+            }
           },
           "type": "banner"
         }
diff --git a/adapters/criteo/criteotest/exemplary/simple-multi-type-banner.json b/adapters/criteo/criteotest/exemplary/simple-multi-type-banner.json
new file mode 100644
index 00000000..a13f5684
--- /dev/null
+++ b/adapters/criteo/criteotest/exemplary/simple-multi-type-banner.json
@@ -0,0 +1,163 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "site": {
+      "id": "site-id",
+      "page": "criteo.com"
+    },
+    "device": {
+      "os": "android",
+      "ip": "91.199.242.236",
+      "ua": "random user agent"
+    },
+    "user": {
+      "ext": {
+        "eids": [
+          {
+            "source": "criteo.com",
+            "uids": [
+              {
+                "id": "criteo-eid"
+              }
+            ]
+          }
+        ]
+      }
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "video": {
+          "mimes": ["video/mp4"],
+          "protocols": [2, 5],
+          "w": 300,
+          "h": 250
+        },
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "zoneid": 123456,
+            "networkid": 78910
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://ssp-bidder.criteo.com/openrtb/pbs/auction/request?profile=230",
+        "headers": {},
+        "body": {
+          "id": "test-request-id",
+          "site": {
+            "id": "site-id",
+            "page": "criteo.com"
+          },
+          "device": {
+            "os": "android",
+            "ip": "91.199.242.236",
+            "ua": "random user agent"
+          },
+          "user": {
+            "ext": {
+              "eids": [
+                {
+                  "source": "criteo.com",
+                  "uids": [
+                    {
+                      "id": "criteo-eid"
+                    }
+                  ]
+                }
+              ]
+            }
+          },
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "video": {
+                "mimes": ["video/mp4"],
+                "protocols": [2, 5],
+                "w": 300,
+                "h": 250
+              },
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "zoneid": 123456,
+                  "networkid": 78910
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "currency": "USD",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "test-slot-id",
+                  "impid": "test-imp-id",
+                  "price": 0.1,
+                  "crid": "creative-123",
+                  "adm": "<iframe id='789abc' name='789abc' src='http://creative-url.criteo.com'></iframe>",
+                  "h": 250,
+                  "w": 300,
+                  "ext": {
+                    "prebid": {
+                      "type": "banner"
+                    }
+                  }
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "test-slot-id",
+            "impid": "test-imp-id",
+            "price": 0.1,
+            "crid": "creative-123",
+            "adm": "<iframe id='789abc' name='789abc' src='http://creative-url.criteo.com'></iframe>",
+            "h": 250,
+            "w": 300,
+            "ext": {
+              "prebid": {
+                "type": "banner"
+              }
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
+
diff --git a/adapters/criteo/criteotest/exemplary/simple-multi-type-video.json b/adapters/criteo/criteotest/exemplary/simple-multi-type-video.json
new file mode 100644
index 00000000..4aaba73f
--- /dev/null
+++ b/adapters/criteo/criteotest/exemplary/simple-multi-type-video.json
@@ -0,0 +1,163 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "site": {
+      "id": "site-id",
+      "page": "criteo.com"
+    },
+    "device": {
+      "os": "android",
+      "ip": "91.199.242.236",
+      "ua": "random user agent"
+    },
+    "user": {
+      "ext": {
+        "eids": [
+          {
+            "source": "criteo.com",
+            "uids": [
+              {
+                "id": "criteo-eid"
+              }
+            ]
+          }
+        ]
+      }
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "video": {
+          "mimes": ["video/mp4"],
+          "protocols": [2, 5],
+          "w": 300,
+          "h": 250
+        },
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "zoneid": 123456,
+            "networkid": 78910
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://ssp-bidder.criteo.com/openrtb/pbs/auction/request?profile=230",
+        "headers": {},
+        "body": {
+          "id": "test-request-id",
+          "site": {
+            "id": "site-id",
+            "page": "criteo.com"
+          },
+          "device": {
+            "os": "android",
+            "ip": "91.199.242.236",
+            "ua": "random user agent"
+          },
+          "user": {
+            "ext": {
+              "eids": [
+                {
+                  "source": "criteo.com",
+                  "uids": [
+                    {
+                      "id": "criteo-eid"
+                    }
+                  ]
+                }
+              ]
+            }
+          },
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "video": {
+                "mimes": ["video/mp4"],
+                "protocols": [2, 5],
+                "w": 300,
+                "h": 250
+              },
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "zoneid": 123456,
+                  "networkid": 78910
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "currency": "USD",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "test-slot-id",
+                  "impid": "test-imp-id",
+                  "price": 0.1,
+                  "crid": "creative-123",
+                  "adm": "some-test-ad-vast",
+                  "h": 250,
+                  "w": 300,
+                  "ext": {
+                    "prebid": {
+                      "type": "video"
+                    }
+                  }
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "test-slot-id",
+            "impid": "test-imp-id",
+            "price": 0.1,
+            "crid": "creative-123",
+            "adm": "some-test-ad-vast",
+            "h": 250,
+            "w": 300,
+            "ext": {
+              "prebid": {
+                "type": "video"
+              }
+            }
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
+
diff --git a/adapters/criteo/criteotest/exemplary/simple-video.json b/adapters/criteo/criteotest/exemplary/simple-video.json
new file mode 100644
index 00000000..01a1ddbd
--- /dev/null
+++ b/adapters/criteo/criteotest/exemplary/simple-video.json
@@ -0,0 +1,147 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "site": {
+      "id": "site-id",
+      "page": "criteo.com"
+    },
+    "device": {
+      "os": "android",
+      "ip": "91.199.242.236",
+      "ua": "random user agent"
+    },
+    "user": {
+      "ext": {
+        "eids": [
+          {
+            "source": "criteo.com",
+            "uids": [
+              {
+                "id": "criteo-eid"
+              }
+            ]
+          }
+        ]
+      }
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "video": {
+          "mimes": ["video/mp4"],
+          "protocols": [2, 5],
+          "w": 300,
+          "h": 250
+        },
+        "ext": {
+          "bidder": {
+            "zoneid": 123456,
+            "networkid": 78910
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://ssp-bidder.criteo.com/openrtb/pbs/auction/request?profile=230",
+        "headers": {},
+        "body": {
+          "id": "test-request-id",
+          "site": {
+            "id": "site-id",
+            "page": "criteo.com"
+          },
+          "device": {
+            "os": "android",
+            "ip": "91.199.242.236",
+            "ua": "random user agent"
+          },
+          "user": {
+            "ext": {
+              "eids": [
+                {
+                  "source": "criteo.com",
+                  "uids": [
+                    {
+                      "id": "criteo-eid"
+                    }
+                  ]
+                }
+              ]
+            }
+          },
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "video": {
+                "mimes": ["video/mp4"],
+                "protocols": [2, 5],
+                "w": 300,
+                "h": 250
+              },
+              "ext": {
+                "bidder": {
+                  "zoneid": 123456,
+                  "networkid": 78910
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "currency": "USD",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "test-slot-id",
+                  "impid": "test-imp-id",
+                  "price": 0.1,
+                  "crid": "creative-123",
+                  "adm": "some-test-ad-vast",
+                  "h": 250,
+                  "w": 300,
+                  "ext": {
+                    "prebid": {
+                      "type": "video"
+                    }
+                  }
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "test-slot-id",
+            "impid": "test-imp-id",
+            "price": 0.1,
+            "crid": "creative-123",
+            "adm": "some-test-ad-vast",
+            "h": 250,
+            "w": 300,
+            "ext": {
+              "prebid": {
+                "type": "video"
+              }
+            }
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
+
diff --git a/adapters/criteo/criteotest/supplemental/simple-banner-with-ipv6.json b/adapters/criteo/criteotest/supplemental/simple-banner-with-ipv6.json
index 3ed01664..31e24fcc 100755
--- a/adapters/criteo/criteotest/supplemental/simple-banner-with-ipv6.json
+++ b/adapters/criteo/criteotest/supplemental/simple-banner-with-ipv6.json
@@ -102,7 +102,12 @@
                   "crid": "creative-123",
                   "adm": "<iframe id='789abc' name='789abc' src='http://creative-url.criteo.com'></iframe>",
                   "w": 300,
-                  "h": 250
+                  "h": 250,
+                  "ext": {
+                    "prebid": {
+                      "type": "banner"
+                    }
+                  }
                 }
               ] 
             }
@@ -123,7 +128,12 @@
             "crid": "creative-123",
             "adm": "<iframe id='789abc' name='789abc' src='http://creative-url.criteo.com'></iframe>",
             "w": 300,
-            "h": 250
+            "h": 250,
+            "ext": {
+              "prebid": {
+                "type": "banner"
+              }
+            }
           },
           "type": "banner"
         }
diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go
index 94bed36e..3947d1f0 100644
--- a/endpoints/cookie_sync_test.go
+++ b/endpoints/cookie_sync_test.go
@@ -97,7 +97,6 @@ func TestNewCookieSyncEndpoint(t *testing.T) {
 	assert.Equal(t, expected.privacyConfig.bidderHashSet, result.privacyConfig.bidderHashSet)
 }
 
-// usersyncPrivacy
 func TestCookieSyncHandle(t *testing.T) {
 	syncTypeExpected := []usersync.SyncType{usersync.SyncTypeIFrame, usersync.SyncTypeRedirect}
 	sync := usersync.Sync{URL: "aURL", Type: usersync.SyncTypeRedirect, SupportCORS: true}
@@ -1366,7 +1365,7 @@ func TestCookieSyncWriteBidderMetrics(t *testing.T) {
 		test.setExpectations(&mockMetrics)
 
 		endpoint := &cookieSyncEndpoint{metrics: &mockMetrics}
-		endpoint.writeBidderMetrics(test.given)
+		endpoint.writeSyncerMetrics(test.given)
 
 		mockMetrics.AssertExpectations(t)
 	}
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index 11be9495..3c198fbe 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -1631,7 +1631,7 @@ func TestValidateRequestExt(t *testing.T) {
 	testCases := []struct {
 		description     string
 		givenRequestExt json.RawMessage
-		expectedError   string
+		expectedErrors  []string
 	}{
 		{
 			description:     "nil",
@@ -1648,17 +1648,17 @@ func TestValidateRequestExt(t *testing.T) {
 		{
 			description:     "prebid cache - empty",
 			givenRequestExt: json.RawMessage(`{"prebid":{"cache":{}}}`),
-			expectedError:   `request.ext is invalid: request.ext.prebid.cache requires one of the "bids" or "vastxml" properties`,
+			expectedErrors:  []string{`request.ext is invalid: request.ext.prebid.cache requires one of the "bids" or "vastxml" properties`},
 		},
 		{
 			description:     "prebid cache - bids - null",
 			givenRequestExt: json.RawMessage(`{"prebid":{"cache":{"bids":null}}}`),
-			expectedError:   `request.ext is invalid: request.ext.prebid.cache requires one of the "bids" or "vastxml" properties`,
+			expectedErrors:  []string{`request.ext is invalid: request.ext.prebid.cache requires one of the "bids" or "vastxml" properties`},
 		},
 		{
 			description:     "prebid cache - bids - wrong type",
 			givenRequestExt: json.RawMessage(`{"prebid":{"cache":{"bids":true}}}`),
-			expectedError:   `json: cannot unmarshal bool into Go struct field ExtRequestPrebidCache.cache.bids of type openrtb_ext.ExtRequestPrebidCacheBids`,
+			expectedErrors:  []string{`json: cannot unmarshal bool into Go struct field ExtRequestPrebidCache.cache.bids of type openrtb_ext.ExtRequestPrebidCacheBids`},
 		},
 		{
 			description:     "prebid cache - bids - provided",
@@ -1666,13 +1666,13 @@ func TestValidateRequestExt(t *testing.T) {
 		},
 		{
 			description:     "prebid cache - vastxml - null",
-			givenRequestExt: json.RawMessage(`{"prebid":{"cache":{"vastxml":null}}}`),
-			expectedError:   `request.ext is invalid: request.ext.prebid.cache requires one of the "bids" or "vastxml" properties`,
+			givenRequestExt: json.RawMessage(`{"prebid": {"cache": {"vastxml": null}}}`),
+			expectedErrors:  []string{`request.ext is invalid: request.ext.prebid.cache requires one of the "bids" or "vastxml" properties`},
 		},
 		{
 			description:     "prebid cache - vastxml - wrong type",
 			givenRequestExt: json.RawMessage(`{"prebid":{"cache":{"vastxml":true}}}`),
-			expectedError:   `json: cannot unmarshal bool into Go struct field ExtRequestPrebidCache.cache.vastxml of type openrtb_ext.ExtRequestPrebidCacheVAST`,
+			expectedErrors:  []string{`json: cannot unmarshal bool into Go struct field ExtRequestPrebidCache.cache.vastxml of type openrtb_ext.ExtRequestPrebidCacheVAST`},
 		},
 		{
 			description:     "prebid cache - vastxml - provided",
@@ -1685,18 +1685,32 @@ func TestValidateRequestExt(t *testing.T) {
 		{
 			description:     "prebid targeting", // test integration with validateTargeting
 			givenRequestExt: json.RawMessage(`{"prebid":{"targeting":{}}}`),
-			expectedError:   "ext.prebid.targeting: At least one of includewinners or includebidderkeys must be enabled to enable targeting support",
+			expectedErrors:  []string{"ext.prebid.targeting: At least one of includewinners or includebidderkeys must be enabled to enable targeting support"},
+		},
+		{
+			description:     "valid multibid",
+			givenRequestExt: json.RawMessage(`{"prebid": {"multibid": [{"Bidder": "pubmatic", "MaxBids": 2}]}}`),
+		},
+		{
+			description:     "multibid with invalid entries",
+			givenRequestExt: json.RawMessage(`{"prebid": {"multibid": [{"Bidder": "pubmatic"}, {"Bidder": "pubmatic", "MaxBids": 2}, {"Bidders": ["pubmatic"], "MaxBids": 3}]}}`),
+			expectedErrors: []string{
+				`maxBids not defined for {Bidder:pubmatic, Bidders:[], MaxBids:<nil>, TargetBidderCodePrefix:}`,
+				`multiBid already defined for pubmatic, ignoring this instance {Bidder:, Bidders:[pubmatic], MaxBids:3, TargetBidderCodePrefix:}`,
+			},
 		},
 	}
 
 	for _, test := range testCases {
 		w := &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: test.givenRequestExt}}
-		err := validateRequestExt(w)
+		errs := validateRequestExt(w)
 
-		if len(test.expectedError) > 0 {
-			assert.EqualError(t, err, test.expectedError, test.description)
+		if len(test.expectedErrors) > 0 {
+			for i, expectedError := range test.expectedErrors {
+				assert.EqualError(t, errs[i], expectedError, test.description)
+			}
 		} else {
-			assert.NoError(t, err, test.description)
+			assert.Nil(t, errs, test.description)
 		}
 	}
 }
@@ -5116,6 +5130,89 @@ func TestSendAuctionResponse_LogsErrors(t *testing.T) {
 	}
 }
 
+func TestParseRequestMultiBid(t *testing.T) {
+	tests := []struct {
+		name             string
+		givenRequestBody string
+		expectedReqExt   json.RawMessage
+		expectedErrors   []error
+	}{
+		{
+			name:             "validate and build multi-bid extension",
+			givenRequestBody: validRequest(t, "multi-bid-error.json"),
+			expectedReqExt:   getObject(t, "multi-bid-error.json", "expectedReqExt"),
+			expectedErrors: []error{
+				&errortypes.Warning{
+					WarningCode: errortypes.MultiBidWarningCode,
+					Message:     "maxBids not defined for {Bidder:appnexus, Bidders:[], MaxBids:<nil>, TargetBidderCodePrefix:}",
+				},
+				&errortypes.Warning{
+					WarningCode: errortypes.MultiBidWarningCode,
+					Message:     "invalid maxBids value, using minimum 1 limit for {Bidder:rubicon, Bidders:[], MaxBids:-1, TargetBidderCodePrefix:rubN}",
+				},
+				&errortypes.Warning{
+					WarningCode: errortypes.MultiBidWarningCode,
+					Message:     "invalid maxBids value, using maximum 9 limit for {Bidder:pubmatic, Bidders:[], MaxBids:10, TargetBidderCodePrefix:pm}",
+				},
+				&errortypes.Warning{
+					WarningCode: errortypes.MultiBidWarningCode,
+					Message:     "multiBid already defined for pubmatic, ignoring this instance {Bidder:pubmatic, Bidders:[], MaxBids:4, TargetBidderCodePrefix:pubM}",
+				},
+				&errortypes.Warning{
+					WarningCode: errortypes.MultiBidWarningCode,
+					Message:     "ignoring bidders from {Bidder:groupm, Bidders:[someBidder], MaxBids:5, TargetBidderCodePrefix:gm}",
+				},
+				&errortypes.Warning{
+					WarningCode: errortypes.MultiBidWarningCode,
+					Message:     "multiBid already defined for groupm, ignoring this instance {Bidder:, Bidders:[groupm], MaxBids:6, TargetBidderCodePrefix:}",
+				},
+				&errortypes.Warning{
+					WarningCode: errortypes.MultiBidWarningCode,
+					Message:     "ignoring targetbiddercodeprefix for {Bidder:, Bidders:[33across], MaxBids:7, TargetBidderCodePrefix:abc}",
+				},
+				&errortypes.Warning{
+					WarningCode: errortypes.MultiBidWarningCode,
+					Message:     "bidder(s) not specified for {Bidder:, Bidders:[], MaxBids:8, TargetBidderCodePrefix:xyz}",
+				},
+			},
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			deps := &endpointDeps{
+				fakeUUIDGenerator{},
+				&warningsCheckExchange{},
+				mockBidderParamValidator{},
+				&mockStoredReqFetcher{},
+				empty_fetcher.EmptyFetcher{},
+				empty_fetcher.EmptyFetcher{},
+				&config.Configuration{MaxRequestSize: int64(len(test.givenRequestBody))},
+				&metricsConfig.NilMetricsEngine{},
+				analyticsConf.NewPBSAnalytics(&config.Analytics{}),
+				map[string]string{},
+				false,
+				[]byte{},
+				openrtb_ext.BuildBidderMap(),
+				nil,
+				nil,
+				hardcodedResponseIPValidator{response: true},
+				empty_fetcher.EmptyFetcher{},
+				hookexecution.NewHookExecutor(hooks.EmptyPlanBuilder{}, hookexecution.EndpointAuction, &metricsConfig.NilMetricsEngine{}),
+			}
+
+			req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(test.givenRequestBody))
+
+			resReq, _, _, _, _, _, errL := deps.parseRequest(req, &metrics.Labels{})
+
+			assert.NoError(t, resReq.RebuildRequest())
+
+			assert.JSONEq(t, string(test.expectedReqExt), string(resReq.Ext))
+
+			assert.Equal(t, errL, test.expectedErrors, "error length should match")
+		})
+	}
+}
+
 type mockStoredResponseFetcher struct {
 	data map[string]json.RawMessage
 }
diff --git a/exchange/auction_test.go b/exchange/auction_test.go
index 6518eda7..d75b9c64 100644
--- a/exchange/auction_test.go
+++ b/exchange/auction_test.go
@@ -8,6 +8,7 @@ import (
 	"os"
 	"path/filepath"
 	"regexp"
+	"sort"
 	"strconv"
 	"testing"
 
@@ -190,7 +191,7 @@ func loadCacheSpec(filename string) (*cacheSpec, error) {
 func runCacheSpec(t *testing.T, fileDisplayName string, specData *cacheSpec) {
 	var bid *entities.PbsOrtbBid
 	winningBidsByImp := make(map[string]*entities.PbsOrtbBid)
-	winningBidsByBidder := make(map[string]map[openrtb_ext.BidderName]*entities.PbsOrtbBid)
+	winningBidsByBidder := make(map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid)
 	roundedPrices := make(map[*entities.PbsOrtbBid]string)
 	bidCategory := make(map[string]string)
 
@@ -209,14 +210,20 @@ func runCacheSpec(t *testing.T, fileDisplayName string, specData *cacheSpec) {
 		}
 
 		// Map this bid if it's the highest we've seen from this bidder so far
-		if _, ok := winningBidsByBidder[bid.Bid.ImpID]; ok {
-			bestSoFar, ok := winningBidsByBidder[bid.Bid.ImpID][pbsBid.Bidder]
-			if !ok || cpm > bestSoFar.Bid.Price {
-				winningBidsByBidder[bid.Bid.ImpID][pbsBid.Bidder] = bid
-			}
+		if bidMap, ok := winningBidsByBidder[bid.Bid.ImpID]; ok {
+			bidMap[pbsBid.Bidder] = append(bidMap[pbsBid.Bidder], bid)
 		} else {
-			winningBidsByBidder[bid.Bid.ImpID] = make(map[openrtb_ext.BidderName]*entities.PbsOrtbBid)
-			winningBidsByBidder[bid.Bid.ImpID][pbsBid.Bidder] = bid
+			winningBidsByBidder[bid.Bid.ImpID] = map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+				pbsBid.Bidder: {bid},
+			}
+		}
+
+		for _, topBidsPerBidder := range winningBidsByBidder {
+			for _, topBids := range topBidsPerBidder {
+				sort.Slice(topBids, func(i, j int) bool {
+					return isNewWinningBid(topBids[i].Bid, topBids[j].Bid, true)
+				})
+			}
 		}
 
 		if len(pbsBid.Bid.Cat) == 1 {
@@ -363,6 +370,18 @@ func TestNewAuction(t *testing.T) {
 			Price: 1.44,
 		},
 	}
+	bid2p155 := entities.PbsOrtbBid{
+		Bid: &openrtb2.Bid{
+			ImpID: "imp2",
+			Price: 1.55,
+		},
+	}
+	bid2p166 := entities.PbsOrtbBid{
+		Bid: &openrtb2.Bid{
+			ImpID: "imp2",
+			Price: 1.66,
+		},
+	}
 	tests := []struct {
 		description     string
 		seatBids        map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid
@@ -386,10 +405,10 @@ func TestNewAuction(t *testing.T) {
 				winningBids: map[string]*entities.PbsOrtbBid{
 					"imp1": &bid1p230,
 				},
-				winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*entities.PbsOrtbBid{
+				winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 					"imp1": {
-						"appnexus": &bid1p123,
-						"rubicon":  &bid1p230,
+						"appnexus": []*entities.PbsOrtbBid{&bid1p123},
+						"rubicon":  []*entities.PbsOrtbBid{&bid1p230},
 					},
 				},
 			},
@@ -414,15 +433,15 @@ func TestNewAuction(t *testing.T) {
 					"imp1": &bid1p230,
 					"imp2": &bid2p144,
 				},
-				winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*entities.PbsOrtbBid{
+				winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 					"imp1": {
-						"appnexus": &bid1p230,
-						"rubicon":  &bid1p077,
-						"openx":    &bid1p123,
+						"appnexus": []*entities.PbsOrtbBid{&bid1p230},
+						"rubicon":  []*entities.PbsOrtbBid{&bid1p077},
+						"openx":    []*entities.PbsOrtbBid{&bid1p123},
 					},
 					"imp2": {
-						"appnexus": &bid2p123,
-						"rubicon":  &bid2p144,
+						"appnexus": []*entities.PbsOrtbBid{&bid2p123},
+						"rubicon":  []*entities.PbsOrtbBid{&bid2p144},
 					},
 				},
 			},
@@ -443,10 +462,10 @@ func TestNewAuction(t *testing.T) {
 				winningBids: map[string]*entities.PbsOrtbBid{
 					"imp1": &bid1p123,
 				},
-				winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*entities.PbsOrtbBid{
+				winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 					"imp1": {
-						"appnexus": &bid1p123,
-						"rubicon":  &bid1p088d,
+						"appnexus": []*entities.PbsOrtbBid{&bid1p123},
+						"rubicon":  []*entities.PbsOrtbBid{&bid1p088d},
 					},
 				},
 			},
@@ -467,10 +486,10 @@ func TestNewAuction(t *testing.T) {
 				winningBids: map[string]*entities.PbsOrtbBid{
 					"imp1": &bid1p088d,
 				},
-				winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*entities.PbsOrtbBid{
+				winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 					"imp1": {
-						"appnexus": &bid1p123,
-						"rubicon":  &bid1p088d,
+						"appnexus": []*entities.PbsOrtbBid{&bid1p123},
+						"rubicon":  []*entities.PbsOrtbBid{&bid1p088d},
 					},
 				},
 			},
@@ -491,10 +510,10 @@ func TestNewAuction(t *testing.T) {
 				winningBids: map[string]*entities.PbsOrtbBid{
 					"imp1": &bid1p166d,
 				},
-				winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*entities.PbsOrtbBid{
+				winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 					"imp1": {
-						"appnexus": &bid1p166d,
-						"rubicon":  &bid1p088d,
+						"appnexus": []*entities.PbsOrtbBid{&bid1p166d},
+						"rubicon":  []*entities.PbsOrtbBid{&bid1p088d},
 					},
 				},
 			},
@@ -518,11 +537,40 @@ func TestNewAuction(t *testing.T) {
 				winningBids: map[string]*entities.PbsOrtbBid{
 					"imp1": &bid1p166d,
 				},
-				winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*entities.PbsOrtbBid{
+				winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+					"imp1": {
+						"appnexus": []*entities.PbsOrtbBid{&bid1p166d},
+						"rubicon":  []*entities.PbsOrtbBid{&bid1p088d},
+						"openx":    []*entities.PbsOrtbBid{&bid1p230},
+					},
+				},
+			},
+		},
+		{
+			description: "Auction with 3 bids and 2 deals - multiple bids under each seatBids",
+			seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+				"appnexus": {
+					Bids: []*entities.PbsOrtbBid{&bid1p166d, &bid1p077, &bid2p123, &bid2p144},
+				},
+				"pubmatic": {
+					Bids: []*entities.PbsOrtbBid{&bid1p088d, &bid1p123, &bid2p155, &bid2p166},
+				},
+			},
+			numImps:     1,
+			preferDeals: true,
+			expectedAuction: auction{
+				winningBids: map[string]*entities.PbsOrtbBid{
+					"imp1": &bid1p166d,
+					"imp2": &bid2p166,
+				},
+				winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 					"imp1": {
-						"appnexus": &bid1p166d,
-						"rubicon":  &bid1p088d,
-						"openx":    &bid1p230,
+						"appnexus": []*entities.PbsOrtbBid{&bid1p166d, &bid1p077},
+						"pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123},
+					},
+					"imp2": {
+						"appnexus": []*entities.PbsOrtbBid{&bid2p123, &bid2p144},
+						"pubmatic": []*entities.PbsOrtbBid{&bid2p155, &bid2p166},
 					},
 				},
 			},
@@ -537,6 +585,257 @@ func TestNewAuction(t *testing.T) {
 
 }
 
+func TestValidateAndUpdateMultiBid(t *testing.T) {
+	// create new bids for new test cases since the last one changes a few bids. Ex marks bid1p001.Bid = nil
+	bid1p001 := entities.PbsOrtbBid{
+		Bid: &openrtb2.Bid{
+			ImpID: "imp1",
+			Price: 0.01,
+		},
+	}
+	bid1p077 := entities.PbsOrtbBid{
+		Bid: &openrtb2.Bid{
+			ImpID: "imp1",
+			Price: 0.77,
+		},
+	}
+	bid1p123 := entities.PbsOrtbBid{
+		Bid: &openrtb2.Bid{
+			ImpID: "imp1",
+			Price: 1.23,
+		},
+	}
+	bid1p088d := entities.PbsOrtbBid{
+		Bid: &openrtb2.Bid{
+			ImpID:  "imp1",
+			Price:  0.88,
+			DealID: "SpecialDeal",
+		},
+	}
+	bid1p166d := entities.PbsOrtbBid{
+		Bid: &openrtb2.Bid{
+			ImpID:  "imp1",
+			Price:  1.66,
+			DealID: "BigDeal",
+		},
+	}
+	bid2p123 := entities.PbsOrtbBid{
+		Bid: &openrtb2.Bid{
+			ImpID: "imp2",
+			Price: 1.23,
+		},
+	}
+	bid2p144 := entities.PbsOrtbBid{
+		Bid: &openrtb2.Bid{
+			ImpID: "imp2",
+			Price: 1.44,
+		},
+	}
+	bid2p155 := entities.PbsOrtbBid{
+		Bid: &openrtb2.Bid{
+			ImpID: "imp2",
+			Price: 1.55,
+		},
+	}
+	bid2p166 := entities.PbsOrtbBid{
+		Bid: &openrtb2.Bid{
+			ImpID: "imp2",
+			Price: 1.66,
+		},
+	}
+
+	type fields struct {
+		winningBids         map[string]*entities.PbsOrtbBid
+		winningBidsByBidder map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid
+		roundedPrices       map[*entities.PbsOrtbBid]string
+		cacheIds            map[*openrtb2.Bid]string
+		vastCacheIds        map[*openrtb2.Bid]string
+	}
+	type args struct {
+		adapterBids            map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid
+		preferDeals            bool
+		accountDefaultBidLimit int
+	}
+	type want struct {
+		winningBidsByBidder map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid
+		adapterBids         map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid
+	}
+	tests := []struct {
+		description string
+		fields      fields
+		args        args
+		want        want
+	}{
+		{
+			description: "DefaultBidLimit is 0 (default value)",
+			fields: fields{
+				winningBids: map[string]*entities.PbsOrtbBid{
+					"imp1": &bid1p166d,
+					"imp2": &bid2p166,
+				},
+				winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+					"imp1": {
+						"appnexus": []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077},
+						"pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123},
+					},
+					"imp2": {
+						"appnexus": []*entities.PbsOrtbBid{&bid2p123, &bid2p144},
+						"pubmatic": []*entities.PbsOrtbBid{&bid2p155, &bid2p166},
+					},
+				},
+			},
+			args: args{
+				adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+					"appnexus": {
+						Bids: []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077, &bid2p123, &bid2p144},
+					},
+					"pubmatic": {
+						Bids: []*entities.PbsOrtbBid{&bid1p088d, &bid1p123, &bid2p155, &bid2p166},
+					},
+				},
+				accountDefaultBidLimit: 0,
+				preferDeals:            true,
+			},
+			want: want{
+				winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+					"imp1": {
+						"appnexus": []*entities.PbsOrtbBid{&bid1p166d, &bid1p077, &bid1p001},
+						"pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123},
+					},
+					"imp2": {
+						"appnexus": []*entities.PbsOrtbBid{&bid2p144, &bid2p123},
+						"pubmatic": []*entities.PbsOrtbBid{&bid2p166, &bid2p155},
+					},
+				},
+				adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+					"appnexus": {
+						Bids: []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077, &bid2p123, &bid2p144},
+					},
+					"pubmatic": {
+						Bids: []*entities.PbsOrtbBid{&bid1p088d, &bid1p123, &bid2p155, &bid2p166},
+					},
+				},
+			},
+		},
+		{
+			description: "Adapters bid count per imp within DefaultBidLimit",
+			fields: fields{
+				winningBids: map[string]*entities.PbsOrtbBid{
+					"imp1": &bid1p166d,
+					"imp2": &bid2p166,
+				},
+				winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+					"imp1": {
+						"appnexus": []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077},
+						"pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123},
+					},
+					"imp2": {
+						"appnexus": []*entities.PbsOrtbBid{&bid2p123, &bid2p144},
+						"pubmatic": []*entities.PbsOrtbBid{&bid2p155, &bid2p166},
+					},
+				},
+			},
+			args: args{
+				adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+					"appnexus": {
+						Bids: []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077, &bid2p123, &bid2p144},
+					},
+					"pubmatic": {
+						Bids: []*entities.PbsOrtbBid{&bid1p088d, &bid1p123, &bid2p155, &bid2p166},
+					},
+				},
+				accountDefaultBidLimit: 3,
+				preferDeals:            true,
+			},
+			want: want{
+				winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+					"imp1": {
+						"appnexus": []*entities.PbsOrtbBid{&bid1p166d, &bid1p077, &bid1p001},
+						"pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123},
+					},
+					"imp2": {
+						"appnexus": []*entities.PbsOrtbBid{&bid2p144, &bid2p123},
+						"pubmatic": []*entities.PbsOrtbBid{&bid2p166, &bid2p155},
+					},
+				},
+				adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+					"appnexus": {
+						Bids: []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077, &bid2p123, &bid2p144},
+					},
+					"pubmatic": {
+						Bids: []*entities.PbsOrtbBid{&bid1p088d, &bid1p123, &bid2p155, &bid2p166},
+					},
+				},
+			},
+		},
+		{
+			description: "Adapters bid count per imp more than DefaultBidLimit",
+			fields: fields{
+				winningBids: map[string]*entities.PbsOrtbBid{
+					"imp1": &bid1p166d,
+					"imp2": &bid2p166,
+				},
+				winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+					"imp1": {
+						"appnexus": []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077},
+						"pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123},
+					},
+					"imp2": {
+						"appnexus": []*entities.PbsOrtbBid{&bid2p123, &bid2p144},
+						"pubmatic": []*entities.PbsOrtbBid{&bid2p155, &bid2p166},
+					},
+				},
+			},
+			args: args{
+				adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+					"appnexus": {
+						Bids: []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077, &bid2p123, &bid2p144},
+					},
+					"pubmatic": {
+						Bids: []*entities.PbsOrtbBid{&bid1p088d, &bid1p123, &bid2p155, &bid2p166},
+					},
+				},
+				accountDefaultBidLimit: 2,
+				preferDeals:            true,
+			},
+			want: want{
+				winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+					"imp1": {
+						"appnexus": []*entities.PbsOrtbBid{&bid1p166d, &bid1p077},
+						"pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123},
+					},
+					"imp2": {
+						"appnexus": []*entities.PbsOrtbBid{&bid2p144, &bid2p123},
+						"pubmatic": []*entities.PbsOrtbBid{&bid2p166, &bid2p155},
+					},
+				},
+				adapterBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+					"appnexus": {
+						Bids: []*entities.PbsOrtbBid{&bid1p166d, &bid1p077, &bid2p123, &bid2p144},
+					},
+					"pubmatic": {
+						Bids: []*entities.PbsOrtbBid{&bid1p088d, &bid1p123, &bid2p155, &bid2p166},
+					},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.description, func(t *testing.T) {
+			a := &auction{
+				winningBids:         tt.fields.winningBids,
+				winningBidsByBidder: tt.fields.winningBidsByBidder,
+				roundedPrices:       tt.fields.roundedPrices,
+				cacheIds:            tt.fields.cacheIds,
+				vastCacheIds:        tt.fields.vastCacheIds,
+			}
+			a.validateAndUpdateMultiBid(tt.args.adapterBids, tt.args.preferDeals, tt.args.accountDefaultBidLimit)
+			assert.Equal(t, tt.want.winningBidsByBidder, tt.fields.winningBidsByBidder, tt.description)
+			assert.Equal(t, tt.want.adapterBids, tt.args.adapterBids, tt.description)
+		})
+	}
+}
+
 type cacheSpec struct {
 	BidRequest                  openrtb2.BidRequest             `json:"bidRequest"`
 	PbsBids                     []pbsBid                        `json:"pbsBids"`
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index 3d374417..44aa3cc6 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -20,21 +20,16 @@ import (
 
 	"github.com/buger/jsonparser"
 	"github.com/prebid/openrtb/v17/openrtb2"
-	"github.com/prebid/prebid-server/exchange/entities"
-	"github.com/prebid/prebid-server/hooks"
-	"github.com/prebid/prebid-server/hooks/hookexecution"
-	"github.com/prebid/prebid-server/hooks/hookstage"
-	"github.com/prebid/prebid-server/util/ptrutil"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/mock"
-	jsonpatch "gopkg.in/evanphx/json-patch.v4"
-
 	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/currency"
 	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/exchange/entities"
 	"github.com/prebid/prebid-server/experiment/adscert"
 	"github.com/prebid/prebid-server/gdpr"
+	"github.com/prebid/prebid-server/hooks"
+	"github.com/prebid/prebid-server/hooks/hookexecution"
+	"github.com/prebid/prebid-server/hooks/hookstage"
 	"github.com/prebid/prebid-server/metrics"
 	metricsConf "github.com/prebid/prebid-server/metrics/config"
 	metricsConfig "github.com/prebid/prebid-server/metrics/config"
@@ -43,6 +38,10 @@ import (
 	"github.com/prebid/prebid-server/stored_requests"
 	"github.com/prebid/prebid-server/stored_requests/backends/file_fetcher"
 	"github.com/prebid/prebid-server/usersync"
+	"github.com/prebid/prebid-server/util/ptrutil"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+	jsonpatch "gopkg.in/evanphx/json-patch.v4"
 )
 
 func TestNewExchange(t *testing.T) {
@@ -2244,6 +2243,26 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
 		HookExecutor:  &hookexecution.EmptyHookExecutor{},
 	}
 
+	if spec.MultiBid != nil {
+		auctionRequest.Account.DefaultBidLimit = spec.MultiBid.AccountMaxBid
+
+		requestExt := &openrtb_ext.ExtRequest{}
+		err := json.Unmarshal(spec.IncomingRequest.OrtbRequest.Ext, requestExt)
+		assert.NoError(t, err, "invalid request ext")
+		validatedMultiBids, errs := openrtb_ext.ValidateAndBuildExtMultiBid(&requestExt.Prebid)
+		for _, err := range errs { // same as in validateRequestExt().
+			auctionRequest.Warnings = append(auctionRequest.Warnings, &errortypes.Warning{
+				WarningCode: errortypes.MultiBidWarningCode,
+				Message:     err.Error(),
+			})
+		}
+
+		requestExt.Prebid.MultiBid = validatedMultiBids
+		updateReqExt, err := json.Marshal(requestExt)
+		assert.NoError(t, err, "invalid request ext")
+		auctionRequest.BidRequestWrapper.Ext = updateReqExt
+	}
+
 	if spec.StartTime > 0 {
 		auctionRequest.StartTime = time.Unix(0, spec.StartTime*1e+6)
 	}
@@ -2293,11 +2312,11 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
 		assert.JSONEq(t, string(bid.Ext), string(spec.Response.Ext), "Debug info modified")
 	}
 
-	if spec.PassthroughFlag {
+	if spec.PassthroughFlag || (spec.MultiBid != nil && spec.MultiBid.AssertMultiBidWarnings) {
 		expectedPassthough := ""
 		actualPassthrough := ""
+		actualBidRespExt := &openrtb_ext.ExtBidResponse{}
 		if bid.Ext != nil {
-			actualBidRespExt := &openrtb_ext.ExtBidResponse{}
 			if err := json.Unmarshal(bid.Ext, actualBidRespExt); err != nil {
 				assert.NoError(t, err, fmt.Sprintf("Error when unmarshalling: %s", err))
 			}
@@ -2305,8 +2324,8 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
 				actualPassthrough = string(actualBidRespExt.Prebid.Passthrough)
 			}
 		}
+		expectedBidRespExt := &openrtb_ext.ExtBidResponse{}
 		if spec.Response.Ext != nil {
-			expectedBidRespExt := &openrtb_ext.ExtBidResponse{}
 			if err := json.Unmarshal(spec.Response.Ext, expectedBidRespExt); err != nil {
 				assert.NoError(t, err, fmt.Sprintf("Error when unmarshalling: %s", err))
 			}
@@ -2315,11 +2334,17 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
 			}
 		}
 
-		// special handling since JSONEq fails if either parameters is an empty string instead of json
-		if expectedPassthough == "" || actualPassthrough == "" {
-			assert.Equal(t, expectedPassthough, actualPassthrough, "Expected bid response extension is incorrect")
-		} else {
-			assert.JSONEq(t, expectedPassthough, actualPassthrough, "Expected bid response extension is incorrect")
+		if spec.MultiBid != nil && spec.MultiBid.AssertMultiBidWarnings {
+			assert.Equal(t, expectedBidRespExt.Warnings, actualBidRespExt.Warnings, "Expected same multi-bid warnings")
+		}
+
+		if spec.PassthroughFlag {
+			// special handling since JSONEq fails if either parameters is an empty string instead of json
+			if expectedPassthough == "" || actualPassthrough == "" {
+				assert.Equal(t, expectedPassthough, actualPassthrough, "Expected bid response extension is incorrect")
+			} else {
+				assert.JSONEq(t, expectedPassthough, actualPassthrough, "Expected bid response extension is incorrect")
+			}
 		}
 
 	}
@@ -2589,10 +2614,10 @@ func TestCategoryMapping(t *testing.T) {
 	bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1}
 	bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 40.0000, Cat: cats4, W: 1, H: 1}
 
-	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"}
-	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, "", 20.0000, "USD"}
-	bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, nil, 0, false, "", 30.0000, "USD"}
-	bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 40.0000, "USD"}
+	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
+	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, "", 20.0000, "USD", ""}
+	bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, nil, 0, false, "", 30.0000, "USD", ""}
+	bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 40.0000, "USD", ""}
 
 	innerBids := []*entities.PbsOrtbBid{
 		&bid1_1,
@@ -2644,10 +2669,10 @@ func TestCategoryMappingNoIncludeBrandCategory(t *testing.T) {
 	bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1}
 	bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 40.0000, Cat: cats4, W: 1, H: 1}
 
-	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"}
-	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, "", 20.0000, "USD"}
-	bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, nil, 0, false, "", 30.0000, "USD"}
-	bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, nil, 0, false, "", 40.0000, "USD"}
+	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
+	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, "", 20.0000, "USD", ""}
+	bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, nil, 0, false, "", 30.0000, "USD", ""}
+	bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, nil, 0, false, "", 40.0000, "USD", ""}
 
 	innerBids := []*entities.PbsOrtbBid{
 		&bid1_1,
@@ -2698,9 +2723,9 @@ func TestCategoryMappingTranslateCategoriesNil(t *testing.T) {
 	bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 20.0000, Cat: cats2, W: 1, H: 1}
 	bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1}
 
-	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"}
-	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, "", 20.0000, "USD"}
-	bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 30.0000, "USD"}
+	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
+	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, "", 20.0000, "USD", ""}
+	bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 30.0000, "USD", ""}
 
 	innerBids := []*entities.PbsOrtbBid{
 		&bid1_1,
@@ -2780,9 +2805,9 @@ func TestCategoryMappingTranslateCategoriesFalse(t *testing.T) {
 	bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 20.0000, Cat: cats2, W: 1, H: 1}
 	bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1}
 
-	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"}
-	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, "", 20.0000, "USD"}
-	bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 30.0000, "USD"}
+	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
+	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, "", 20.0000, "USD", ""}
+	bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 30.0000, "USD", ""}
 
 	innerBids := []*entities.PbsOrtbBid{
 		&bid1_1,
@@ -2832,11 +2857,11 @@ func TestCategoryDedupe(t *testing.T) {
 	bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 20.0000, Cat: cats4, W: 1, H: 1}
 	bid5 := openrtb2.Bid{ID: "bid_id5", ImpID: "imp_id5", Price: 20.0000, Cat: cats1, W: 1, H: 1}
 
-	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"}
-	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, nil, 0, false, "", 15.0000, "USD"}
-	bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD"}
-	bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD"}
-	bid1_5 := entities.PbsOrtbBid{&bid5, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD"}
+	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
+	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, nil, 0, false, "", 15.0000, "USD", ""}
+	bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", ""}
+	bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", ""}
+	bid1_5 := entities.PbsOrtbBid{&bid5, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", ""}
 
 	selectedBids := make(map[string]int)
 	expectedCategories := map[string]string{
@@ -2911,11 +2936,11 @@ func TestNoCategoryDedupe(t *testing.T) {
 	bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 20.0000, Cat: cats4, W: 1, H: 1}
 	bid5 := openrtb2.Bid{ID: "bid_id5", ImpID: "imp_id5", Price: 10.0000, Cat: cats1, W: 1, H: 1}
 
-	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 14.0000, "USD"}
-	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 14.0000, "USD"}
-	bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD"}
-	bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD"}
-	bid1_5 := entities.PbsOrtbBid{&bid5, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"}
+	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 14.0000, "USD", ""}
+	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 14.0000, "USD", ""}
+	bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", ""}
+	bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", ""}
+	bid1_5 := entities.PbsOrtbBid{&bid5, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
 
 	selectedBids := make(map[string]int)
 	expectedCategories := map[string]string{
@@ -2991,8 +3016,8 @@ func TestCategoryMappingBidderName(t *testing.T) {
 	bid1 := openrtb2.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1}
 	bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 10.0000, Cat: cats2, W: 1, H: 1}
 
-	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"}
-	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"}
+	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
+	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
 
 	innerBids1 := []*entities.PbsOrtbBid{
 		&bid1_1,
@@ -3045,8 +3070,8 @@ func TestCategoryMappingBidderNameNoCategories(t *testing.T) {
 	bid1 := openrtb2.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1}
 	bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 12.0000, Cat: cats2, W: 1, H: 1}
 
-	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"}
-	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 12.0000, "USD"}
+	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
+	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 12.0000, "USD", ""}
 
 	innerBids1 := []*entities.PbsOrtbBid{
 		&bid1_1,
@@ -3157,7 +3182,7 @@ func TestBidRejectionErrors(t *testing.T) {
 		innerBids := []*entities.PbsOrtbBid{}
 		for _, bid := range test.bids {
 			currentBid := entities.PbsOrtbBid{
-				bid, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: test.duration}, nil, 0, false, "", 10.0000, "USD"}
+				bid, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: test.duration}, nil, 0, false, "", 10.0000, "USD", ""}
 			innerBids = append(innerBids, &currentBid)
 		}
 
@@ -3205,8 +3230,8 @@ func TestCategoryMappingTwoBiddersOneBidEachNoCategorySamePrice(t *testing.T) {
 	bidApn1 := openrtb2.Bid{ID: "bid_idApn1", ImpID: "imp_idApn1", Price: 10.0000, Cat: cats1, W: 1, H: 1}
 	bidApn2 := openrtb2.Bid{ID: "bid_idApn2", ImpID: "imp_idApn2", Price: 10.0000, Cat: cats2, W: 1, H: 1}
 
-	bid1_Apn1 := entities.PbsOrtbBid{&bidApn1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"}
-	bid1_Apn2 := entities.PbsOrtbBid{&bidApn2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"}
+	bid1_Apn1 := entities.PbsOrtbBid{&bidApn1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
+	bid1_Apn2 := entities.PbsOrtbBid{&bidApn2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
 
 	innerBidsApn1 := []*entities.PbsOrtbBid{
 		&bid1_Apn1,
@@ -3285,11 +3310,11 @@ func TestCategoryMappingTwoBiddersManyBidsEachNoCategorySamePrice(t *testing.T)
 	bidApn2_1 := openrtb2.Bid{ID: "bid_idApn2_1", ImpID: "imp_idApn2_1", Price: 10.0000, Cat: cats2, W: 1, H: 1}
 	bidApn2_2 := openrtb2.Bid{ID: "bid_idApn2_2", ImpID: "imp_idApn2_2", Price: 20.0000, Cat: cats2, W: 1, H: 1}
 
-	bid1_Apn1_1 := entities.PbsOrtbBid{&bidApn1_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"}
-	bid1_Apn1_2 := entities.PbsOrtbBid{&bidApn1_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD"}
+	bid1_Apn1_1 := entities.PbsOrtbBid{&bidApn1_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
+	bid1_Apn1_2 := entities.PbsOrtbBid{&bidApn1_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", ""}
 
-	bid1_Apn2_1 := entities.PbsOrtbBid{&bidApn2_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"}
-	bid1_Apn2_2 := entities.PbsOrtbBid{&bidApn2_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD"}
+	bid1_Apn2_1 := entities.PbsOrtbBid{&bidApn2_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
+	bid1_Apn2_2 := entities.PbsOrtbBid{&bidApn2_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", ""}
 
 	innerBidsApn1 := []*entities.PbsOrtbBid{
 		&bid1_Apn1_1,
@@ -3367,9 +3392,9 @@ func TestRemoveBidById(t *testing.T) {
 	bidApn1_2 := openrtb2.Bid{ID: "bid_idApn1_2", ImpID: "imp_idApn1_2", Price: 20.0000, Cat: cats1, W: 1, H: 1}
 	bidApn1_3 := openrtb2.Bid{ID: "bid_idApn1_3", ImpID: "imp_idApn1_3", Price: 10.0000, Cat: cats1, W: 1, H: 1}
 
-	bid1_Apn1_1 := entities.PbsOrtbBid{&bidApn1_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"}
-	bid1_Apn1_2 := entities.PbsOrtbBid{&bidApn1_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD"}
-	bid1_Apn1_3 := entities.PbsOrtbBid{&bidApn1_3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD"}
+	bid1_Apn1_1 := entities.PbsOrtbBid{&bidApn1_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
+	bid1_Apn1_2 := entities.PbsOrtbBid{&bidApn1_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", ""}
+	bid1_Apn1_3 := entities.PbsOrtbBid{&bidApn1_3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
 
 	type aTest struct {
 		desc      string
@@ -3494,29 +3519,211 @@ func TestApplyDealSupport(t *testing.T) {
 			},
 		}
 
-		bid := entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, test.dealPriority, false, "", 0, "USD"}
+		bid := entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, test.dealPriority, false, "", 0, "USD", ""}
 		bidCategory := map[string]string{
 			bid.Bid.ID: test.targ["hb_pb_cat_dur"],
 		}
 
 		auc := &auction{
-			winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*entities.PbsOrtbBid{
+			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				"imp_id1": {
-					bidderName: &bid,
+					bidderName: {&bid},
 				},
 			},
 		}
 
-		dealErrs := applyDealSupport(bidRequest, auc, bidCategory)
+		dealErrs := applyDealSupport(bidRequest, auc, bidCategory, nil)
 
-		assert.Equal(t, test.expectedHbPbCatDur, bidCategory[auc.winningBidsByBidder["imp_id1"][bidderName].Bid.ID], test.description)
-		assert.Equal(t, test.expectedDealTierSatisfied, auc.winningBidsByBidder["imp_id1"][bidderName].DealTierSatisfied, "expectedDealTierSatisfied=%v when %v", test.expectedDealTierSatisfied, test.description)
+		assert.Equal(t, test.expectedHbPbCatDur, bidCategory[auc.winningBidsByBidder["imp_id1"][bidderName][0].Bid.ID], test.description)
+		assert.Equal(t, test.expectedDealTierSatisfied, auc.winningBidsByBidder["imp_id1"][bidderName][0].DealTierSatisfied, "expectedDealTierSatisfied=%v when %v", test.expectedDealTierSatisfied, test.description)
 		if len(test.expectedDealErr) > 0 {
 			assert.Containsf(t, dealErrs, errors.New(test.expectedDealErr), "Expected error message not found in deal errors")
 		}
 	}
 }
 
+func TestApplyDealSupportMultiBid(t *testing.T) {
+	type args struct {
+		bidRequest  *openrtb2.BidRequest
+		auc         *auction
+		bidCategory map[string]string
+		multiBid    map[string]openrtb_ext.ExtMultiBid
+	}
+	type want struct {
+		errs                      []error
+		expectedHbPbCatDur        map[string]map[string][]string
+		expectedDealTierSatisfied map[string]map[string][]bool
+	}
+	tests := []struct {
+		name string
+		args args
+		want want
+	}{
+		{
+			name: "multibid disabled, hb_pb_cat_dur should be modified only for first bid",
+			args: args{
+				bidRequest: &openrtb2.BidRequest{
+					ID: "some-request-id",
+					Imp: []openrtb2.Imp{
+						{
+							ID:  "imp_id1",
+							Ext: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`),
+						},
+						{
+							ID:  "imp_id1",
+							Ext: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`),
+						},
+					},
+				},
+				auc: &auction{
+					winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+						"imp_id1": {
+							openrtb_ext.BidderName("appnexus"): {
+								&entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, 5, false, "", 0, "USD", ""},
+								&entities.PbsOrtbBid{&openrtb2.Bid{ID: "789101"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, 5, false, "", 0, "USD", ""},
+							},
+						},
+					},
+				},
+				bidCategory: map[string]string{
+					"123456": "12.00_movies_30s",
+					"789101": "12.00_movies_30s",
+				},
+				multiBid: nil,
+			},
+			want: want{
+				errs: []error{},
+				expectedHbPbCatDur: map[string]map[string][]string{
+					"imp_id1": {
+						"appnexus": []string{"tier5_movies_30s", "12.00_movies_30s"},
+					},
+				},
+				expectedDealTierSatisfied: map[string]map[string][]bool{
+					"imp_id1": {
+						"appnexus": []bool{true, false},
+					},
+				},
+			},
+		},
+		{
+			name: "multibid enabled, hb_pb_cat_dur should be modified for all winning bids",
+			args: args{
+				bidRequest: &openrtb2.BidRequest{
+					ID: "some-request-id",
+					Imp: []openrtb2.Imp{
+						{
+							ID:  "imp_id1",
+							Ext: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`),
+						},
+						{
+							ID:  "imp_id1",
+							Ext: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`),
+						},
+					},
+				},
+				auc: &auction{
+					winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+						"imp_id1": {
+							openrtb_ext.BidderName("appnexus"): {
+								&entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, 5, false, "", 0, "USD", ""},
+								&entities.PbsOrtbBid{&openrtb2.Bid{ID: "789101"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, 5, false, "", 0, "USD", ""},
+							},
+						},
+					},
+				},
+				bidCategory: map[string]string{
+					"123456": "12.00_movies_30s",
+					"789101": "12.00_movies_30s",
+				},
+				multiBid: map[string]openrtb_ext.ExtMultiBid{
+					"appnexus": {
+						TargetBidderCodePrefix: "appN",
+						MaxBids:                ptrutil.ToPtr(2),
+					},
+				},
+			},
+			want: want{
+				errs: []error{},
+				expectedHbPbCatDur: map[string]map[string][]string{
+					"imp_id1": {
+						"appnexus": []string{"tier5_movies_30s", "tier5_movies_30s"},
+					},
+				},
+				expectedDealTierSatisfied: map[string]map[string][]bool{
+					"imp_id1": {
+						"appnexus": []bool{true, true},
+					},
+				},
+			},
+		},
+		{
+			name: "multibid enabled but TargetBidderCodePrefix not defined, hb_pb_cat_dur should be modified only for first bid",
+			args: args{
+				bidRequest: &openrtb2.BidRequest{
+					ID: "some-request-id",
+					Imp: []openrtb2.Imp{
+						{
+							ID:  "imp_id1",
+							Ext: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`),
+						},
+						{
+							ID:  "imp_id1",
+							Ext: json.RawMessage(`{"appnexus": {"dealTier": {"minDealTier": 5, "prefix": "tier"}, "placementId": 10433394}}`),
+						},
+					},
+				},
+				auc: &auction{
+					winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+						"imp_id1": {
+							openrtb_ext.BidderName("appnexus"): {
+								&entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, 5, false, "", 0, "USD", ""},
+								&entities.PbsOrtbBid{&openrtb2.Bid{ID: "789101"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, 5, false, "", 0, "USD", ""},
+							},
+						},
+					},
+				},
+				bidCategory: map[string]string{
+					"123456": "12.00_movies_30s",
+					"789101": "12.00_movies_30s",
+				},
+				multiBid: map[string]openrtb_ext.ExtMultiBid{
+					"appnexus": {
+						MaxBids: ptrutil.ToPtr(2),
+					},
+				},
+			},
+			want: want{
+				errs: []error{},
+				expectedHbPbCatDur: map[string]map[string][]string{
+					"imp_id1": {
+						"appnexus": []string{"tier5_movies_30s", "12.00_movies_30s"},
+					},
+				},
+				expectedDealTierSatisfied: map[string]map[string][]bool{
+					"imp_id1": {
+						"appnexus": []bool{true, false},
+					},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			errs := applyDealSupport(tt.args.bidRequest, tt.args.auc, tt.args.bidCategory, tt.args.multiBid)
+			assert.Equal(t, tt.want.errs, errs)
+
+			for impID, topBidsPerImp := range tt.args.auc.winningBidsByBidder {
+				for bidder, topBidsPerBidder := range topBidsPerImp {
+					for i, topBid := range topBidsPerBidder {
+						assert.Equal(t, tt.want.expectedHbPbCatDur[impID][bidder.String()][i], tt.args.bidCategory[topBid.Bid.ID], tt.name)
+						assert.Equal(t, tt.want.expectedDealTierSatisfied[impID][bidder.String()][i], topBid.DealTierSatisfied, tt.name)
+					}
+				}
+			}
+		})
+	}
+}
+
 func TestGetDealTiers(t *testing.T) {
 	testCases := []struct {
 		description string
@@ -3661,7 +3868,7 @@ func TestUpdateHbPbCatDur(t *testing.T) {
 	}
 
 	for _, test := range testCases {
-		bid := entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, test.dealPriority, false, "", 0, "USD"}
+		bid := entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, test.dealPriority, false, "", 0, "USD", ""}
 		bidCategory := map[string]string{
 			bid.Bid.ID: test.targ["hb_pb_cat_dur"],
 		}
@@ -4756,6 +4963,12 @@ type exchangeSpec struct {
 	HostConfigBidValidation    config.Validations     `json:"host_bid_validations"`
 	AccountConfigBidValidation config.Validations     `json:"account_bid_validations"`
 	FledgeEnabled              bool                   `json:"fledge_enabled,omitempty"`
+	MultiBid                   *multiBidSpec          `json:"multiBid,omitempty"`
+}
+
+type multiBidSpec struct {
+	AccountMaxBid          int  `json:"default_bid_limit"`
+	AssertMultiBidWarnings bool `json:"assert_multi_bid_warnings"`
 }
 
 type exchangeRequest struct {
@@ -4934,11 +5147,12 @@ func mapifySeatBids(t *testing.T, context string, seatBids []openrtb2.SeatBid) m
 			// The sequence of extra bids for same seat from different bidder is not guaranteed as we randomize the list of adapters
 			// This is w.r.t changes at exchange.go#561 (club bids from different bidders for same extra-bid)
 			sort.Slice(seatBids[i].Bid, func(x, y int) bool {
-				return seatBids[i].Bid[x].Price > seatBids[i].Bid[y].Price
+				return isNewWinningBid(&seatBids[i].Bid[x], &seatBids[i].Bid[y], true)
 			})
 			seatMap[seatName] = &seatBids[i]
 		}
 	}
+
 	return seatMap
 }
 
diff --git a/exchange/exchangetest/multi-bid-default-bid-limit.json b/exchange/exchangetest/multi-bid-default-bid-limit.json
new file mode 100644
index 00000000..ccc33621
--- /dev/null
+++ b/exchange/exchangetest/multi-bid-default-bid-limit.json
@@ -0,0 +1,707 @@
+{
+  "multiBid": {
+    "default_bid_limit": 2
+  },
+  "incomingRequest": {
+    "ortbRequest": {
+      "id": "tid",
+      "imp": [
+        {
+          "id": "impId1",
+          "video": {
+            "mimes": [
+              "mimes"
+            ],
+            "minduration": 20,
+            "maxduration": 60,
+            "protocols": [
+              1
+            ],
+            "w": 300,
+            "h": 250,
+            "startdelay": 5,
+            "skipmin": 0,
+            "skipafter": 0,
+            "playbackmethod": [
+              1
+            ]
+          },
+          "ext": {
+            "prebid": {
+              "bidder": {
+                "rubicon": {
+                  "accountId": 2001,
+                  "siteId": 3001,
+                  "inventory": {
+                    "rating": [
+                      "5-star"
+                    ],
+                    "prodtype": [
+                      "tech"
+                    ]
+                  },
+                  "video": {
+                    "size_id": 15,
+                    "playerWidth": 780,
+                    "playerHeight": "438",
+                    "skip": 5,
+                    "skipdelay": 1
+                  }
+                },
+                "appnexus": {
+                  "traffic_source_code": "trafficSource"
+                }
+              }
+            }
+          }
+        }
+      ],
+      "device": {
+        "pxratio": 4.2,
+        "dnt": 2,
+        "ip": "80.215.195.122",
+        "language": "en",
+        "ifa": "ifaId",
+        "ext": {
+          "prebid": {
+            "interstitial": {
+              "minwidthperc": 50,
+              "minheightperc": 60
+            }
+          }
+        }
+      },
+      "site": {
+        "publisher": {
+          "id": "5001"
+        }
+      },
+      "at": 1,
+      "tmax": 5000,
+      "cur": [
+        "USD"
+      ],
+      "source": {
+        "fd": 1,
+        "tid": "tid"
+      },
+      "ext": {
+        "prebid": {
+          "debug": false,
+          "currency": {
+            "rates": {
+              "EUR": {
+                "USD": 1.2406
+              },
+              "USD": {
+                "EUR": 0.811
+              }
+            },
+            "usepbsrates": false
+          },
+          "targeting": {
+            "includewinners": true,
+            "includebidderkeys": true,
+            "pricegranularity": {
+              "precision": 2,
+              "ranges": [
+                {
+                  "max": 20,
+                  "increment": 0.1
+                }
+              ]
+            }
+          },
+          "multibid": [
+            {
+              "bidder": "rubicon",
+              "maxbids": 2,
+              "targetbiddercodeprefix": "rubN"
+            },
+            {
+              "bidders": [
+                "appnexus",
+                "someBidder"
+              ],
+              "maxbids": 2
+            }
+          ],
+          "auctiontimestamp": 1000
+        }
+      },
+      "user": {
+        "ext": {
+          "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA"
+        }
+      },
+      "regs": {
+        "ext": {
+          "us_privacy": "1YNN"
+        }
+      }
+    }
+  },
+  "outgoingRequests": {
+    "appnexus": {
+      "expectRequest": {
+        "ortbRequest": {
+          "id": "tid",
+          "imp": [
+            {
+              "id": "impId1",
+              "video": {
+                "mimes": [
+                  "mimes"
+                ],
+                "minduration": 20,
+                "maxduration": 60,
+                "protocols": [
+                  1
+                ],
+                "w": 300,
+                "h": 250,
+                "startdelay": 5,
+                "skipmin": 0,
+                "skipafter": 0,
+                "playbackmethod": [
+                  1
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "traffic_source_code": "trafficSource"
+                }
+              }
+            }
+          ],
+          "site": {
+            "publisher": {
+              "id": "5001"
+            }
+          },
+          "device": {
+            "dnt": 2,
+            "ip": "80.215.195.122",
+            "pxratio": 4.2,
+            "language": "en",
+            "ifa": "ifaId",
+            "ext": {
+              "prebid": {
+                "interstitial": {
+                  "minwidthperc": 50,
+                  "minheightperc": 60
+                }
+              }
+            }
+          },
+          "user": {
+            "ext": {
+              "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA"
+            }
+          },
+          "at": 1,
+          "tmax": 5000,
+          "cur": [
+            "USD"
+          ],
+          "source": {
+            "fd": 1,
+            "tid": "tid"
+          },
+          "regs": {
+            "ext": {
+              "us_privacy": "1YNN"
+            }
+          },
+          "ext": {
+            "prebid": {
+              "currency": {
+                "rates": {
+                  "EUR": {
+                    "USD": 1.2406
+                  },
+                  "USD": {
+                    "EUR": 0.811
+                  }
+                },
+                "usepbsrates": false
+              },
+              "multibid": [
+                {
+                  "bidders": [
+                    "appnexus"
+                  ],
+                  "maxbids": 2
+                }
+              ],
+              "server": {
+                "externalurl": "http://hosturl.com",
+                "gvlid": 1,
+                "datacenter": "Datacenter"
+              }
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "pbsSeatBids": [
+          {
+            "pbsBids": [
+              {
+                "ortbBid": {
+                  "id": "7706636740145184841",
+                  "impid": "impId1",
+                  "price": 5.5,
+                  "adid": "29681110",
+                  "adm": "some-test-ad2",
+                  "adomain": [
+                    "appnexus.com"
+                  ],
+                  "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110",
+                  "cid": "958",
+                  "crid": "29681110",
+                  "ext": {
+                    "appnexus": {
+                      "brand_id": 1,
+                      "auction_id": 8189378542222915000,
+                      "bidder_id": 2,
+                      "bid_ad_type": 1,
+                      "ranking_price": 0
+                    }
+                  }
+                },
+                "bidType": "video",
+                "bidMeta": {
+                  "adaptercode": "appnexus"
+                }
+              },
+              {
+                "ortbBid": {
+                  "id": "928185755156387460",
+                  "impid": "impId1",
+                  "price": 1,
+                  "adid": "69595837",
+                  "adm": "some-test-ad1",
+                  "adomain": [
+                    "appnexus.com"
+                  ],
+                  "iurl": "http://nym1-ib.adnxs.com/cr?id=69595837",
+                  "cid": "958",
+                  "crid": "69595837",
+                  "ext": {
+                    "appnexus": {
+                      "brand_id": 1,
+                      "brand_category_id": 1,
+                      "auction_id": 5607483846416358000,
+                      "bidder_id": 2,
+                      "bid_ad_type": 1
+                    }
+                  }
+                },
+                "bidType": "video",
+                "bidMeta": {
+                  "adaptercode": "appnexus"
+                }
+              },
+              {
+                "ortbBid": {
+                  "id": "222214214214",
+                  "impid": "impId1",
+                  "price": 2,
+                  "adid": "69595837",
+                  "adomain": [
+                    "appnexus.com"
+                  ],
+                  "adm": "some-test-ad1",
+                  "iurl": "http://nym1-ib.adnxs.com/cr?id=69595837",
+                  "cid": "958",
+                  "crid": "69595837",
+                  "ext": {
+                    "appnexus": {
+                      "brand_id": 1,
+                      "brand_category_id": 1,
+                      "auction_id": 5607483846416358000,
+                      "bidder_id": 2,
+                      "bid_ad_type": 1
+                    }
+                  }
+                },
+                "bidType": "video",
+                "bidMeta": {
+                  "adaptercode": "appnexus"
+                }
+              }
+            ],
+            "seat": "appnexus"
+          }
+        ]
+      }
+    },
+    "rubicon": {
+      "expectRequest": {
+        "ortbRequest": {
+          "id": "tid",
+          "imp": [
+            {
+              "id": "impId1",
+              "video": {
+                "mimes": [
+                  "mimes"
+                ],
+                "minduration": 20,
+                "maxduration": 60,
+                "protocols": [
+                  1
+                ],
+                "w": 300,
+                "h": 250,
+                "startdelay": 5,
+                "skipmin": 0,
+                "skipafter": 0,
+                "playbackmethod": [
+                  1
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "accountId": 2001,
+                  "siteId": 3001,
+                  "inventory": {
+                    "rating": [
+                      "5-star"
+                    ],
+                    "prodtype": [
+                      "tech"
+                    ]
+                  },
+                  "video": {
+                    "size_id": 15,
+                    "playerWidth": 780,
+                    "playerHeight": "438",
+                    "skip": 5,
+                    "skipdelay": 1
+                  }
+                }
+              }
+            }
+          ],
+          "site": {
+            "publisher": {
+              "id": "5001"
+            }
+          },
+          "device": {
+            "dnt": 2,
+            "ip": "80.215.195.122",
+            "pxratio": 4.2,
+            "language": "en",
+            "ifa": "ifaId",
+            "ext": {
+              "prebid": {
+                "interstitial": {
+                  "minwidthperc": 50,
+                  "minheightperc": 60
+                }
+              }
+            }
+          },
+          "user": {
+            "ext": {
+              "consent": "CPBCa-mPBCa-mAAAAAENA0CAAEAAAAAAACiQAaQAwAAgAgABoAAAAAA"
+            }
+          },
+          "at": 1,
+          "tmax": 5000,
+          "cur": [
+            "USD"
+          ],
+          "source": {
+            "fd": 1,
+            "tid": "tid"
+          },
+          "regs": {
+            "ext": {
+              "us_privacy": "1YNN"
+            }
+          },
+          "ext": {
+            "prebid": {
+              "currency": {
+                "rates": {
+                  "EUR": {
+                    "USD": 1.2406
+                  },
+                  "USD": {
+                    "EUR": 0.811
+                  }
+                },
+                "usepbsrates": false
+              },
+              "multibid": [
+                {
+                  "bidder": "rubicon",
+                  "maxbids": 2,
+                  "targetbiddercodeprefix": "rubN"
+                }
+              ],
+              "server": {
+                "externalurl": "http://hosturl.com",
+                "gvlid": 1,
+                "datacenter": "Datacenter"
+              }
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "pbsSeatBids": [
+          {
+            "pbsBids": [
+              {
+                "ortbBid": {
+                  "id": "880290288",
+                  "impid": "impId1",
+                  "price": 8.43,
+                  "adm": "<InLine><Impression><![CDATA[]]></Impression><Impression><![CDATA[http://localhost:8080/event?t=imp&b=880290288&a=5001&aid=tid&ts=1000&bidder=rubicon&f=b&int=]]></Impression></InLine>",
+                  "crid": "crid-880290288",
+                  "w": 300,
+                  "h": 250,
+                  "ext": {
+                    "rubicon": {
+                      "targeting": [
+                        {
+                          "key": "rpfl_1001",
+                          "values": [
+                            "2_tier0100"
+                          ]
+                        }
+                      ]
+                    }
+                  }
+                },
+                "bidType": "video",
+                "bidMeta": {
+                  "adaptercode": "rubicon"
+                }
+              },
+              {
+                "ortbBid": {
+                  "id": "21521324",
+                  "impid": "impId1",
+                  "price": 12.43,
+                  "adm": "<InLine><Impression><![CDATA[]]></Impression><Impression><![CDATA[http://localhost:8080/event?t=imp&b=21521324&a=5001&aid=tid&ts=1000&bidder=rubicon&f=b&int=]]></Impression></InLine>",
+                  "crid": "crid-21521324",
+                  "w": 300,
+                  "h": 250,
+                  "ext": {
+                    "rubicon": {
+                      "targeting": [
+                        {
+                          "key": "rpfl_1001",
+                          "values": [
+                            "2_tier0100"
+                          ]
+                        }
+                      ]
+                    }
+                  }
+                },
+                "bidType": "video",
+                "bidMeta": {
+                  "adaptercode": "rubicon"
+                }
+              },
+              {
+                "ortbBid": {
+                  "id": "992342532",
+                  "impid": "impId1",
+                  "price": 2,
+                  "adm": "992342532-admn",
+                  "crid": "crid-992342532",
+                  "w": 300,
+                  "h": 250,
+                  "ext": {
+                    "rubicon": {
+                      "targeting": [
+                        {
+                          "key": "rpfl_1001",
+                          "values": [
+                            "2_tier0100"
+                          ]
+                        }
+                      ]
+                    }
+                  }
+                },
+                "bidType": "banner",
+                "bidMeta": {
+                  "adaptercode": "rubicon"
+                }
+              }
+            ],
+            "seat": "rubicon"
+          }
+        ]
+      }
+    }
+  },
+  "response": {
+    "bids": {
+      "id": "tid",
+      "seatbid": [
+        {
+          "bid": [
+            {
+              "id": "21521324",
+              "impid": "impId1",
+              "price": 12.43,
+              "adm": "<InLine><Impression><![CDATA[]]></Impression><Impression><![CDATA[http://localhost:8080/event?t=imp&b=21521324&a=5001&aid=tid&ts=1000&bidder=rubicon&f=b&int=]]></Impression></InLine>",
+              "crid": "crid-21521324",
+              "w": 300,
+              "h": 250,
+              "ext": {
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "rubicon"
+                  },
+                  "type": "video",
+                  "targeting": {
+                    "hb_size_rubicon": "300x250",
+                    "hb_cache_path_rubico": "/pbcache/endpoint",
+                    "hb_cache_host_rubico": "www.pbcserver.com",
+                    "hb_pb": "12.40",
+                    "hb_pb_rubicon": "12.40",
+                    "hb_cache_path": "/pbcache/endpoint",
+                    "hb_size": "300x250",
+                    "hb_bidder": "rubicon",
+                    "hb_bidder_rubicon": "rubicon",
+                    "hb_cache_host": "www.pbcserver.com"
+                  },
+                  "targetbiddercode": "rubicon"
+                },
+                "origbidcpm": 12.43,
+                "rubicon": {
+                  "targeting": [
+                    {
+                      "key": "rpfl_1001",
+                      "values": [
+                        "2_tier0100"
+                      ]
+                    }
+                  ]
+                }
+              }
+            },
+            {
+              "id": "880290288",
+              "impid": "impId1",
+              "price": 8.43,
+              "adm": "<InLine><Impression><![CDATA[]]></Impression><Impression><![CDATA[http://localhost:8080/event?t=imp&b=880290288&a=5001&aid=tid&ts=1000&bidder=rubicon&f=b&int=]]></Impression></InLine>",
+              "crid": "crid-880290288",
+              "w": 300,
+              "h": 250,
+              "ext": {
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "rubicon"
+                  },
+                  "type": "video",
+                  "targeting": {
+                    "hb_pb_rubN2": "8.40",
+                    "hb_cache_host_rubN2": "www.pbcserver.com",
+                    "hb_bidder_rubN2": "rubN2",
+                    "hb_cache_path_rubN2": "/pbcache/endpoint",
+                    "hb_size_rubN2": "300x250"
+                  },
+                  "targetbiddercode": "rubN2"
+                },
+                "origbidcpm": 8.43,
+                "rubicon": {
+                  "targeting": [
+                    {
+                      "key": "rpfl_1001",
+                      "values": [
+                        "2_tier0100"
+                      ]
+                    }
+                  ]
+                }
+              }
+            }
+          ],
+          "seat": "rubicon"
+        },
+        {
+          "bid": [
+            {
+              "id": "7706636740145184841",
+              "impid": "impId1",
+              "price": 5.5,
+              "adm": "some-test-ad2",
+              "adid": "29681110",
+              "adomain": [
+                "appnexus.com"
+              ],
+              "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110",
+              "cid": "958",
+              "crid": "29681110",
+              "ext": {
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "appnexus"
+                  },
+                  "type": "video",
+                  "targeting": {
+                    "hb_pb_appnexus": "5.50",
+                    "hb_cache_path_appnex": "/pbcache/endpoint",
+                    "hb_bidder_appnexus": "appnexus",
+                    "hb_cache_host_appnex": "www.pbcserver.com"
+                  },
+                  "targetbiddercode": "appnexus"
+                },
+                "origbidcpm": 5.5,
+                "appnexus": {
+                  "brand_id": 1,
+                  "auction_id": 8189378542222915000,
+                  "bidder_id": 2,
+                  "bid_ad_type": 1,
+                  "ranking_price": 0
+                }
+              }
+            },
+            {
+              "id": "222214214214",
+              "impid": "impId1",
+              "price": 2,
+              "adm": "some-test-ad1",
+              "adid": "69595837",
+              "adomain": [
+                "appnexus.com"
+              ],
+              "iurl": "http://nym1-ib.adnxs.com/cr?id=69595837",
+              "cid": "958",
+              "crid": "69595837",
+              "ext": {
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "appnexus"
+                  },
+                  "type": "video"
+                },
+                "origbidcpm": 2,
+                "appnexus": {
+                  "brand_id": 1,
+                  "brand_category_id": 1,
+                  "auction_id": 5607483846416358000,
+                  "bidder_id": 2,
+                  "bid_ad_type": 1
+                }
+              }
+            }
+          ],
+          "seat": "appnexus"
+        }
+      ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/exchange/exchangetest/multi-bids-error.json b/exchange/exchangetest/multi-bids-error.json
new file mode 100644
index 00000000..fd8eb6dc
--- /dev/null
+++ b/exchange/exchangetest/multi-bids-error.json
@@ -0,0 +1,534 @@
+{
+  "multiBid": {
+    "default_bid_limit": 3,
+    "assert_multi_bid_warnings": true
+  },
+  "incomingRequest": {
+    "ortbRequest": {
+      "id": "some-request-id",
+      "site": {
+        "page": "test.somepage.com"
+      },
+      "imp": [
+        {
+          "id": "my-imp-id",
+          "video": {
+            "mimes": [
+              "video/mp4"
+            ]
+          },
+          "ext": {
+            "prebid": {
+              "bidder": {
+                "pubmatic": {
+                  "publisherId": 5890
+                },
+                "appnexus": {
+                  "placementId": 1
+                }
+              }
+            }
+          }
+        },
+        {
+          "id": "imp-id-2",
+          "video": {
+            "mimes": [
+              "video/mp4"
+            ]
+          },
+          "ext": {
+            "prebid": {
+              "bidder": {
+                "pubmatic": {
+                  "publisherId": 5890
+                },
+                "appnexus": {
+                  "placementId": 1
+                }
+              }
+            }
+          }
+        }
+      ],
+      "ext": {
+        "prebid": {
+          "targeting": {
+            "includewinners": true,
+            "includebidderkeys": true,
+            "pricegranularity": {
+              "precision": 2,
+              "ranges": [
+                {
+                  "min": 0,
+                  "max": 20,
+                  "increment": 0.1
+                }
+              ]
+            }
+          },
+          "multibid": [
+            {
+              "bidder": "appnexus"
+            },
+            {
+              "maxbids": -1,
+              "bidder": "rubicon",
+              "targetbiddercodeprefix": "rubN"
+            },
+            {
+              "maxbids": 10,
+              "bidder": "pubmatic",
+              "targetbiddercodeprefix": "pm"
+            },
+            {
+              "maxbids": 4,
+              "bidder": "pubmatic",
+              "targetbiddercodeprefix": "pubM"
+            },
+            {
+              "maxbids": 5,
+              "bidder": "groupm",
+              "bidders": [
+                "someBidder"
+              ],
+              "targetbiddercodeprefix": "gm"
+            },
+            {
+              "maxbids": 6,
+              "bidders": [
+                "groupm"
+              ]
+            },
+            {
+              "maxbids": 7,
+              "bidders": [
+                "33across"
+              ],
+              "targetbiddercodeprefix": "abc"
+            },
+            {
+              "maxbids": 8,
+              "targetbiddercodeprefix": "xyz"
+            }
+          ]
+        }
+      }
+    }
+  },
+  "outgoingRequests": {
+    "pubmatic": {
+      "expectRequest": {
+        "ortbRequest": {
+          "id": "some-request-id",
+          "site": {
+            "page": "test.somepage.com"
+          },
+          "imp": [
+            {
+              "id": "my-imp-id",
+              "video": {
+                "mimes": [
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "publisherId": 5890
+                }
+              }
+            },
+            {
+              "id": "imp-id-2",
+              "video": {
+                "mimes": [
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "publisherId": 5890
+                }
+              }
+            }
+          ],
+          "ext": {
+            "prebid": {
+              "multibid": [
+                {
+                  "bidder": "pubmatic",
+                  "maxbids": 9,
+                  "targetbiddercodeprefix": "pm"
+                }
+              ],
+              "server": {
+                "datacenter": "Datacenter",
+                "externalurl": "http://hosturl.com",
+                "gvlid": 1
+              }
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "pbsSeatBids": [
+          {
+            "pbsBids": [
+              {
+                "ortbBid": {
+                  "id": "winning-bid",
+                  "impid": "my-imp-id",
+                  "price": 0.71,
+                  "w": 200,
+                  "h": 250,
+                  "crid": "creative-1"
+                },
+                "bidType": "video",
+                "bidMeta": {
+                  "adaptercode": "pubmatic"
+                }
+              },
+              {
+                "ortbBid": {
+                  "id": "losing-bid",
+                  "impid": "my-imp-id",
+                  "price": 0.21,
+                  "w": 200,
+                  "h": 250,
+                  "crid": "creative-2"
+                },
+                "bidType": "video",
+                "bidMeta": {
+                  "adaptercode": "pubmatic"
+                }
+              },
+              {
+                "ortbBid": {
+                  "id": "other-bid",
+                  "impid": "imp-id-2",
+                  "price": 0.61,
+                  "w": 300,
+                  "h": 500,
+                  "crid": "creative-3"
+                },
+                "bidType": "video",
+                "bidMeta": {
+                  "adaptercode": "pubmatic"
+                }
+              },
+              {
+                "ortbBid": {
+                  "id": "contending-bid",
+                  "impid": "my-imp-id",
+                  "price": 0.51,
+                  "w": 200,
+                  "h": 250,
+                  "crid": "creative-4"
+                },
+                "bidType": "video",
+                "bidMeta": {
+                  "adaptercode": "pubmatic"
+                }
+              }
+            ],
+            "seat": "pubmatic"
+          }
+        ]
+      }
+    },
+    "appnexus": {
+      "expectRequest": {
+        "ortbRequest": {
+          "id": "some-request-id",
+          "site": {
+            "page": "test.somepage.com"
+          },
+          "imp": [
+            {
+              "id": "my-imp-id",
+              "video": {
+                "mimes": [
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "placementId": 1
+                }
+              }
+            },
+            {
+              "id": "imp-id-2",
+              "video": {
+                "mimes": [
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "placementId": 1
+                }
+              }
+            }
+          ],
+          "ext": {
+            "prebid": {
+              "server": {
+                "datacenter": "Datacenter",
+                "externalurl": "http://hosturl.com",
+                "gvlid": 1
+              }
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "pbsSeatBids": [
+          {
+            "pbsBids": [
+              {
+                "ortbBid": {
+                  "id": "apn-bid",
+                  "impid": "my-imp-id",
+                  "price": 0.3,
+                  "w": 200,
+                  "h": 500,
+                  "crid": "creative-a-1"
+                },
+                "bidType": "banner",
+                "bidMeta": {
+                  "adaptercode": "appnexus"
+                }
+              },
+              {
+                "ortbBid": {
+                  "id": "apn-bid-2",
+                  "impid": "my-imp-id",
+                  "price": 0.3,
+                  "w": 200,
+                  "h": 500,
+                  "crid": "creative-a-2"
+                },
+                "bidType": "banner",
+                "bidMeta": {
+                  "adaptercode": "appnexus"
+                }
+              }
+            ],
+            "seat": "appnexus"
+          }
+        ]
+      }
+    }
+  },
+  "response": {
+    "bids": {
+      "id": "some-request-id",
+      "seatbid": [
+        {
+          "seat": "pubmatic",
+          "bid": [
+            {
+              "id": "winning-bid",
+              "impid": "my-imp-id",
+              "price": 0.71,
+              "w": 200,
+              "h": 250,
+              "crid": "creative-1",
+              "ext": {
+                "origbidcpm": 0.71,
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "pubmatic"
+                  },
+                  "type": "video",
+                  "targeting": {
+                    "hb_bidder": "pubmatic",
+                    "hb_bidder_pubmatic": "pubmatic",
+                    "hb_cache_host": "www.pbcserver.com",
+                    "hb_cache_host_pubmat": "www.pbcserver.com",
+                    "hb_cache_path": "/pbcache/endpoint",
+                    "hb_cache_path_pubmat": "/pbcache/endpoint",
+                    "hb_pb": "0.70",
+                    "hb_pb_pubmatic": "0.70",
+                    "hb_size": "200x250",
+                    "hb_size_pubmatic": "200x250"
+                  },
+                  "targetbiddercode": "pubmatic"
+                }
+              }
+            },
+            {
+              "id": "losing-bid",
+              "impid": "my-imp-id",
+              "price": 0.21,
+              "w": 200,
+              "h": 250,
+              "crid": "creative-2",
+              "ext": {
+                "origbidcpm": 0.21,
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "pubmatic"
+                  },
+                  "type": "video",
+                  "targeting": {
+                    "hb_bidder_pm3": "pm3",
+                    "hb_cache_host_pm3": "www.pbcserver.com",
+                    "hb_cache_path_pm3": "/pbcache/endpoint",
+                    "hb_pb_pm3": "0.20",
+                    "hb_size_pm3": "200x250"
+                  },
+                  "targetbiddercode": "pm3"
+                }
+              }
+            },
+            {
+              "id": "other-bid",
+              "impid": "imp-id-2",
+              "price": 0.61,
+              "w": 300,
+              "h": 500,
+              "crid": "creative-3",
+              "ext": {
+                "origbidcpm": 0.61,
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "pubmatic"
+                  },
+                  "type": "video",
+                  "targeting": {
+                    "hb_bidder": "pubmatic",
+                    "hb_bidder_pubmatic": "pubmatic",
+                    "hb_cache_host": "www.pbcserver.com",
+                    "hb_cache_host_pubmat": "www.pbcserver.com",
+                    "hb_cache_path": "/pbcache/endpoint",
+                    "hb_cache_path_pubmat": "/pbcache/endpoint",
+                    "hb_pb": "0.60",
+                    "hb_pb_pubmatic": "0.60",
+                    "hb_size": "300x500",
+                    "hb_size_pubmatic": "300x500"
+                  },
+                  "targetbiddercode": "pubmatic"
+                }
+              }
+            },
+            {
+              "id": "contending-bid",
+              "impid": "my-imp-id",
+              "price": 0.51,
+              "w": 200,
+              "h": 250,
+              "crid": "creative-4",
+              "ext": {
+                "origbidcpm": 0.51,
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "pubmatic"
+                  },
+                  "type": "video",
+                  "targeting": {
+                    "hb_bidder_pm2": "pm2",
+                    "hb_cache_host_pm2": "www.pbcserver.com",
+                    "hb_cache_path_pm2": "/pbcache/endpoint",
+                    "hb_pb_pm2": "0.50",
+                    "hb_size_pm2": "200x250"
+                  },
+                  "targetbiddercode": "pm2"
+                }
+              }
+            }
+          ]
+        },
+        {
+          "seat": "appnexus",
+          "bid": [
+            {
+              "id": "apn-bid",
+              "impid": "my-imp-id",
+              "price": 0.3,
+              "w": 200,
+              "h": 500,
+              "crid": "creative-a-1",
+              "ext": {
+                "origbidcpm": 0.3,
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "appnexus"
+                  },
+                  "type": "banner",
+                  "targeting": {
+                    "hb_bidder_appnexus": "appnexus",
+                    "hb_cache_host_appnex": "www.pbcserver.com",
+                    "hb_cache_path_appnex": "/pbcache/endpoint",
+                    "hb_pb_appnexus": "0.20",
+                    "hb_size_appnexus": "200x500"
+                  }
+                }
+              }
+            },
+            {
+              "id": "apn-bid-2",
+              "impid": "my-imp-id",
+              "price": 0.3,
+              "w": 200,
+              "h": 500,
+              "crid": "creative-a-2",
+              "ext": {
+                "origbidcpm": 0.3,
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "appnexus"
+                  },
+                  "type": "banner"
+                }
+              }
+            }
+          ]
+        }
+      ]
+    },
+    "ext": {
+      "warnings": {
+        "general": [
+          {
+            "code": 10002,
+            "message": "debug turned off for account"
+          },
+          {
+            "code": 10006,
+            "message": "maxBids not defined for {Bidder:appnexus, Bidders:[], MaxBids:<nil>, TargetBidderCodePrefix:}"
+          },
+          {
+            "code": 10006,
+            "message": "invalid maxBids value, using minimum 1 limit for {Bidder:rubicon, Bidders:[], MaxBids:-1, TargetBidderCodePrefix:rubN}"
+          },
+          {
+            "code": 10006,
+            "message": "invalid maxBids value, using maximum 9 limit for {Bidder:pubmatic, Bidders:[], MaxBids:10, TargetBidderCodePrefix:pm}"
+          },
+          {
+            "code": 10006,
+            "message": "multiBid already defined for pubmatic, ignoring this instance {Bidder:pubmatic, Bidders:[], MaxBids:4, TargetBidderCodePrefix:pubM}"
+          },
+          {
+            "code": 10006,
+            "message": "ignoring bidders from {Bidder:groupm, Bidders:[someBidder], MaxBids:5, TargetBidderCodePrefix:gm}"
+          },
+          {
+            "code": 10006,
+            "message": "multiBid already defined for groupm, ignoring this instance {Bidder:, Bidders:[groupm], MaxBids:6, TargetBidderCodePrefix:}"
+          },
+          {
+            "code": 10006,
+            "message": "ignoring targetbiddercodeprefix for {Bidder:, Bidders:[33across], MaxBids:7, TargetBidderCodePrefix:abc}"
+          },
+          {
+            "code": 10006,
+            "message": "bidder(s) not specified for {Bidder:, Bidders:[], MaxBids:8, TargetBidderCodePrefix:xyz}"
+          }
+        ]
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/exchange/exchangetest/multi-bids-with-extra-bids.json b/exchange/exchangetest/multi-bids-with-extra-bids.json
new file mode 100644
index 00000000..247eb8bb
--- /dev/null
+++ b/exchange/exchangetest/multi-bids-with-extra-bids.json
@@ -0,0 +1,489 @@
+{
+  "multiBid": {},
+  "incomingRequest": {
+    "ortbRequest": {
+      "id": "some-request-id",
+      "site": {
+        "page": "test.somepage.com"
+      },
+      "imp": [
+        {
+          "id": "my-imp-id",
+          "video": {
+            "mimes": [
+              "video/mp4"
+            ]
+          },
+          "ext": {
+            "prebid": {
+              "bidder": {
+                "pubmatic": {
+                  "publisherId": 5890
+                },
+                "appnexus": {
+                  "placementId": 1
+                }
+              }
+            }
+          }
+        },
+        {
+          "id": "imp-id-2",
+          "video": {
+            "mimes": [
+              "video/mp4"
+            ]
+          },
+          "ext": {
+            "prebid": {
+              "bidder": {
+                "pubmatic": {
+                  "publisherId": 5890
+                },
+                "appnexus": {
+                  "placementId": 1
+                }
+              }
+            }
+          }
+        }
+      ],
+      "ext": {
+        "prebid": {
+          "targeting": {
+            "includewinners": true,
+            "includebidderkeys": true,
+            "pricegranularity": {
+              "precision": 2,
+              "ranges": [
+                {
+                  "min": 0,
+                  "max": 20,
+                  "increment": 0.1
+                }
+              ]
+            }
+          },
+          "alternatebiddercodes": {
+            "enabled": true,
+            "bidders": {
+              "pubmatic": {
+                "enabled": true,
+                "allowedbiddercodes": [
+                  "groupm"
+                ]
+              }
+            }
+          },
+          "multibid": [
+            {
+              "bidder": "pubmatic",
+              "maxbids": 2,
+              "targetbiddercodeprefix": "pubm"
+            },
+            {
+              "bidders": [
+                "appnexus",
+                "groupm"
+              ],
+              "maxbids": 2
+            }
+          ]
+        }
+      }
+    }
+  },
+  "outgoingRequests": {
+    "pubmatic": {
+      "expectRequest": {
+        "ortbRequest": {
+          "id": "some-request-id",
+          "site": {
+            "page": "test.somepage.com"
+          },
+          "imp": [
+            {
+              "id": "my-imp-id",
+              "video": {
+                "mimes": [
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "publisherId": 5890
+                }
+              }
+            },
+            {
+              "id": "imp-id-2",
+              "video": {
+                "mimes": [
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "publisherId": 5890
+                }
+              }
+            }
+          ],
+          "ext": {
+            "prebid": {
+              "alternatebiddercodes": {
+                "enabled": true,
+                "bidders": {
+                  "pubmatic": {
+                    "enabled": true,
+                    "allowedbiddercodes": [
+                      "groupm"
+                    ]
+                  }
+                }
+              },
+              "multibid": [
+                {
+                  "bidder": "pubmatic",
+                  "maxbids": 2,
+                  "targetbiddercodeprefix": "pubm"
+                },
+                {
+                  "bidders": ["groupm"],
+                  "maxbids": 2
+                }
+              ],
+              "server": {
+                "datacenter": "Datacenter",
+                "externalurl": "http://hosturl.com",
+                "gvlid": 1
+              } 
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "pbsSeatBids": [
+          {
+            "pbsBids": [
+              {
+                "ortbBid": {
+                  "id": "winning-bid",
+                  "impid": "my-imp-id",
+                  "price": 0.71,
+                  "w": 200,
+                  "h": 250,
+                  "crid": "creative-1"
+                },
+                "bidType": "video",
+                "bidMeta": {
+                  "adaptercode": "pubmatic"
+                }
+              },
+              {
+                "ortbBid": {
+                  "id": "losing-bid",
+                  "impid": "my-imp-id",
+                  "price": 0.21,
+                  "w": 200,
+                  "h": 250,
+                  "crid": "creative-2"
+                },
+                "bidType": "video",
+                "bidMeta": {
+                  "adaptercode": "pubmatic"
+                }
+              },
+              {
+                "ortbBid": {
+                  "id": "other-bid",
+                  "impid": "imp-id-2",
+                  "price": 0.61,
+                  "w": 300,
+                  "h": 500,
+                  "crid": "creative-3"
+                },
+                "bidType": "video",
+                "bidMeta": {
+                  "adaptercode": "pubmatic"
+                }
+              },
+              {
+                "ortbBid": {
+                  "id": "contending-bid",
+                  "impid": "my-imp-id",
+                  "price": 0.51,
+                  "w": 200,
+                  "h": 250,
+                  "crid": "creative-4"
+                },
+                "bidType": "video",
+                "bidMeta": {
+                  "adaptercode": "pubmatic"
+                }
+              }
+            ],
+            "seat": "pubmatic"
+          }
+        ]
+      }
+    },
+    "appnexus": {
+      "expectRequest": {
+        "ortbRequest": {
+          "id": "some-request-id",
+          "site": {
+            "page": "test.somepage.com"
+          },
+          "imp": [
+            {
+              "id": "my-imp-id",
+              "video": {
+                "mimes": [
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "placementId": 1
+                }
+              }
+            },
+            {
+              "id": "imp-id-2",
+              "video": {
+                "mimes": [
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "placementId": 1
+                }
+              }
+            }
+          ],
+          "ext": {
+            "prebid": {
+              "alternatebiddercodes": {
+                "enabled": true,
+                "bidders": null
+              },
+              "multibid": [
+                {
+                  "bidders": [
+                    "appnexus"
+                  ],
+                  "maxbids": 2
+                }
+              ],
+              "server": {
+                "datacenter": "Datacenter",
+                "externalurl": "http://hosturl.com",
+                "gvlid": 1
+              }
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "pbsSeatBids": [
+          {
+            "pbsBids": [
+              {
+                "ortbBid": {
+                  "id": "apn-bid",
+                  "impid": "my-imp-id",
+                  "price": 0.3,
+                  "w": 200,
+                  "h": 500,
+                  "crid": "creative-a-1"
+                },
+                "bidType": "banner",
+                "bidMeta": {
+                  "adaptercode": "appnexus"
+                }
+              },
+              {
+                "ortbBid": {
+                  "id": "apn-bid-2",
+                  "impid": "my-imp-id",
+                  "price": 0.3,
+                  "w": 200,
+                  "h": 500,
+                  "crid": "creative-a-2"
+                },
+                "bidType": "banner",
+                "bidMeta": {
+                  "adaptercode": "appnexus"
+                }
+              }
+            ],
+            "seat": "appnexus"
+          }
+        ]
+      }
+    }
+  },
+  "response": {
+    "bids": {
+      "id": "some-request-id",
+      "seatbid": [
+        {
+          "seat": "pubmatic",
+          "bid": [
+            {
+              "id": "winning-bid",
+              "impid": "my-imp-id",
+              "price": 0.71,
+              "w": 200,
+              "h": 250,
+              "crid": "creative-1",
+              "ext": {
+                "origbidcpm": 0.71,
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "pubmatic"
+                  },
+                  "type": "video",
+                  "targeting": {
+                    "hb_bidder": "pubmatic",
+                    "hb_bidder_pubmatic": "pubmatic",
+                    "hb_cache_host": "www.pbcserver.com",
+                    "hb_cache_host_pubmat": "www.pbcserver.com",
+                    "hb_cache_path": "/pbcache/endpoint",
+                    "hb_cache_path_pubmat": "/pbcache/endpoint",
+                    "hb_pb": "0.70",
+                    "hb_pb_pubmatic": "0.70",
+                    "hb_size": "200x250",
+                    "hb_size_pubmatic": "200x250"
+                  },
+                  "targetbiddercode": "pubmatic"
+                }
+              }
+            },
+            {
+              "id": "losing-bid",
+              "impid": "my-imp-id",
+              "price": 0.21,
+              "w": 200,
+              "h": 250,
+              "crid": "creative-2",
+              "ext": {
+                "origbidcpm": 0.21,
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "pubmatic"
+                  },
+                  "type": "video"
+                }
+              }
+            },
+            {
+              "id": "other-bid",
+              "impid": "imp-id-2",
+              "price": 0.61,
+              "w": 300,
+              "h": 500,
+              "crid": "creative-3",
+              "ext": {
+                "origbidcpm": 0.61,
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "pubmatic"
+                  },
+                  "type": "video",
+                  "targeting": {
+                    "hb_bidder": "pubmatic",
+                    "hb_bidder_pubmatic": "pubmatic",
+                    "hb_cache_host": "www.pbcserver.com",
+                    "hb_cache_host_pubmat": "www.pbcserver.com",
+                    "hb_cache_path": "/pbcache/endpoint",
+                    "hb_cache_path_pubmat": "/pbcache/endpoint",
+                    "hb_pb": "0.60",
+                    "hb_pb_pubmatic": "0.60",
+                    "hb_size": "300x500",
+                    "hb_size_pubmatic": "300x500"
+                  },
+                  "targetbiddercode": "pubmatic"
+                }
+              }
+            },
+            {
+              "id": "contending-bid",
+              "impid": "my-imp-id",
+              "price": 0.51,
+              "w": 200,
+              "h": 250,
+              "crid": "creative-4",
+              "ext": {
+                "origbidcpm": 0.51,
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "pubmatic"
+                  },
+                  "type": "video",
+                  "targeting": {
+                    "hb_bidder_pubm2": "pubm2",
+                    "hb_cache_host_pubm2": "www.pbcserver.com",
+                    "hb_cache_path_pubm2": "/pbcache/endpoint",
+                    "hb_pb_pubm2": "0.50",
+                    "hb_size_pubm2": "200x250"
+                  },
+                  "targetbiddercode": "pubm2"
+                }
+              }
+            }
+          ]
+        },
+        {
+          "seat": "appnexus",
+          "bid": [
+            {
+              "id": "apn-bid",
+              "impid": "my-imp-id",
+              "price": 0.3,
+              "w": 200,
+              "h": 500,
+              "crid": "creative-a-1",
+              "ext": {
+                "origbidcpm": 0.3,
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "appnexus"
+                  },
+                  "targetbiddercode": "appnexus",
+                  "type": "banner",
+                  "targeting": {
+                    "hb_bidder_appnexus": "appnexus",
+                    "hb_cache_host_appnex": "www.pbcserver.com",
+                    "hb_cache_path_appnex": "/pbcache/endpoint",
+                    "hb_pb_appnexus": "0.20",
+                    "hb_size_appnexus": "200x500"
+                  }
+                }
+              }
+            },
+            {
+              "id": "apn-bid-2",
+              "impid": "my-imp-id",
+              "price": 0.3,
+              "w": 200,
+              "h": 500,
+              "crid": "creative-a-2",
+              "ext": {
+                "origbidcpm": 0.3,
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "appnexus"
+                  },
+                  "type": "banner"
+                }
+              }
+            }
+          ]
+        }
+      ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/exchange/exchangetest/multi-bids.json b/exchange/exchangetest/multi-bids.json
new file mode 100644
index 00000000..7d76bf95
--- /dev/null
+++ b/exchange/exchangetest/multi-bids.json
@@ -0,0 +1,459 @@
+{
+  "multiBid": {},
+  "incomingRequest": {
+    "ortbRequest": {
+      "id": "some-request-id",
+      "site": {
+        "page": "test.somepage.com"
+      },
+      "imp": [
+        {
+          "id": "my-imp-id",
+          "video": {
+            "mimes": [
+              "video/mp4"
+            ]
+          },
+          "ext": {
+            "prebid": {
+              "bidder": {
+                "pubmatic": {
+                  "publisherId": 5890
+                },
+                "appnexus": {
+                  "placementId": 1
+                }
+              }
+            }
+          }
+        },
+        {
+          "id": "imp-id-2",
+          "video": {
+            "mimes": [
+              "video/mp4"
+            ]
+          },
+          "ext": {
+            "prebid": {
+              "bidder": {
+                "pubmatic": {
+                  "publisherId": 5890
+                },
+                "appnexus": {
+                  "placementId": 1
+                }
+              }
+            }
+          }
+        }
+      ],
+      "ext": {
+        "prebid": {
+          "targeting": {
+            "includewinners": true,
+            "includebidderkeys": true,
+            "pricegranularity": {
+              "precision": 2,
+              "ranges": [
+                {
+                  "min": 0,
+                  "max": 20,
+                  "increment": 0.1
+                }
+              ]
+            }
+          },
+          "multibid": [
+            {
+              "bidder": "pubmatic",
+              "maxbids": 2,
+              "targetbiddercodeprefix": "pubm"
+            },
+            {
+              "bidders": [
+                "appnexus",
+                "someBidder"
+              ],
+              "maxbids": 2
+            }
+          ]
+        }
+      }
+    }
+  },
+  "outgoingRequests": {
+    "pubmatic": {
+      "expectRequest": {
+        "ortbRequest": {
+          "id": "some-request-id",
+          "site": {
+            "page": "test.somepage.com"
+          },
+          "imp": [
+            {
+              "id": "my-imp-id",
+              "video": {
+                "mimes": [
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "publisherId": 5890
+                }
+              }
+            },
+            {
+              "id": "imp-id-2",
+              "video": {
+                "mimes": [
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "publisherId": 5890
+                }
+              }
+            }
+          ],
+          "ext": {
+            "prebid": {
+              "multibid": [
+                {
+                  "bidder": "pubmatic",
+                  "maxbids": 2,
+                  "targetbiddercodeprefix": "pubm"
+                }
+              ],
+              "server": {
+                "datacenter": "Datacenter",
+                "externalurl": "http://hosturl.com",
+                "gvlid": 1
+              } 
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "pbsSeatBids": [
+          {
+            "pbsBids": [
+              {
+                "ortbBid": {
+                  "id": "winning-bid",
+                  "impid": "my-imp-id",
+                  "price": 0.71,
+                  "w": 200,
+                  "h": 250,
+                  "crid": "creative-1"
+                },
+                "bidType": "video",
+                "bidMeta": {
+                  "adaptercode": "pubmatic"
+                }
+              },
+              {
+                "ortbBid": {
+                  "id": "losing-bid",
+                  "impid": "my-imp-id",
+                  "price": 0.21,
+                  "w": 200,
+                  "h": 250,
+                  "crid": "creative-2"
+                },
+                "bidType": "video",
+                "bidMeta": {
+                  "adaptercode": "pubmatic"
+                }
+              },
+              {
+                "ortbBid": {
+                  "id": "other-bid",
+                  "impid": "imp-id-2",
+                  "price": 0.61,
+                  "w": 300,
+                  "h": 500,
+                  "crid": "creative-3"
+                },
+                "bidType": "video",
+                "bidMeta": {
+                  "adaptercode": "pubmatic"
+                }
+              },
+              {
+                "ortbBid": {
+                  "id": "contending-bid",
+                  "impid": "my-imp-id",
+                  "price": 0.51,
+                  "w": 200,
+                  "h": 250,
+                  "crid": "creative-4"
+                },
+                "bidType": "video",
+                "bidMeta": {
+                  "adaptercode": "pubmatic"
+                }
+              }
+            ],
+            "seat": "pubmatic"
+          }
+        ]
+      }
+    },
+    "appnexus": {
+      "expectRequest": {
+        "ortbRequest": {
+          "id": "some-request-id",
+          "site": {
+            "page": "test.somepage.com"
+          },
+          "imp": [
+            {
+              "id": "my-imp-id",
+              "video": {
+                "mimes": [
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "placementId": 1
+                }
+              }
+            },
+            {
+              "id": "imp-id-2",
+              "video": {
+                "mimes": [
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "placementId": 1
+                }
+              }
+            }
+          ],
+          "ext": {
+            "prebid": {
+              "multibid": [
+                {
+                  "bidders": [
+                    "appnexus"
+                  ],
+                  "maxbids": 2
+                }
+              ],
+              "server": {
+                "datacenter": "Datacenter",
+                "externalurl": "http://hosturl.com",
+                "gvlid": 1
+              }
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "pbsSeatBids": [
+          {
+            "pbsBids": [
+              {
+                "ortbBid": {
+                  "id": "apn-bid",
+                  "impid": "my-imp-id",
+                  "price": 0.3,
+                  "w": 200,
+                  "h": 500,
+                  "crid": "creative-a-1"
+                },
+                "bidType": "banner",
+                "bidMeta": {
+                  "adaptercode": "appnexus"
+                }
+              },
+              {
+                "ortbBid": {
+                  "id": "apn-bid-2",
+                  "impid": "my-imp-id",
+                  "price": 0.3,
+                  "w": 200,
+                  "h": 500,
+                  "crid": "creative-a-2"
+                },
+                "bidType": "banner",
+                "bidMeta": {
+                  "adaptercode": "appnexus"
+                }
+              }
+            ],
+            "seat": "appnexus"
+          }
+        ]
+      }
+    }
+  },
+  "response": {
+    "bids": {
+      "id": "some-request-id",
+      "seatbid": [
+        {
+          "seat": "pubmatic",
+          "bid": [
+            {
+              "id": "winning-bid",
+              "impid": "my-imp-id",
+              "price": 0.71,
+              "w": 200,
+              "h": 250,
+              "crid": "creative-1",
+              "ext": {
+                "origbidcpm": 0.71,
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "pubmatic"
+                  },
+                  "type": "video",
+                  "targeting": {
+                    "hb_bidder": "pubmatic",
+                    "hb_bidder_pubmatic": "pubmatic",
+                    "hb_cache_host": "www.pbcserver.com",
+                    "hb_cache_host_pubmat": "www.pbcserver.com",
+                    "hb_cache_path": "/pbcache/endpoint",
+                    "hb_cache_path_pubmat": "/pbcache/endpoint",
+                    "hb_pb": "0.70",
+                    "hb_pb_pubmatic": "0.70",
+                    "hb_size": "200x250",
+                    "hb_size_pubmatic": "200x250"
+                  },
+                  "targetbiddercode": "pubmatic"
+                }
+              }
+            },
+            {
+              "id": "losing-bid",
+              "impid": "my-imp-id",
+              "price": 0.21,
+              "w": 200,
+              "h": 250,
+              "crid": "creative-2",
+              "ext": {
+                "origbidcpm": 0.21,
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "pubmatic"
+                  },
+                  "type": "video"
+                }
+              }
+            },
+            {
+              "id": "other-bid",
+              "impid": "imp-id-2",
+              "price": 0.61,
+              "w": 300,
+              "h": 500,
+              "crid": "creative-3",
+              "ext": {
+                "origbidcpm": 0.61,
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "pubmatic"
+                  },
+                  "type": "video",
+                  "targeting": {
+                    "hb_bidder": "pubmatic",
+                    "hb_bidder_pubmatic": "pubmatic",
+                    "hb_cache_host": "www.pbcserver.com",
+                    "hb_cache_host_pubmat": "www.pbcserver.com",
+                    "hb_cache_path": "/pbcache/endpoint",
+                    "hb_cache_path_pubmat": "/pbcache/endpoint",
+                    "hb_pb": "0.60",
+                    "hb_pb_pubmatic": "0.60",
+                    "hb_size": "300x500",
+                    "hb_size_pubmatic": "300x500"
+                  },
+                  "targetbiddercode": "pubmatic"
+                }
+              }
+            },
+            {
+              "id": "contending-bid",
+              "impid": "my-imp-id",
+              "price": 0.51,
+              "w": 200,
+              "h": 250,
+              "crid": "creative-4",
+              "ext": {
+                "origbidcpm": 0.51,
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "pubmatic"
+                  },
+                  "type": "video",
+                  "targeting": {
+                    "hb_bidder_pubm2": "pubm2",
+                    "hb_cache_host_pubm2": "www.pbcserver.com",
+                    "hb_cache_path_pubm2": "/pbcache/endpoint",
+                    "hb_pb_pubm2": "0.50",
+                    "hb_size_pubm2": "200x250"
+                  },
+                  "targetbiddercode": "pubm2"
+                }
+              }
+            }
+          ]
+        },
+        {
+          "seat": "appnexus",
+          "bid": [
+            {
+              "id": "apn-bid",
+              "impid": "my-imp-id",
+              "price": 0.3,
+              "w": 200,
+              "h": 500,
+              "crid": "creative-a-1",
+              "ext": {
+                "origbidcpm": 0.3,
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "appnexus"
+                  },
+                  "targetbiddercode": "appnexus",
+                  "type": "banner",
+                  "targeting": {
+                    "hb_bidder_appnexus": "appnexus",
+                    "hb_cache_host_appnex": "www.pbcserver.com",
+                    "hb_cache_path_appnex": "/pbcache/endpoint",
+                    "hb_pb_appnexus": "0.20",
+                    "hb_size_appnexus": "200x500"
+                  }
+                }
+              }
+            },
+            {
+              "id": "apn-bid-2",
+              "impid": "my-imp-id",
+              "price": 0.3,
+              "w": 200,
+              "h": 500,
+              "crid": "creative-a-2",
+              "ext": {
+                "origbidcpm": 0.3,
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "appnexus"
+                  },
+                  "type": "banner"
+                }
+              }
+            }
+          ]
+        }
+      ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/exchange/targeting_test.go b/exchange/targeting_test.go
index 19b6ea5e..9e124835 100644
--- a/exchange/targeting_test.go
+++ b/exchange/targeting_test.go
@@ -16,6 +16,7 @@ import (
 	"github.com/prebid/prebid-server/hooks/hookexecution"
 	metricsConfig "github.com/prebid/prebid-server/metrics/config"
 	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/prebid/prebid-server/util/ptrutil"
 
 	"github.com/prebid/openrtb/v17/openrtb2"
 	"github.com/stretchr/testify/assert"
@@ -265,13 +266,20 @@ func mockServer(w http.ResponseWriter, req *http.Request) {
 }
 
 type TargetingTestData struct {
-	Description                string
-	TargetData                 targetData
-	Auction                    auction
-	IsApp                      bool
-	CategoryMapping            map[string]string
-	ExpectedBidTargetsByBidder map[string]map[openrtb_ext.BidderName]map[string]string
-	TruncateTargetAttr         *int
+	Description        string
+	TargetData         targetData
+	Auction            auction
+	IsApp              bool
+	CategoryMapping    map[string]string
+	ExpectedPbsBids    map[string]map[openrtb_ext.BidderName][]ExpectedPbsBid
+	TruncateTargetAttr *int
+	MultiBidMap        map[string]openrtb_ext.ExtMultiBid
+	DefaultBidLimit    int
+}
+
+type ExpectedPbsBid struct {
+	BidTargets       map[string]string
+	TargetBidderCode string
 }
 
 var bid123 *openrtb2.Bid = &openrtb2.Bid{
@@ -286,6 +294,34 @@ var bid084 *openrtb2.Bid = &openrtb2.Bid{
 	Price: 0.84,
 }
 
+var bid1p001 *openrtb2.Bid = &openrtb2.Bid{
+	Price: 0.01,
+}
+
+var bid1p077 *openrtb2.Bid = &openrtb2.Bid{
+	Price: 0.77,
+}
+
+var bid1p120 *openrtb2.Bid = &openrtb2.Bid{
+	Price: 1.20,
+}
+
+var bid2p123 *openrtb2.Bid = &openrtb2.Bid{
+	Price: 1.23,
+}
+
+var bid2p144 *openrtb2.Bid = &openrtb2.Bid{
+	Price: 1.44,
+}
+
+var bid2p155 *openrtb2.Bid = &openrtb2.Bid{
+	Price: 1.55,
+}
+
+var bid2p166 *openrtb2.Bid = &openrtb2.Bid{
+	Price: 1.66,
+}
+
 var (
 	truncateTargetAttrValue10       int = 10
 	truncateTargetAttrValue5        int = 5
@@ -306,26 +342,30 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 			includeWinners:   true,
 		},
 		Auction: auction{
-			winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*entities.PbsOrtbBid{
+			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				"ImpId-1": {
-					openrtb_ext.BidderAppnexus: {
+					openrtb_ext.BidderAppnexus: {{
 						Bid:     bid123,
 						BidType: openrtb_ext.BidTypeBanner,
-					},
-					openrtb_ext.BidderRubicon: {
+					}},
+					openrtb_ext.BidderRubicon: {{
 						Bid:     bid084,
 						BidType: openrtb_ext.BidTypeBanner,
-					},
+					}},
 				},
 			},
 		},
-		ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{
+		ExpectedPbsBids: map[string]map[openrtb_ext.BidderName][]ExpectedPbsBid{
 			"ImpId-1": {
-				openrtb_ext.BidderAppnexus: {
-					"hb_bidder": "appnexus",
-					"hb_pb":     "1.20",
+				openrtb_ext.BidderAppnexus: []ExpectedPbsBid{
+					{
+						BidTargets: map[string]string{
+							"hb_bidder": "appnexus",
+							"hb_pb":     "1.20",
+						},
+					},
 				},
-				openrtb_ext.BidderRubicon: {},
+				openrtb_ext.BidderRubicon: []ExpectedPbsBid{},
 			},
 		},
 		TruncateTargetAttr: nil,
@@ -337,28 +377,36 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 			includeBidderKeys: true,
 		},
 		Auction: auction{
-			winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*entities.PbsOrtbBid{
+			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				"ImpId-1": {
-					openrtb_ext.BidderAppnexus: {
+					openrtb_ext.BidderAppnexus: {{
 						Bid:     bid123,
 						BidType: openrtb_ext.BidTypeBanner,
-					},
-					openrtb_ext.BidderRubicon: {
+					}},
+					openrtb_ext.BidderRubicon: {{
 						Bid:     bid084,
 						BidType: openrtb_ext.BidTypeBanner,
-					},
+					}},
 				},
 			},
 		},
-		ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{
+		ExpectedPbsBids: map[string]map[openrtb_ext.BidderName][]ExpectedPbsBid{
 			"ImpId-1": {
-				openrtb_ext.BidderAppnexus: {
-					"hb_bidder_appnexus": "appnexus",
-					"hb_pb_appnexus":     "1.20",
+				openrtb_ext.BidderAppnexus: []ExpectedPbsBid{
+					{
+						BidTargets: map[string]string{
+							"hb_bidder_appnexus": "appnexus",
+							"hb_pb_appnexus":     "1.20",
+						},
+					},
 				},
-				openrtb_ext.BidderRubicon: {
-					"hb_bidder_rubicon": "rubicon",
-					"hb_pb_rubicon":     "0.80",
+				openrtb_ext.BidderRubicon: []ExpectedPbsBid{
+					{
+						BidTargets: map[string]string{
+							"hb_bidder_rubicon": "rubicon",
+							"hb_pb_rubicon":     "0.80",
+						},
+					},
 				},
 			},
 		},
@@ -373,33 +421,41 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 			includeFormat:     true,
 		},
 		Auction: auction{
-			winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*entities.PbsOrtbBid{
+			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				"ImpId-1": {
-					openrtb_ext.BidderAppnexus: {
+					openrtb_ext.BidderAppnexus: {{
 						Bid:     bid123,
 						BidType: openrtb_ext.BidTypeBanner,
-					},
-					openrtb_ext.BidderRubicon: {
+					}},
+					openrtb_ext.BidderRubicon: {{
 						Bid:     bid084,
 						BidType: openrtb_ext.BidTypeBanner,
-					},
+					}},
 				},
 			},
 		},
-		ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{
+		ExpectedPbsBids: map[string]map[openrtb_ext.BidderName][]ExpectedPbsBid{
 			"ImpId-1": {
-				openrtb_ext.BidderAppnexus: {
-					"hb_bidder":          "appnexus",
-					"hb_bidder_appnexus": "appnexus",
-					"hb_pb":              "1.20",
-					"hb_pb_appnexus":     "1.20",
-					"hb_format":          "banner",
-					"hb_format_appnexus": "banner",
-				},
-				openrtb_ext.BidderRubicon: {
-					"hb_bidder_rubicon": "rubicon",
-					"hb_pb_rubicon":     "0.80",
-					"hb_format_rubicon": "banner",
+				openrtb_ext.BidderAppnexus: []ExpectedPbsBid{
+					{
+						BidTargets: map[string]string{
+							"hb_bidder":          "appnexus",
+							"hb_bidder_appnexus": "appnexus",
+							"hb_pb":              "1.20",
+							"hb_pb_appnexus":     "1.20",
+							"hb_format":          "banner",
+							"hb_format_appnexus": "banner",
+						},
+					},
+				},
+				openrtb_ext.BidderRubicon: []ExpectedPbsBid{
+					{
+						BidTargets: map[string]string{
+							"hb_bidder_rubicon": "rubicon",
+							"hb_pb_rubicon":     "0.80",
+							"hb_format_rubicon": "banner",
+						},
+					},
 				},
 			},
 		},
@@ -414,16 +470,16 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 			cachePath:         "cache",
 		},
 		Auction: auction{
-			winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*entities.PbsOrtbBid{
+			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				"ImpId-1": {
-					openrtb_ext.BidderAppnexus: {
+					openrtb_ext.BidderAppnexus: {{
 						Bid:     bid123,
 						BidType: openrtb_ext.BidTypeBanner,
-					},
-					openrtb_ext.BidderRubicon: {
+					}},
+					openrtb_ext.BidderRubicon: {{
 						Bid:     bid111,
 						BidType: openrtb_ext.BidTypeBanner,
-					},
+					}},
 				},
 			},
 			cacheIds: map[*openrtb2.Bid]string{
@@ -431,22 +487,30 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 				bid111: "cacheme",
 			},
 		},
-		ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{
+		ExpectedPbsBids: map[string]map[openrtb_ext.BidderName][]ExpectedPbsBid{
 			"ImpId-1": {
-				openrtb_ext.BidderAppnexus: {
-					"hb_bidder_appnexus":   "appnexus",
-					"hb_pb_appnexus":       "1.20",
-					"hb_cache_id_appnexus": "55555",
-					"hb_cache_host_appnex": "cache.prebid.com",
-					"hb_cache_path_appnex": "cache",
-				},
-				openrtb_ext.BidderRubicon: {
-					"hb_bidder_rubicon":    "rubicon",
-					"hb_pb_rubicon":        "1.10",
-					"hb_cache_id_rubicon":  "cacheme",
-					"hb_deal_rubicon":      "mydeal",
-					"hb_cache_host_rubico": "cache.prebid.com",
-					"hb_cache_path_rubico": "cache",
+				openrtb_ext.BidderAppnexus: []ExpectedPbsBid{
+					{
+						BidTargets: map[string]string{
+							"hb_bidder_appnexus":   "appnexus",
+							"hb_pb_appnexus":       "1.20",
+							"hb_cache_id_appnexus": "55555",
+							"hb_cache_host_appnex": "cache.prebid.com",
+							"hb_cache_path_appnex": "cache",
+						},
+					},
+				},
+				openrtb_ext.BidderRubicon: []ExpectedPbsBid{
+					{
+						BidTargets: map[string]string{
+							"hb_bidder_rubicon":    "rubicon",
+							"hb_pb_rubicon":        "1.10",
+							"hb_cache_id_rubicon":  "cacheme",
+							"hb_deal_rubicon":      "mydeal",
+							"hb_cache_host_rubico": "cache.prebid.com",
+							"hb_cache_path_rubico": "cache",
+						},
+					},
 				},
 			},
 		},
@@ -459,20 +523,24 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 			includeBidderKeys: true,
 		},
 		Auction: auction{
-			winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*entities.PbsOrtbBid{
+			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				"ImpId-1": {
-					openrtb_ext.BidderAppnexus: {
+					openrtb_ext.BidderAppnexus: {{
 						Bid:     bid123,
 						BidType: openrtb_ext.BidTypeBanner,
-					},
+					}},
 				},
 			},
 		},
-		ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{
+		ExpectedPbsBids: map[string]map[openrtb_ext.BidderName][]ExpectedPbsBid{
 			"ImpId-1": {
-				openrtb_ext.BidderAppnexus: {
-					"hb_bidder_appnexus": "appnexus",
-					"hb_pb_appnexus":     "1.20",
+				openrtb_ext.BidderAppnexus: []ExpectedPbsBid{
+					{
+						BidTargets: map[string]string{
+							"hb_bidder_appnexus": "appnexus",
+							"hb_pb_appnexus":     "1.20",
+						},
+					},
 				},
 			},
 		},
@@ -485,28 +553,36 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 			includeBidderKeys: true,
 		},
 		Auction: auction{
-			winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*entities.PbsOrtbBid{
+			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				"ImpId-1": {
-					openrtb_ext.BidderAppnexus: {
+					openrtb_ext.BidderAppnexus: {{
 						Bid:     bid123,
 						BidType: openrtb_ext.BidTypeBanner,
-					},
-					openrtb_ext.BidderRubicon: {
+					}},
+					openrtb_ext.BidderRubicon: {{
 						Bid:     bid084,
 						BidType: openrtb_ext.BidTypeBanner,
-					},
+					}},
 				},
 			},
 		},
-		ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{
+		ExpectedPbsBids: map[string]map[openrtb_ext.BidderName][]ExpectedPbsBid{
 			"ImpId-1": {
-				openrtb_ext.BidderAppnexus: {
-					"hb_bidder_": "appnexus",
-					"hb_pb_appn": "1.20",
+				openrtb_ext.BidderAppnexus: []ExpectedPbsBid{
+					{
+						BidTargets: map[string]string{
+							"hb_bidder_": "appnexus",
+							"hb_pb_appn": "1.20",
+						},
+					},
 				},
-				openrtb_ext.BidderRubicon: {
-					"hb_bidder_": "rubicon",
-					"hb_pb_rubi": "0.80",
+				openrtb_ext.BidderRubicon: []ExpectedPbsBid{
+					{
+						BidTargets: map[string]string{
+							"hb_bidder_": "rubicon",
+							"hb_pb_rubi": "0.80",
+						},
+					},
 				},
 			},
 		},
@@ -519,28 +595,36 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 			includeBidderKeys: true,
 		},
 		Auction: auction{
-			winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*entities.PbsOrtbBid{
+			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				"ImpId-1": {
-					openrtb_ext.BidderAppnexus: {
+					openrtb_ext.BidderAppnexus: {{
 						Bid:     bid123,
 						BidType: openrtb_ext.BidTypeBanner,
-					},
-					openrtb_ext.BidderRubicon: {
+					}},
+					openrtb_ext.BidderRubicon: {{
 						Bid:     bid084,
 						BidType: openrtb_ext.BidTypeBanner,
-					},
+					}},
 				},
 			},
 		},
-		ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{
+		ExpectedPbsBids: map[string]map[openrtb_ext.BidderName][]ExpectedPbsBid{
 			"ImpId-1": {
-				openrtb_ext.BidderAppnexus: {
-					"hb_bidder_appnexus": "appnexus",
-					"hb_pb_appnexus":     "1.20",
+				openrtb_ext.BidderAppnexus: []ExpectedPbsBid{
+					{
+						BidTargets: map[string]string{
+							"hb_bidder_appnexus": "appnexus",
+							"hb_pb_appnexus":     "1.20",
+						},
+					},
 				},
-				openrtb_ext.BidderRubicon: {
-					"hb_bidder_rubicon": "rubicon",
-					"hb_pb_rubicon":     "0.80",
+				openrtb_ext.BidderRubicon: []ExpectedPbsBid{
+					{
+						BidTargets: map[string]string{
+							"hb_bidder_rubicon": "rubicon",
+							"hb_pb_rubicon":     "0.80",
+						},
+					},
 				},
 			},
 		},
@@ -553,28 +637,36 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 			includeBidderKeys: true,
 		},
 		Auction: auction{
-			winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*entities.PbsOrtbBid{
+			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				"ImpId-1": {
-					openrtb_ext.BidderAppnexus: {
+					openrtb_ext.BidderAppnexus: {{
 						Bid:     bid123,
 						BidType: openrtb_ext.BidTypeBanner,
-					},
-					openrtb_ext.BidderRubicon: {
+					}},
+					openrtb_ext.BidderRubicon: {{
 						Bid:     bid084,
 						BidType: openrtb_ext.BidTypeBanner,
-					},
+					}},
 				},
 			},
 		},
-		ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{
+		ExpectedPbsBids: map[string]map[openrtb_ext.BidderName][]ExpectedPbsBid{
 			"ImpId-1": {
-				openrtb_ext.BidderAppnexus: {
-					"hb_bidder_appnexus": "appnexus",
-					"hb_pb_appnexus":     "1.20",
+				openrtb_ext.BidderAppnexus: []ExpectedPbsBid{
+					{
+						BidTargets: map[string]string{
+							"hb_bidder_appnexus": "appnexus",
+							"hb_pb_appnexus":     "1.20",
+						},
+					},
 				},
-				openrtb_ext.BidderRubicon: {
-					"hb_bidder_rubicon": "rubicon",
-					"hb_pb_rubicon":     "0.80",
+				openrtb_ext.BidderRubicon: []ExpectedPbsBid{
+					{
+						BidTargets: map[string]string{
+							"hb_bidder_rubicon": "rubicon",
+							"hb_pb_rubicon":     "0.80",
+						},
+					},
 				},
 			},
 		},
@@ -587,26 +679,30 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 			includeWinners:   true,
 		},
 		Auction: auction{
-			winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*entities.PbsOrtbBid{
+			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				"ImpId-1": {
-					openrtb_ext.BidderAppnexus: {
+					openrtb_ext.BidderAppnexus: {{
 						Bid:     bid123,
 						BidType: openrtb_ext.BidTypeBanner,
-					},
-					openrtb_ext.BidderRubicon: {
+					}},
+					openrtb_ext.BidderRubicon: {{
 						Bid:     bid084,
 						BidType: openrtb_ext.BidTypeBanner,
-					},
+					}},
 				},
 			},
 		},
-		ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{
+		ExpectedPbsBids: map[string]map[openrtb_ext.BidderName][]ExpectedPbsBid{
 			"ImpId-1": {
-				openrtb_ext.BidderAppnexus: {
-					"hb_bi": "appnexus",
-					"hb_pb": "1.20",
+				openrtb_ext.BidderAppnexus: []ExpectedPbsBid{
+					{
+						BidTargets: map[string]string{
+							"hb_bi": "appnexus",
+							"hb_pb": "1.20",
+						},
+					},
 				},
-				openrtb_ext.BidderRubicon: {},
+				openrtb_ext.BidderRubicon: []ExpectedPbsBid{},
 			},
 		},
 		TruncateTargetAttr: &truncateTargetAttrValue5,
@@ -618,26 +714,30 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 			includeWinners:   true,
 		},
 		Auction: auction{
-			winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*entities.PbsOrtbBid{
+			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				"ImpId-1": {
-					openrtb_ext.BidderAppnexus: {
+					openrtb_ext.BidderAppnexus: {{
 						Bid:     bid123,
 						BidType: openrtb_ext.BidTypeBanner,
-					},
-					openrtb_ext.BidderRubicon: {
+					}},
+					openrtb_ext.BidderRubicon: {{
 						Bid:     bid084,
 						BidType: openrtb_ext.BidTypeBanner,
-					},
+					}},
 				},
 			},
 		},
-		ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{
+		ExpectedPbsBids: map[string]map[openrtb_ext.BidderName][]ExpectedPbsBid{
 			"ImpId-1": {
-				openrtb_ext.BidderAppnexus: {
-					"hb_bidder": "appnexus",
-					"hb_pb":     "1.20",
+				openrtb_ext.BidderAppnexus: []ExpectedPbsBid{
+					{
+						BidTargets: map[string]string{
+							"hb_bidder": "appnexus",
+							"hb_pb":     "1.20",
+						},
+					},
 				},
-				openrtb_ext.BidderRubicon: {},
+				openrtb_ext.BidderRubicon: []ExpectedPbsBid{},
 			},
 		},
 		TruncateTargetAttr: &truncateTargetAttrValue25,
@@ -649,30 +749,169 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 			includeWinners:   true,
 		},
 		Auction: auction{
-			winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*entities.PbsOrtbBid{
+			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				"ImpId-1": {
-					openrtb_ext.BidderAppnexus: {
+					openrtb_ext.BidderAppnexus: {{
 						Bid:     bid123,
 						BidType: openrtb_ext.BidTypeBanner,
-					},
-					openrtb_ext.BidderRubicon: {
+					}},
+					openrtb_ext.BidderRubicon: {{
 						Bid:     bid084,
 						BidType: openrtb_ext.BidTypeBanner,
-					},
+					}},
 				},
 			},
 		},
-		ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{
+		ExpectedPbsBids: map[string]map[openrtb_ext.BidderName][]ExpectedPbsBid{
 			"ImpId-1": {
-				openrtb_ext.BidderAppnexus: {
-					"hb_bidder": "appnexus",
-					"hb_pb":     "1.20",
+				openrtb_ext.BidderAppnexus: []ExpectedPbsBid{
+					{
+						BidTargets: map[string]string{
+							"hb_bidder": "appnexus",
+							"hb_pb":     "1.20",
+						},
+					},
 				},
-				openrtb_ext.BidderRubicon: {},
+				openrtb_ext.BidderRubicon: []ExpectedPbsBid{},
 			},
 		},
 		TruncateTargetAttr: &truncateTargetAttrValueNegative,
 	},
+	{
+		Description: "Full basic targeting with multibid",
+		TargetData: targetData{
+			priceGranularity:  lookupPriceGranularity("med"),
+			includeWinners:    true,
+			includeBidderKeys: true,
+			includeFormat:     true,
+		},
+		Auction: auction{
+			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+				"ImpId-1": {
+					openrtb_ext.BidderAppnexus: {
+						{
+							Bid:     bid1p120,
+							BidType: openrtb_ext.BidTypeBanner,
+						},
+						{
+							Bid:     bid1p077,
+							BidType: openrtb_ext.BidTypeBanner,
+						},
+						{
+							Bid:     bid1p001,
+							BidType: openrtb_ext.BidTypeBanner,
+						},
+					},
+					openrtb_ext.BidderRubicon: {
+						{
+							Bid:     bid123,
+							BidType: openrtb_ext.BidTypeBanner,
+						},
+						{
+							Bid:     bid111,
+							BidType: openrtb_ext.BidTypeBanner,
+						},
+						{
+							Bid:     bid084,
+							BidType: openrtb_ext.BidTypeBanner,
+						},
+					},
+				},
+				"ImpId-2": {
+					openrtb_ext.BidderPubmatic: {
+						{
+							Bid:     bid2p166,
+							BidType: openrtb_ext.BidTypeBanner,
+						},
+						{
+							Bid:     bid2p155,
+							BidType: openrtb_ext.BidTypeBanner,
+						},
+						{
+							Bid:     bid2p144,
+							BidType: openrtb_ext.BidTypeBanner,
+						},
+						{
+							Bid:     bid2p123,
+							BidType: openrtb_ext.BidTypeBanner,
+						},
+					},
+				},
+			},
+		},
+		ExpectedPbsBids: map[string]map[openrtb_ext.BidderName][]ExpectedPbsBid{
+			"ImpId-1": {
+				openrtb_ext.BidderAppnexus: []ExpectedPbsBid{
+					{
+						BidTargets: map[string]string{
+							"hb_bidder_appnexus": "appnexus",
+							"hb_pb_appnexus":     "1.10",
+							"hb_format_appnexus": "banner",
+						},
+						TargetBidderCode: "appnexus",
+					},
+					{},
+					{},
+				},
+				openrtb_ext.BidderRubicon: []ExpectedPbsBid{
+					{
+						BidTargets: map[string]string{
+							"hb_bidder":         "rubicon",
+							"hb_bidder_rubicon": "rubicon",
+							"hb_pb":             "1.20",
+							"hb_pb_rubicon":     "1.20",
+							"hb_format":         "banner",
+							"hb_format_rubicon": "banner",
+						},
+					},
+					{},
+					{},
+				},
+			},
+			"ImpId-2": {
+				openrtb_ext.BidderPubmatic: []ExpectedPbsBid{
+					{
+						BidTargets: map[string]string{
+							"hb_bidder":          "pubmatic",
+							"hb_bidder_pubmatic": "pubmatic",
+							"hb_pb":              "1.60",
+							"hb_pb_pubmatic":     "1.60",
+							"hb_format":          "banner",
+							"hb_format_pubmatic": "banner",
+						},
+						TargetBidderCode: "pubmatic",
+					},
+					{
+						BidTargets: map[string]string{
+							"hb_bidder_pm2": "pm2",
+							"hb_pb_pm2":     "1.50",
+							"hb_format_pm2": "banner",
+						},
+						TargetBidderCode: "pm2",
+					},
+					{
+						BidTargets: map[string]string{
+							"hb_bidder_pm3": "pm3",
+							"hb_pb_pm3":     "1.40",
+							"hb_format_pm3": "banner",
+						},
+						TargetBidderCode: "pm3",
+					},
+					{},
+				},
+			},
+		},
+		TruncateTargetAttr: nil,
+		MultiBidMap: map[string]openrtb_ext.ExtMultiBid{
+			string(openrtb_ext.BidderPubmatic): {
+				MaxBids:                ptrutil.ToPtr(3),
+				TargetBidderCodePrefix: "pm",
+			},
+			string(openrtb_ext.BidderAppnexus): {
+				MaxBids: ptrutil.ToPtr(2),
+			},
+		},
+	},
 }
 
 func TestSetTargeting(t *testing.T) {
@@ -683,30 +922,34 @@ func TestSetTargeting(t *testing.T) {
 		winningBids := make(map[string]*entities.PbsOrtbBid)
 		// Set winning bids from the auction data
 		for imp, bidsByBidder := range auc.winningBidsByBidder {
-			for _, bid := range bidsByBidder {
-				if winningBid, ok := winningBids[imp]; ok {
-					if winningBid.Bid.Price < bid.Bid.Price {
+			for _, bids := range bidsByBidder {
+				for _, bid := range bids {
+					if winningBid, ok := winningBids[imp]; ok {
+						if winningBid.Bid.Price < bid.Bid.Price {
+							winningBids[imp] = bid
+						}
+					} else {
 						winningBids[imp] = bid
 					}
-				} else {
-					winningBids[imp] = bid
 				}
 			}
 		}
 		auc.winningBids = winningBids
 		targData := test.TargetData
-		targData.setTargeting(auc, test.IsApp, test.CategoryMapping, test.TruncateTargetAttr)
-		for imp, targetsByBidder := range test.ExpectedBidTargetsByBidder {
-			for bidder, expected := range targetsByBidder {
-				assert.Equal(t,
-					expected,
-					auc.winningBidsByBidder[imp][bidder].BidTargets,
-					"Test: %s\nTargeting failed for bidder %s on imp %s.",
-					test.Description,
-					string(bidder),
-					imp)
+		targData.setTargeting(auc, test.IsApp, test.CategoryMapping, test.TruncateTargetAttr, test.MultiBidMap)
+		for imp, targetsByBidder := range test.ExpectedPbsBids {
+			for bidder, expectedTargets := range targetsByBidder {
+				for i, expected := range expectedTargets {
+					assert.Equal(t,
+						expected.BidTargets,
+						auc.winningBidsByBidder[imp][bidder][i].BidTargets,
+						"Test: %s\nTargeting failed for bidder %s on imp %s.",
+						test.Description,
+						string(bidder),
+						imp)
+					assert.Equal(t, expected.TargetBidderCode, auc.winningBidsByBidder[imp][bidder][i].TargetBidderCode)
+				}
 			}
 		}
 	}
-
 }
diff --git a/exchange/utils_test.go b/exchange/utils_test.go
index 4a2d5fe7..92d92745 100644
--- a/exchange/utils_test.go
+++ b/exchange/utils_test.go
@@ -2487,6 +2487,55 @@ func TestBuildRequestExtForBidder(t *testing.T) {
 			requestExt:           json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"foo":{"enabled":true,"allowedbiddercodes":["foo2"]},"bar":{"enabled":true,"allowedbiddercodes":["ix"]}}}}}`),
 			expectedJson:         json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"foo":{"enabled":true,"allowedbiddercodes":["foo2"]}}},"bidderparams":"bar"}}`),
 		},
+		{
+			description:  "Prebid + Other + Bider Params + MultiBid.Bidder",
+			bidderParams: map[string]json.RawMessage{bidder: bidderParams},
+			requestExt:   json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"multibid":[{"bidder":"foo","maxbids":2,"targetbiddercodeprefix":"fmb"},{"bidders":["appnexus","groupm"],"maxbids":2}]}}`),
+			expectedJson: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"multibid":[{"bidder":"foo","maxbids":2,"targetbiddercodeprefix":"fmb"}],"bidderparams":"bar"}}`),
+		},
+		{
+			description:  "Prebid + Other + Bider Params + MultiBid.Bidders",
+			bidderParams: map[string]json.RawMessage{bidder: bidderParams},
+			requestExt:   json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"multibid":[{"bidder":"pubmatic","maxbids":3,"targetbiddercodeprefix":"pubM"},{"bidders":["foo","groupm"],"maxbids":4}]}}`),
+			expectedJson: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"multibid":[{"bidders":["foo"],"maxbids":4}],"bidderparams":"bar"}}`),
+		},
+		{
+			description:  "Prebid + Other + Bider Params + MultiBid (foo not in MultiBid)",
+			bidderParams: map[string]json.RawMessage{bidder: bidderParams},
+			requestExt:   json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"multibid":[{"bidder":"foo2","maxbids":2,"targetbiddercodeprefix":"fmb"},{"bidders":["appnexus","groupm"],"maxbids":2}]}}`),
+			expectedJson: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"bidderparams":"bar"}}`),
+		},
+		{
+			description:  "Prebid + Other + Bider Params + MultiBid (foo not in MultiBid)",
+			bidderParams: map[string]json.RawMessage{bidder: bidderParams},
+			requestExt:   json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"multibid":[{"bidder":"foo2","maxbids":2,"targetbiddercodeprefix":"fmb"},{"bidders":["appnexus","groupm"],"maxbids":2}]}}`),
+			expectedJson: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"bidderparams":"bar"}}`),
+		},
+		{
+			description:  "Prebid + AlternateBidderCodes.MultiBid.Bidder",
+			requestExt:   json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"foo":{"enabled":true,"allowedbiddercodes":["pubmatic"]}}},"multibid":[{"bidder":"foo","maxbids":3,"targetbiddercodeprefix":"fmb"},{"bidder":"foo2","maxbids":4,"targetbiddercodeprefix":"fmb2"},{"bidder":"pubmatic","maxbids":5,"targetbiddercodeprefix":"pm"}]}}`),
+			expectedJson: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"foo":{"enabled":true,"allowedbiddercodes":["pubmatic"]}}},"multibid":[{"bidder":"foo","maxbids":3,"targetbiddercodeprefix":"fmb"},{"bidder":"pubmatic","maxbids":5,"targetbiddercodeprefix":"pm"}]}}`),
+		},
+		{
+			description:  "Prebid + AlternateBidderCodes.MultiBid.Bidders",
+			requestExt:   json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"foo":{"enabled":true,"allowedbiddercodes":["pubmatic"]}}},"multibid":[{"bidder":"foo","maxbids":3,"targetbiddercodeprefix":"fmb"},{"bidders":["pubmatic","groupm"],"maxbids":4}]}}`),
+			expectedJson: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"foo":{"enabled":true,"allowedbiddercodes":["pubmatic"]}}},"multibid":[{"bidder":"foo","maxbids":3,"targetbiddercodeprefix":"fmb"},{"bidders":["pubmatic"],"maxbids":4}]}}`),
+		},
+		{
+			description:  "Prebid + AlternateBidderCodes.MultiBid.Bidder with *",
+			requestExt:   json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"foo":{"enabled":true,"allowedbiddercodes":["*"]}}},"multibid":[{"bidder":"foo","maxbids":3,"targetbiddercodeprefix":"fmb"},{"bidder":"foo2","maxbids":4,"targetbiddercodeprefix":"fmb2"},{"bidder":"pubmatic","maxbids":5,"targetbiddercodeprefix":"pm"}]}}`),
+			expectedJson: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"foo":{"enabled":true,"allowedbiddercodes":["*"]}}},"multibid":[{"bidder":"foo","maxbids":3,"targetbiddercodeprefix":"fmb"},{"bidder":"foo2","maxbids":4,"targetbiddercodeprefix":"fmb2"},{"bidder":"pubmatic","maxbids":5,"targetbiddercodeprefix":"pm"}]}}`),
+		},
+		{
+			description:  "Prebid + AlternateBidderCodes.MultiBid.Bidders with *",
+			requestExt:   json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"foo":{"enabled":true,"allowedbiddercodes":["*"]}}},"multibid":[{"bidder":"foo","maxbids":3,"targetbiddercodeprefix":"fmb"},{"bidders":["pubmatic","groupm"],"maxbids":4}]}}`),
+			expectedJson: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"foo":{"enabled":true,"allowedbiddercodes":["*"]}}},"multibid":[{"bidder":"foo","maxbids":3,"targetbiddercodeprefix":"fmb"},{"bidders":["pubmatic"],"maxbids":4},{"bidders":["groupm"],"maxbids":4}]}}`),
+		},
+		{
+			description:  "Prebid + AlternateBidderCodes + MultiBid",
+			requestExt:   json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"foo":{"enabled":true,"allowedbiddercodes":["foo2"]}}},"multibid":[{"bidder":"foo3","maxbids":3,"targetbiddercodeprefix":"fmb"},{"bidders":["pubmatic","groupm"],"maxbids":4}]}}`),
+			expectedJson: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true},"alternatebiddercodes":{"enabled":true,"bidders":{"foo":{"enabled":true,"allowedbiddercodes":["foo2"]}}}}}`),
+		},
 	}
 
 	for _, test := range testCases {
@@ -3476,3 +3525,340 @@ func TestCleanOpenRTBRequestsFilterBidderRequestExt(t *testing.T) {
 		}
 	}
 }
+
+func Test_isBidderInExtAlternateBidderCodes(t *testing.T) {
+	type args struct {
+		adapter               string
+		currentMultiBidBidder string
+		adapterABC            *openrtb_ext.ExtAlternateBidderCodes
+	}
+	tests := []struct {
+		name string
+		args args
+		want bool
+	}{
+		{
+			name: "alternatebiddercodes not defined",
+			want: false,
+		},
+		{
+			name: "adapter not defined in alternatebiddercodes",
+			args: args{
+				adapter: string(openrtb_ext.BidderPubmatic),
+				adapterABC: &openrtb_ext.ExtAlternateBidderCodes{
+					Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{string(openrtb_ext.BidderAppnexus): {}},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "adapter defined in alternatebiddercodes but currentMultiBidBidder not in AllowedBidders list",
+			args: args{
+				adapter:               string(openrtb_ext.BidderPubmatic),
+				currentMultiBidBidder: string(openrtb_ext.BidderGroupm),
+				adapterABC: &openrtb_ext.ExtAlternateBidderCodes{
+					Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{
+						string(openrtb_ext.BidderPubmatic): {
+							AllowedBidderCodes: []string{string(openrtb_ext.BidderAppnexus)},
+						},
+					},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "adapter defined in alternatebiddercodes with currentMultiBidBidder mentioned in AllowedBidders list",
+			args: args{
+				adapter:               string(openrtb_ext.BidderPubmatic),
+				currentMultiBidBidder: string(openrtb_ext.BidderGroupm),
+				adapterABC: &openrtb_ext.ExtAlternateBidderCodes{
+					Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{
+						string(openrtb_ext.BidderPubmatic): {
+							AllowedBidderCodes: []string{string(openrtb_ext.BidderGroupm)},
+						},
+					},
+				},
+			},
+			want: true,
+		},
+		{
+			name: "adapter defined in alternatebiddercodes with AllowedBidders list as *",
+			args: args{
+				adapter:               string(openrtb_ext.BidderPubmatic),
+				currentMultiBidBidder: string(openrtb_ext.BidderGroupm),
+				adapterABC: &openrtb_ext.ExtAlternateBidderCodes{
+					Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{
+						string(openrtb_ext.BidderPubmatic): {
+							AllowedBidderCodes: []string{"*"},
+						},
+					},
+				},
+			},
+			want: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := isBidderInExtAlternateBidderCodes(tt.args.adapter, tt.args.currentMultiBidBidder, tt.args.adapterABC); got != tt.want {
+				t.Errorf("isBidderInExtAlternateBidderCodes() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_buildRequestExtMultiBid(t *testing.T) {
+	type args struct {
+		adapter     string
+		reqMultiBid []*openrtb_ext.ExtMultiBid
+		adapterABC  *openrtb_ext.ExtAlternateBidderCodes
+	}
+	tests := []struct {
+		name string
+		args args
+		want []*openrtb_ext.ExtMultiBid
+	}{
+		{
+			name: "multi-bid config not defined",
+			args: args{
+				adapter:     string(openrtb_ext.BidderPubmatic),
+				reqMultiBid: nil,
+			},
+			want: nil,
+		},
+		{
+			name: "adapter not defined in multi-bid config",
+			args: args{
+				adapter: string(openrtb_ext.BidderPubmatic),
+				reqMultiBid: []*openrtb_ext.ExtMultiBid{
+					{
+						Bidder:  string(openrtb_ext.BidderAppnexus),
+						MaxBids: ptrutil.ToPtr(2),
+					},
+				},
+			},
+			want: nil,
+		},
+		{
+			name: "adapter defined in multi-bid config as Bidder object along with other bidders",
+			args: args{
+				adapter: string(openrtb_ext.BidderPubmatic),
+				reqMultiBid: []*openrtb_ext.ExtMultiBid{
+					{
+						Bidder:  string(openrtb_ext.BidderAppnexus),
+						MaxBids: ptrutil.ToPtr(3),
+					},
+					{
+						Bidder:  string(openrtb_ext.BidderPubmatic),
+						MaxBids: ptrutil.ToPtr(2),
+					},
+					{
+						Bidders: []string{string(openrtb_ext.Bidder33Across), string(openrtb_ext.BidderRubicon)},
+						MaxBids: ptrutil.ToPtr(2),
+					},
+				},
+			},
+			want: []*openrtb_ext.ExtMultiBid{
+				{
+					Bidder:  string(openrtb_ext.BidderPubmatic),
+					MaxBids: ptrutil.ToPtr(2),
+				},
+			},
+		},
+		{
+			name: "adapter defined in multi-bid config as a entry of Bidders list along with other bidders",
+			args: args{
+				adapter: string(openrtb_ext.BidderRubicon),
+				reqMultiBid: []*openrtb_ext.ExtMultiBid{
+					{
+						Bidder:  string(openrtb_ext.BidderAppnexus),
+						MaxBids: ptrutil.ToPtr(3),
+					},
+					{
+						Bidder:  string(openrtb_ext.BidderPubmatic),
+						MaxBids: ptrutil.ToPtr(2),
+					},
+					{
+						Bidders: []string{string(openrtb_ext.Bidder33Across), string(openrtb_ext.BidderRubicon)},
+						MaxBids: ptrutil.ToPtr(4),
+					},
+				},
+			},
+			want: []*openrtb_ext.ExtMultiBid{
+				{
+					Bidders: []string{string(openrtb_ext.BidderRubicon)},
+					MaxBids: ptrutil.ToPtr(4),
+				},
+			},
+		},
+		{
+			name: "adapter defined in multi-bid config as Bidder object along with other bidders with alternateBiddercode",
+			args: args{
+				adapter: string(openrtb_ext.BidderPubmatic),
+				reqMultiBid: []*openrtb_ext.ExtMultiBid{
+					{
+						Bidder:  string(openrtb_ext.BidderGroupm),
+						MaxBids: ptrutil.ToPtr(3),
+					},
+					{
+						Bidder:  string(openrtb_ext.BidderPubmatic),
+						MaxBids: ptrutil.ToPtr(2),
+					},
+					{
+						Bidders: []string{string(openrtb_ext.Bidder33Across), string(openrtb_ext.BidderRubicon)},
+						MaxBids: ptrutil.ToPtr(2),
+					},
+				},
+				adapterABC: &openrtb_ext.ExtAlternateBidderCodes{
+					Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{
+						string(openrtb_ext.BidderPubmatic): {
+							AllowedBidderCodes: []string{string(openrtb_ext.BidderGroupm)},
+						},
+					},
+				},
+			},
+			want: []*openrtb_ext.ExtMultiBid{
+				{
+					Bidder:  string(openrtb_ext.BidderGroupm),
+					MaxBids: ptrutil.ToPtr(3),
+				},
+				{
+					Bidder:  string(openrtb_ext.BidderPubmatic),
+					MaxBids: ptrutil.ToPtr(2),
+				},
+			},
+		},
+		{
+			name: "adapter defined in multi-bid config as a entry of Bidders list along with other bidders with alternateBiddercode",
+			args: args{
+				adapter: string(openrtb_ext.BidderAppnexus),
+				reqMultiBid: []*openrtb_ext.ExtMultiBid{
+					{
+						Bidder:  string(openrtb_ext.BidderGroupm),
+						MaxBids: ptrutil.ToPtr(3),
+					},
+					{
+						Bidder:  string(openrtb_ext.BidderPubmatic),
+						MaxBids: ptrutil.ToPtr(2),
+					},
+					{
+						Bidders: []string{string(openrtb_ext.Bidder33Across), string(openrtb_ext.BidderAppnexus)},
+						MaxBids: ptrutil.ToPtr(4),
+					},
+				},
+				adapterABC: &openrtb_ext.ExtAlternateBidderCodes{
+					Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{
+						string(openrtb_ext.BidderAppnexus): {
+							AllowedBidderCodes: []string{string(openrtb_ext.BidderGroupm)},
+						},
+					},
+				},
+			},
+			want: []*openrtb_ext.ExtMultiBid{
+				{
+					Bidder:  string(openrtb_ext.BidderGroupm),
+					MaxBids: ptrutil.ToPtr(3),
+				},
+				{
+					Bidders: []string{string(openrtb_ext.BidderAppnexus)},
+					MaxBids: ptrutil.ToPtr(4),
+				},
+			},
+		},
+		{
+			name: "adapter defined in multi-bid config as Bidder object along with other bidders with alternateBiddercode.AllowedBidders as *",
+			args: args{
+				adapter: string(openrtb_ext.BidderPubmatic),
+				reqMultiBid: []*openrtb_ext.ExtMultiBid{
+					{
+						Bidder:  string(openrtb_ext.BidderGroupm),
+						MaxBids: ptrutil.ToPtr(3),
+					},
+					{
+						Bidder:  string(openrtb_ext.BidderPubmatic),
+						MaxBids: ptrutil.ToPtr(2),
+					},
+					{
+						Bidders: []string{string(openrtb_ext.Bidder33Across), string(openrtb_ext.BidderRubicon)},
+						MaxBids: ptrutil.ToPtr(2),
+					},
+				},
+				adapterABC: &openrtb_ext.ExtAlternateBidderCodes{
+					Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{
+						string(openrtb_ext.BidderPubmatic): {
+							AllowedBidderCodes: []string{"*"},
+						},
+					},
+				},
+			},
+			want: []*openrtb_ext.ExtMultiBid{
+				{
+					Bidder:  string(openrtb_ext.BidderGroupm),
+					MaxBids: ptrutil.ToPtr(3),
+				},
+				{
+					Bidder:  string(openrtb_ext.BidderPubmatic),
+					MaxBids: ptrutil.ToPtr(2),
+				},
+				{
+					Bidders: []string{string(openrtb_ext.Bidder33Across)},
+					MaxBids: ptrutil.ToPtr(2),
+				},
+				{
+					Bidders: []string{string(openrtb_ext.BidderRubicon)},
+					MaxBids: ptrutil.ToPtr(2),
+				},
+			},
+		},
+		{
+			name: "adapter defined in multi-bid config as a entry of Bidders list along with other bidders with alternateBiddercode.AllowedBidders as *",
+			args: args{
+				adapter: string(openrtb_ext.BidderAppnexus),
+				reqMultiBid: []*openrtb_ext.ExtMultiBid{
+					{
+						Bidder:  string(openrtb_ext.BidderGroupm),
+						MaxBids: ptrutil.ToPtr(3),
+					},
+					{
+						Bidder:  string(openrtb_ext.BidderPubmatic),
+						MaxBids: ptrutil.ToPtr(2),
+					},
+					{
+						Bidders: []string{string(openrtb_ext.Bidder33Across), string(openrtb_ext.BidderAppnexus)},
+						MaxBids: ptrutil.ToPtr(4),
+					},
+				},
+				adapterABC: &openrtb_ext.ExtAlternateBidderCodes{
+					Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{
+						string(openrtb_ext.BidderAppnexus): {
+							AllowedBidderCodes: []string{"*"},
+						},
+					},
+				},
+			},
+			want: []*openrtb_ext.ExtMultiBid{
+				{
+					Bidder:  string(openrtb_ext.BidderGroupm),
+					MaxBids: ptrutil.ToPtr(3),
+				},
+				{
+					Bidder:  string(openrtb_ext.BidderPubmatic),
+					MaxBids: ptrutil.ToPtr(2),
+				},
+				{
+					Bidders: []string{string(openrtb_ext.Bidder33Across)},
+					MaxBids: ptrutil.ToPtr(4),
+				},
+				{
+					Bidders: []string{string(openrtb_ext.BidderAppnexus)},
+					MaxBids: ptrutil.ToPtr(4),
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := buildRequestExtMultiBid(tt.args.adapter, tt.args.reqMultiBid, tt.args.adapterABC)
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
diff --git a/openrtb_ext/multibid_test.go b/openrtb_ext/multibid_test.go
new file mode 100644
index 00000000..a27631d6
--- /dev/null
+++ b/openrtb_ext/multibid_test.go
@@ -0,0 +1,304 @@
+package openrtb_ext
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/prebid/prebid-server/util/ptrutil"
+	"github.com/stretchr/testify/assert"
+)
+
+var maxBid1 = ptrutil.ToPtr(1)
+var maxBid2 = ptrutil.ToPtr(2)
+var maxBid3 = ptrutil.ToPtr(3)
+var maxBid9 = ptrutil.ToPtr(9)
+
+func TestValidateAndBuildExtMultiBid(t *testing.T) {
+	var maxBid0 = ptrutil.ToPtr(0)
+	var maxBid10 = ptrutil.ToPtr(10)
+
+	type args struct {
+		prebid *ExtRequestPrebid
+	}
+	tests := []struct {
+		name                   string
+		args                   args
+		wantValidatedMultiBids []*ExtMultiBid
+		wantErrs               []error
+	}{
+		{
+			name:                   "prebid nil",
+			wantValidatedMultiBids: nil,
+			wantErrs:               nil,
+		},
+		{
+			name: "prebid.MultiBid nil",
+			args: args{
+				prebid: &ExtRequestPrebid{MultiBid: nil},
+			},
+			wantValidatedMultiBids: nil,
+			wantErrs:               nil,
+		},
+		{
+			name: "prebid.MultiBid empty",
+			args: args{
+				prebid: &ExtRequestPrebid{MultiBid: make([]*ExtMultiBid, 0)},
+			},
+			wantValidatedMultiBids: nil,
+			wantErrs:               nil,
+		},
+		{
+			name: "MaxBids not defined",
+			args: args{
+				prebid: &ExtRequestPrebid{MultiBid: []*ExtMultiBid{{Bidder: "pubmatic"}}},
+			},
+			wantValidatedMultiBids: nil,
+			wantErrs:               []error{fmt.Errorf("maxBids not defined for %v", ExtMultiBid{Bidder: "pubmatic"})},
+		},
+		{
+			name: "Bidder or Bidders not defined",
+			args: args{
+				prebid: &ExtRequestPrebid{MultiBid: []*ExtMultiBid{{MaxBids: maxBid2}}},
+			},
+			wantValidatedMultiBids: nil,
+			wantErrs:               []error{fmt.Errorf("bidder(s) not specified for %v", ExtMultiBid{MaxBids: maxBid2})},
+		},
+		{
+			name: "Input bidder is already present in multibid",
+			args: args{
+				prebid: &ExtRequestPrebid{
+					MultiBid: []*ExtMultiBid{
+						{Bidder: "pubmatic", MaxBids: maxBid2, TargetBidderCodePrefix: "pm"},
+						{Bidder: "pubmatic", MaxBids: maxBid3, TargetBidderCodePrefix: "pubm"},
+					},
+				},
+			},
+			wantErrs:               []error{fmt.Errorf("multiBid already defined for pubmatic, ignoring this instance %v", ExtMultiBid{MaxBids: maxBid3, Bidder: "pubmatic", TargetBidderCodePrefix: "pubm"})},
+			wantValidatedMultiBids: []*ExtMultiBid{{Bidder: "pubmatic", MaxBids: maxBid2, TargetBidderCodePrefix: "pm"}},
+		},
+		{
+			name: "Bidder and Bidders both defined (only Bidder will be used)",
+			args: args{
+				prebid: &ExtRequestPrebid{
+					MultiBid: []*ExtMultiBid{
+						{Bidder: "pubmatic", MaxBids: maxBid2, TargetBidderCodePrefix: "pm"},
+						{MaxBids: maxBid2, Bidder: "appnexus", Bidders: []string{"rubicon"}, TargetBidderCodePrefix: "appN"},
+					},
+				},
+			},
+			wantErrs:               []error{fmt.Errorf("ignoring bidders from %v", ExtMultiBid{MaxBids: maxBid2, Bidder: "appnexus", Bidders: []string{"rubicon"}, TargetBidderCodePrefix: "appN"})},
+			wantValidatedMultiBids: []*ExtMultiBid{{Bidder: "pubmatic", MaxBids: maxBid2, TargetBidderCodePrefix: "pm"}, {Bidder: "appnexus", MaxBids: maxBid2, TargetBidderCodePrefix: "appN"}},
+		},
+		{
+			name: "Only Bidder defined",
+			args: args{
+				prebid: &ExtRequestPrebid{
+					MultiBid: []*ExtMultiBid{
+						{MaxBids: maxBid2, Bidder: "appnexus", TargetBidderCodePrefix: "appN"},
+					},
+				},
+			},
+			wantErrs:               nil,
+			wantValidatedMultiBids: []*ExtMultiBid{{Bidder: "appnexus", MaxBids: maxBid2, TargetBidderCodePrefix: "appN"}},
+		},
+		{
+			name: "Only Bidders defined",
+			args: args{
+				prebid: &ExtRequestPrebid{
+					MultiBid: []*ExtMultiBid{
+						{MaxBids: maxBid2, Bidders: []string{"appnexus", "someBidder"}},
+					},
+				},
+			},
+			wantErrs:               nil,
+			wantValidatedMultiBids: []*ExtMultiBid{{Bidders: []string{"appnexus", "someBidder"}, MaxBids: maxBid2}},
+		},
+		{
+			name: "Bidders defined where a bidder is already present in multibid",
+			args: args{
+				prebid: &ExtRequestPrebid{
+					MultiBid: []*ExtMultiBid{
+						{Bidder: "pubmatic", MaxBids: maxBid9, TargetBidderCodePrefix: "pm"},
+						{MaxBids: maxBid2, Bidders: []string{"appnexus", "pubmatic"}},
+					},
+				},
+			},
+			wantErrs:               []error{fmt.Errorf("multiBid already defined for pubmatic, ignoring this instance %v", ExtMultiBid{MaxBids: maxBid2, Bidders: []string{"appnexus", "pubmatic"}})},
+			wantValidatedMultiBids: []*ExtMultiBid{{Bidder: "pubmatic", MaxBids: maxBid9, TargetBidderCodePrefix: "pm"}, {Bidders: []string{"appnexus"}, MaxBids: maxBid2}},
+		},
+		{
+			name: "Bidders defined where all the bidders are already present in multibid",
+			args: args{
+				prebid: &ExtRequestPrebid{
+					MultiBid: []*ExtMultiBid{
+						{Bidder: "pubmatic", MaxBids: maxBid9, TargetBidderCodePrefix: "pm"},
+						{Bidders: []string{"appnexus"}, MaxBids: maxBid3},
+						{MaxBids: maxBid2, Bidders: []string{"appnexus", "pubmatic"}},
+					},
+				},
+			},
+			wantErrs: []error{
+				fmt.Errorf("multiBid already defined for appnexus, ignoring this instance %v", ExtMultiBid{MaxBids: maxBid2, Bidders: []string{"appnexus", "pubmatic"}}),
+				fmt.Errorf("multiBid already defined for pubmatic, ignoring this instance %v", ExtMultiBid{MaxBids: maxBid2, Bidders: []string{"appnexus", "pubmatic"}}),
+			},
+			wantValidatedMultiBids: []*ExtMultiBid{{Bidder: "pubmatic", MaxBids: maxBid9, TargetBidderCodePrefix: "pm"}, {Bidders: []string{"appnexus"}, MaxBids: maxBid3}},
+		},
+		{
+			name: "Bidders defined along with TargetBidderCodePrefix",
+			args: args{
+				prebid: &ExtRequestPrebid{
+					MultiBid: []*ExtMultiBid{
+						{MaxBids: maxBid2, Bidders: []string{"appnexus"}, TargetBidderCodePrefix: "appN"},
+					},
+				},
+			},
+			wantErrs:               []error{fmt.Errorf("ignoring targetbiddercodeprefix for %v", ExtMultiBid{MaxBids: maxBid2, Bidders: []string{"appnexus"}, TargetBidderCodePrefix: "appN"})},
+			wantValidatedMultiBids: []*ExtMultiBid{{Bidders: []string{"appnexus"}, MaxBids: maxBid2}},
+		},
+		{
+			name: "MaxBids defined below minimum limit",
+			args: args{
+				prebid: &ExtRequestPrebid{
+					MultiBid: []*ExtMultiBid{
+						{MaxBids: maxBid0, Bidder: "appnexus", TargetBidderCodePrefix: "appN"},
+					},
+				},
+			},
+			wantErrs:               []error{fmt.Errorf("invalid maxBids value, using minimum 1 limit for %v", ExtMultiBid{MaxBids: maxBid0, Bidder: "appnexus", TargetBidderCodePrefix: "appN"})},
+			wantValidatedMultiBids: []*ExtMultiBid{{Bidder: "appnexus", TargetBidderCodePrefix: "appN", MaxBids: maxBid1}},
+		},
+		{
+			name: "MaxBids defined over max limit",
+			args: args{
+				prebid: &ExtRequestPrebid{
+					MultiBid: []*ExtMultiBid{
+						{MaxBids: maxBid10, Bidder: "appnexus", TargetBidderCodePrefix: "appN"},
+					},
+				},
+			},
+			wantErrs:               []error{fmt.Errorf("invalid maxBids value, using maximum 9 limit for %v", ExtMultiBid{MaxBids: maxBid10, Bidder: "appnexus", TargetBidderCodePrefix: "appN"})},
+			wantValidatedMultiBids: []*ExtMultiBid{{Bidder: "appnexus", TargetBidderCodePrefix: "appN", MaxBids: maxBid9}},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotValidatedMultiBids, gotErrs := ValidateAndBuildExtMultiBid(tt.args.prebid)
+			assert.Equal(t, tt.wantValidatedMultiBids, gotValidatedMultiBids)
+			assert.Equal(t, tt.wantErrs, gotErrs)
+		})
+	}
+}
+
+func Test_addMultiBid(t *testing.T) {
+	var maxBid0 = ptrutil.ToPtr(0)
+	var maxBid10 = ptrutil.ToPtr(10)
+
+	type args struct {
+		multiBidMap map[string]struct{}
+		multiBid    *ExtMultiBid
+	}
+	tests := []struct {
+		name          string
+		args          args
+		wantErrs      []error
+		wantMultiBids []*ExtMultiBid
+	}{
+		{
+			name: "MaxBids not defined",
+			args: args{
+				multiBidMap: map[string]struct{}{},
+				multiBid:    &ExtMultiBid{},
+			},
+			wantErrs:      []error{fmt.Errorf("maxBids not defined for %v", ExtMultiBid{})},
+			wantMultiBids: nil,
+		},
+		{
+			name: "Bidder or Bidders not defined",
+			args: args{
+				multiBidMap: map[string]struct{}{},
+				multiBid:    &ExtMultiBid{MaxBids: maxBid2},
+			},
+			wantErrs:      []error{fmt.Errorf("bidder(s) not specified for %v", ExtMultiBid{MaxBids: maxBid2})},
+			wantMultiBids: nil,
+		},
+		{
+			name: "Input bidder is already present in multibid",
+			args: args{
+				multiBidMap: map[string]struct{}{"pubmatic": {}},
+				multiBid:    &ExtMultiBid{MaxBids: maxBid2, Bidder: "pubmatic"},
+			},
+			wantErrs:      []error{fmt.Errorf("multiBid already defined for pubmatic, ignoring this instance %v", ExtMultiBid{MaxBids: maxBid2, Bidder: "pubmatic"})},
+			wantMultiBids: nil,
+		},
+		{
+			name: "Bidder and Bidders both defined (only Bidder will be used)",
+			args: args{
+				multiBidMap: map[string]struct{}{"pubmatic": {}},
+				multiBid:    &ExtMultiBid{MaxBids: maxBid2, Bidder: "appnexus", Bidders: []string{"rubicon"}, TargetBidderCodePrefix: "appN"},
+			},
+			wantErrs:      []error{fmt.Errorf("ignoring bidders from %v", ExtMultiBid{MaxBids: maxBid2, Bidder: "appnexus", Bidders: []string{"rubicon"}, TargetBidderCodePrefix: "appN"})},
+			wantMultiBids: []*ExtMultiBid{{Bidder: "appnexus", MaxBids: maxBid2, TargetBidderCodePrefix: "appN"}},
+		},
+		{
+			name: "Only Bidder defined",
+			args: args{
+				multiBidMap: map[string]struct{}{},
+				multiBid:    &ExtMultiBid{MaxBids: maxBid2, Bidder: "appnexus", TargetBidderCodePrefix: "appN"},
+			},
+			wantErrs:      []error{},
+			wantMultiBids: []*ExtMultiBid{{Bidder: "appnexus", MaxBids: maxBid2, TargetBidderCodePrefix: "appN"}},
+		},
+		{
+			name: "Only Bidders defined",
+			args: args{
+				multiBidMap: map[string]struct{}{},
+				multiBid:    &ExtMultiBid{MaxBids: maxBid2, Bidders: []string{"appnexus", "someBidder"}},
+			},
+			wantErrs:      []error{},
+			wantMultiBids: []*ExtMultiBid{{Bidders: []string{"appnexus", "someBidder"}, MaxBids: maxBid2}},
+		},
+		{
+			name: "Bidders defined where a bidder is already present in multibid",
+			args: args{
+				multiBidMap: map[string]struct{}{"pubmatic": {}},
+				multiBid:    &ExtMultiBid{MaxBids: maxBid2, Bidders: []string{"appnexus", "pubmatic"}},
+			},
+			wantErrs:      []error{fmt.Errorf("multiBid already defined for pubmatic, ignoring this instance %v", ExtMultiBid{MaxBids: maxBid2, Bidders: []string{"appnexus", "pubmatic"}})},
+			wantMultiBids: []*ExtMultiBid{{Bidders: []string{"appnexus"}, MaxBids: maxBid2}},
+		},
+		{
+			name: "Bidders defined along with TargetBidderCodePrefix",
+			args: args{
+				multiBidMap: map[string]struct{}{},
+				multiBid:    &ExtMultiBid{MaxBids: maxBid2, Bidders: []string{"appnexus"}, TargetBidderCodePrefix: "appN"},
+			},
+			wantErrs:      []error{fmt.Errorf("ignoring targetbiddercodeprefix for %v", ExtMultiBid{MaxBids: maxBid2, Bidders: []string{"appnexus"}, TargetBidderCodePrefix: "appN"})},
+			wantMultiBids: []*ExtMultiBid{{Bidders: []string{"appnexus"}, MaxBids: maxBid2}},
+		},
+		{
+			name: "MaxBids defined below minimum limit",
+			args: args{
+				multiBidMap: map[string]struct{}{},
+				multiBid:    &ExtMultiBid{MaxBids: maxBid0, Bidder: "appnexus", TargetBidderCodePrefix: "appN"},
+			},
+			wantErrs:      []error{fmt.Errorf("invalid maxBids value, using minimum 1 limit for %v", ExtMultiBid{MaxBids: maxBid0, Bidder: "appnexus", TargetBidderCodePrefix: "appN"})},
+			wantMultiBids: []*ExtMultiBid{{Bidder: "appnexus", TargetBidderCodePrefix: "appN", MaxBids: maxBid1}},
+		},
+		{
+			name: "MaxBids defined over max limit",
+			args: args{
+				multiBidMap: map[string]struct{}{},
+				multiBid:    &ExtMultiBid{MaxBids: maxBid10, Bidder: "appnexus", TargetBidderCodePrefix: "appN"},
+			},
+			wantErrs:      []error{fmt.Errorf("invalid maxBids value, using maximum 9 limit for %v", ExtMultiBid{MaxBids: maxBid10, Bidder: "appnexus", TargetBidderCodePrefix: "appN"})},
+			wantMultiBids: []*ExtMultiBid{{Bidder: "appnexus", TargetBidderCodePrefix: "appN", MaxBids: maxBid9}},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotMultiBids, gotErrs := addMultiBid(tt.args.multiBidMap, tt.args.multiBid)
+			assert.Equal(t, tt.wantErrs, gotErrs)
+			assert.Equal(t, tt.wantMultiBids, gotMultiBids)
+		})
+	}
+}
diff --git a/usersync/chooser_test.go b/usersync/chooser_test.go
index 8a129bd4..3b820b99 100644
--- a/usersync/chooser_test.go
+++ b/usersync/chooser_test.go
@@ -115,7 +115,7 @@ func TestChooserChoose(t *testing.T) {
 			givenCookie:        Cookie{},
 			expected: Result{
 				Status:           StatusOK,
-				BiddersEvaluated: []BidderEvaluation{{Bidder: "a", Status: StatusOK}},
+				BiddersEvaluated: []BidderEvaluation{{Bidder: "a", SyncerKey: "keyA", Status: StatusOK}},
 				SyncersChosen:    []SyncerChoice{syncerChoiceA},
 			},
 		},
@@ -129,7 +129,7 @@ func TestChooserChoose(t *testing.T) {
 			givenCookie:        Cookie{},
 			expected: Result{
 				Status:           StatusOK,
-				BiddersEvaluated: []BidderEvaluation{{Bidder: "c", Status: StatusTypeNotSupported}},
+				BiddersEvaluated: []BidderEvaluation{{Bidder: "c", SyncerKey: "keyC", Status: StatusTypeNotSupported}},
 				SyncersChosen:    []SyncerChoice{},
 			},
 		},
@@ -143,7 +143,7 @@ func TestChooserChoose(t *testing.T) {
 			givenCookie:        Cookie{},
 			expected: Result{
 				Status:           StatusOK,
-				BiddersEvaluated: []BidderEvaluation{{Bidder: "a", Status: StatusOK}, {Bidder: "b", Status: StatusOK}},
+				BiddersEvaluated: []BidderEvaluation{{Bidder: "a", SyncerKey: "keyA", Status: StatusOK}, {Bidder: "b", SyncerKey: "keyB", Status: StatusOK}},
 				SyncersChosen:    []SyncerChoice{syncerChoiceA, syncerChoiceB},
 			},
 		},
@@ -157,7 +157,7 @@ func TestChooserChoose(t *testing.T) {
 			givenCookie:        Cookie{},
 			expected: Result{
 				Status:           StatusOK,
-				BiddersEvaluated: []BidderEvaluation{{Bidder: "a", Status: StatusOK}, {Bidder: "b", Status: StatusOK}},
+				BiddersEvaluated: []BidderEvaluation{{Bidder: "a", SyncerKey: "keyA", Status: StatusOK}, {Bidder: "b", SyncerKey: "keyB", Status: StatusOK}},
 				SyncersChosen:    []SyncerChoice{syncerChoiceA, syncerChoiceB},
 			},
 		},
@@ -171,7 +171,7 @@ func TestChooserChoose(t *testing.T) {
 			givenCookie:        Cookie{},
 			expected: Result{
 				Status:           StatusOK,
-				BiddersEvaluated: []BidderEvaluation{{Bidder: "a", Status: StatusOK}},
+				BiddersEvaluated: []BidderEvaluation{{Bidder: "a", SyncerKey: "keyA", Status: StatusOK}},
 				SyncersChosen:    []SyncerChoice{syncerChoiceA},
 			},
 		},
@@ -185,7 +185,7 @@ func TestChooserChoose(t *testing.T) {
 			givenCookie:        Cookie{},
 			expected: Result{
 				Status:           StatusOK,
-				BiddersEvaluated: []BidderEvaluation{{Bidder: "c", Status: StatusTypeNotSupported}, {Bidder: "a", Status: StatusOK}},
+				BiddersEvaluated: []BidderEvaluation{{Bidder: "c", SyncerKey: "keyC", Status: StatusTypeNotSupported}, {Bidder: "a", SyncerKey: "keyA", Status: StatusOK}},
 				SyncersChosen:    []SyncerChoice{syncerChoiceA},
 			},
 		},
@@ -199,7 +199,7 @@ func TestChooserChoose(t *testing.T) {
 			givenCookie:        Cookie{},
 			expected: Result{
 				Status:           StatusOK,
-				BiddersEvaluated: []BidderEvaluation{{Bidder: "a", Status: StatusOK}, {Bidder: "c", Status: StatusTypeNotSupported}},
+				BiddersEvaluated: []BidderEvaluation{{Bidder: "a", SyncerKey: "keyA", Status: StatusOK}, {Bidder: "c", SyncerKey: "keyC", Status: StatusTypeNotSupported}},
 				SyncersChosen:    []SyncerChoice{syncerChoiceA},
 			},
 		},
@@ -242,94 +242,85 @@ func TestChooserEvaluate(t *testing.T) {
 	cookieAlreadyHasSyncForB := Cookie{uids: map[string]uidWithExpiry{"keyB": {Expires: time.Now().Add(time.Duration(24) * time.Hour)}}}
 
 	testCases := []struct {
-		description      string
-		givenBidder      string
-		givenSyncersSeen map[string]struct{}
-		givenPrivacy     Privacy
-		givenCookie      Cookie
-		expectedSyncer   Syncer
-		expectedBidder   string
-		expectedStatus   Status
+		description        string
+		givenBidder        string
+		givenSyncersSeen   map[string]struct{}
+		givenPrivacy       Privacy
+		givenCookie        Cookie
+		expectedSyncer     Syncer
+		expectedEvaluation BidderEvaluation
 	}{
 		{
-			description:      "Valid",
-			givenBidder:      "a",
-			givenSyncersSeen: map[string]struct{}{},
-			givenPrivacy:     fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
-			givenCookie:      cookieNeedsSync,
-			expectedSyncer:   fakeSyncerA,
-			expectedBidder:   "a",
-			expectedStatus:   StatusOK,
+			description:        "Valid",
+			givenBidder:        "a",
+			givenSyncersSeen:   map[string]struct{}{},
+			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
+			givenCookie:        cookieNeedsSync,
+			expectedSyncer:     fakeSyncerA,
+			expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusOK},
 		},
 		{
-			description:      "Unknown Bidder",
-			givenBidder:      "unknown",
-			givenSyncersSeen: map[string]struct{}{},
-			givenPrivacy:     fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
-			givenCookie:      cookieNeedsSync,
-			expectedSyncer:   nil,
-			expectedBidder:   "unknown",
-			expectedStatus:   StatusUnknownBidder,
+			description:        "Unknown Bidder",
+			givenBidder:        "unknown",
+			givenSyncersSeen:   map[string]struct{}{},
+			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
+			givenCookie:        cookieNeedsSync,
+			expectedSyncer:     nil,
+			expectedEvaluation: BidderEvaluation{Bidder: "unknown", Status: StatusUnknownBidder},
 		},
 		{
-			description:      "Duplicate Syncer",
-			givenBidder:      "a",
-			givenSyncersSeen: map[string]struct{}{"keyA": {}},
-			givenPrivacy:     fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
-			givenCookie:      cookieNeedsSync,
-			expectedSyncer:   nil,
-			expectedBidder:   "a",
-			expectedStatus:   StatusDuplicate,
+			description:        "Duplicate Syncer",
+			givenBidder:        "a",
+			givenSyncersSeen:   map[string]struct{}{"keyA": {}},
+			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
+			givenCookie:        cookieNeedsSync,
+			expectedSyncer:     nil,
+			expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusDuplicate},
 		},
 		{
-			description:      "Incompatible Kind",
-			givenBidder:      "b",
-			givenSyncersSeen: map[string]struct{}{},
-			givenPrivacy:     fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
-			givenCookie:      cookieNeedsSync,
-			expectedSyncer:   nil,
-			expectedBidder:   "b",
-			expectedStatus:   StatusTypeNotSupported,
+			description:        "Incompatible Kind",
+			givenBidder:        "b",
+			givenSyncersSeen:   map[string]struct{}{},
+			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
+			givenCookie:        cookieNeedsSync,
+			expectedSyncer:     nil,
+			expectedEvaluation: BidderEvaluation{Bidder: "b", SyncerKey: "keyB", Status: StatusTypeNotSupported},
 		},
 		{
-			description:      "Already Synced",
-			givenBidder:      "a",
-			givenSyncersSeen: map[string]struct{}{},
-			givenPrivacy:     fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
-			givenCookie:      cookieAlreadyHasSyncForA,
-			expectedSyncer:   nil,
-			expectedBidder:   "a",
-			expectedStatus:   StatusAlreadySynced,
+			description:        "Already Synced",
+			givenBidder:        "a",
+			givenSyncersSeen:   map[string]struct{}{},
+			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
+			givenCookie:        cookieAlreadyHasSyncForA,
+			expectedSyncer:     nil,
+			expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusAlreadySynced},
 		},
 		{
-			description:      "Different Bidder Already Synced",
-			givenBidder:      "a",
-			givenSyncersSeen: map[string]struct{}{},
-			givenPrivacy:     fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
-			givenCookie:      cookieAlreadyHasSyncForB,
-			expectedSyncer:   fakeSyncerA,
-			expectedBidder:   "a",
-			expectedStatus:   StatusOK,
+			description:        "Different Bidder Already Synced",
+			givenBidder:        "a",
+			givenSyncersSeen:   map[string]struct{}{},
+			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
+			givenCookie:        cookieAlreadyHasSyncForB,
+			expectedSyncer:     fakeSyncerA,
+			expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusOK},
 		},
 		{
-			description:      "Blocked By GDPR",
-			givenBidder:      "a",
-			givenSyncersSeen: map[string]struct{}{},
-			givenPrivacy:     fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: false, ccpaAllowsBidderSync: true},
-			givenCookie:      cookieNeedsSync,
-			expectedSyncer:   nil,
-			expectedBidder:   "a",
-			expectedStatus:   StatusBlockedByGDPR,
+			description:        "Blocked By GDPR",
+			givenBidder:        "a",
+			givenSyncersSeen:   map[string]struct{}{},
+			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: false, ccpaAllowsBidderSync: true},
+			givenCookie:        cookieNeedsSync,
+			expectedSyncer:     nil,
+			expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByGDPR},
 		},
 		{
-			description:      "Blocked By CCPA",
-			givenBidder:      "a",
-			givenSyncersSeen: map[string]struct{}{},
-			givenPrivacy:     fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: false},
-			givenCookie:      cookieNeedsSync,
-			expectedSyncer:   nil,
-			expectedBidder:   "a",
-			expectedStatus:   StatusBlockedByCCPA,
+			description:        "Blocked By CCPA",
+			givenBidder:        "a",
+			givenSyncersSeen:   map[string]struct{}{},
+			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: false},
+			givenCookie:        cookieNeedsSync,
+			expectedSyncer:     nil,
+			expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByCCPA},
 		},
 	}
 
@@ -338,9 +329,7 @@ func TestChooserEvaluate(t *testing.T) {
 		sync, evaluation := chooser.evaluate(test.givenBidder, test.givenSyncersSeen, syncTypeFilter, test.givenPrivacy, &test.givenCookie)
 
 		assert.Equal(t, test.expectedSyncer, sync, test.description+":syncer")
-
-		expectedEvaluation := BidderEvaluation{Bidder: test.expectedBidder, Status: test.expectedStatus}
-		assert.Equal(t, expectedEvaluation, evaluation, test.description+":evaluation")
+		assert.Equal(t, test.expectedEvaluation, evaluation, test.description+":evaluation")
 	}
 }