Repository: prebid/prebid-server
Pull request #1870 (2021-05-28 11:48:45)

Between: Fix for bid floor issue#1787
 - removed bidfloor from bidder extension
 - since BetweenSSP now supports default bidfloor, removed default bidfloor from code
 - removed bidfloor from tests


Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index fb9b6592..a1459626 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -3,7 +3,7 @@ name: Release
 on:
   push:
     tags:
-      - '[0-9]+.[0-9]+.[0-9]+'
+      - 'v[0-9]+.[0-9]+.[0-9]+'
 
 jobs:
   release:
@@ -13,13 +13,15 @@ jobs:
     steps:
       - name: Get Version
         id: get_version
-        run: echo ::set-output name=version::${GITHUB_REF/refs\/tags\//}
+        run: |
+          echo ::set-output name=tag::${GITHUB_REF/refs\/tags\/}
+          echo ::set-output name=version::${GITHUB_REF/refs\/tags\/v}
 
       - name: Create & Publish Release
         uses: release-drafter/release-drafter@v5.12.1
         with:
           name: ${{ steps.get_version.outputs.version }}
-          tag: ${{ steps.get_version.outputs.version }}
+          tag: ${{ steps.get_version.outputs.tag }}
           version: ${{ steps.get_version.outputs.version }}
           publish: true
         env:
diff --git a/README.md b/README.md
index 83ac89a9..f3338ce1 100644
--- a/README.md
+++ b/README.md
@@ -50,6 +50,11 @@ go build .
 Load the landing page in your browser at `http://localhost:8000/`.
 For the full API reference, see [the endpoint documentation](https://docs.prebid.org/prebid-server/endpoints/pbs-endpoint-overview.html)
 
+## Go Modules
+
+The packages within this repository are intended to be used as part of the Prebid Server compiled binary. If you
+choose to import Prebid Server packages in other projects, please understand we make no promises on the stability
+of exported types.
 
 ## Contributing
 
@@ -59,7 +64,7 @@ Report bugs, request features, and suggest improvements [on Github](https://gith
 
 Or better yet, [open a pull request](https://github.com/prebid/prebid-server/compare) with the changes you'd like to see.
 
-## IDE Setup for PBS-Go development
+## IDE Recommendations
 
-The quickest way to start developing PBS-Go in a reproducible environment isolated from your host OS
-is by using this [VScode Remote Container Setup](devcontainer.md)
+The quickest way to start developing Prebid Server in a reproducible environment isolated from your host OS
+is by using Visual Studio Code with [Remote Container Setup](devcontainer.md).
diff --git a/adapters/appnexus/appnexus.go b/adapters/appnexus/appnexus.go
index 4d4324c4..b1004601 100644
--- a/adapters/appnexus/appnexus.go
+++ b/adapters/appnexus/appnexus.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io/ioutil"
 	"math/rand"
@@ -301,11 +302,20 @@ func (a *AppNexusAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *ad
 			defaultDisplayManagerVer = fmt.Sprintf("%s-%s", source, version)
 		}
 	}
+	var adPodId *bool
+
 	for i := 0; i < len(request.Imp); i++ {
-		memberId, err := preprocess(&request.Imp[i], defaultDisplayManagerVer)
+		memberId, impAdPodId, err := preprocess(&request.Imp[i], defaultDisplayManagerVer)
 		if memberId != "" {
 			memberIds[memberId] = true
 		}
+		if adPodId == nil {
+			adPodId = &impAdPodId
+		} else if *adPodId != impAdPodId {
+			errs = append(errs, errors.New("generate ad pod option should be same for all pods in request"))
+			return nil, errs
+		}
+
 		// If the preprocessing failed, the server won't be able to bid on this Imp. Delete it, and note the error.
 		if err != nil {
 			errs = append(errs, err)
@@ -362,12 +372,13 @@ func (a *AppNexusAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *ad
 
 	imps := request.Imp
 
-	// For long form requests adpod_id must be sent downstream.
+	// For long form requests if adpodId feature enabled, adpod_id must be sent downstream.
 	// Adpod id is a unique identifier for pod
 	// All impressions in the same pod must have the same pod id in request extension
 	// For this all impressions in  request should belong to the same pod
 	// If impressions number per pod is more than maxImpsPerReq - divide those imps to several requests but keep pod id the same
-	if isVIDEO == 1 {
+	// If  adpodId feature disabled and impressions number per pod is more than maxImpsPerReq  - divide those imps to several requests but do not include ad pod id
+	if isVIDEO == 1 && *adPodId {
 		podImps := groupByPods(imps)
 
 		requests := make([]*adapters.RequestData, 0, len(podImps))
@@ -463,15 +474,15 @@ func keys(m map[string]bool) []string {
 // preprocess mutates the imp to get it ready to send to appnexus.
 //
 // It returns the member param, if it exists, and an error if anything went wrong during the preprocessing.
-func preprocess(imp *openrtb2.Imp, defaultDisplayManagerVer string) (string, error) {
+func preprocess(imp *openrtb2.Imp, defaultDisplayManagerVer string) (string, bool, error) {
 	var bidderExt adapters.ExtImpBidder
 	if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
-		return "", err
+		return "", false, err
 	}
 
 	var appnexusExt openrtb_ext.ExtImpAppnexus
 	if err := json.Unmarshal(bidderExt.Bidder, &appnexusExt); err != nil {
-		return "", err
+		return "", false, err
 	}
 
 	// Accept legacy Appnexus parameters if we don't have modern ones
@@ -487,7 +498,7 @@ func preprocess(imp *openrtb2.Imp, defaultDisplayManagerVer string) (string, err
 	}
 
 	if appnexusExt.PlacementId == 0 && (appnexusExt.InvCode == "" || appnexusExt.Member == "") {
-		return "", &errortypes.BadInput{
+		return "", false, &errortypes.BadInput{
 			Message: "No placement or member+invcode provided",
 		}
 	}
@@ -529,10 +540,10 @@ func preprocess(imp *openrtb2.Imp, defaultDisplayManagerVer string) (string, err
 	}}
 	var err error
 	if imp.Ext, err = json.Marshal(&impExt); err != nil {
-		return appnexusExt.Member, err
+		return appnexusExt.Member, appnexusExt.AdPodId, err
 	}
 
-	return appnexusExt.Member, nil
+	return appnexusExt.Member, appnexusExt.AdPodId, nil
 }
 
 func makeKeywordStr(keywords []*openrtb_ext.ExtImpAppnexusKeyVal) string {
diff --git a/adapters/audienceNetwork/facebook.go b/adapters/audienceNetwork/facebook.go
index 5cbdbc90..63138c42 100644
--- a/adapters/audienceNetwork/facebook.go
+++ b/adapters/audienceNetwork/facebook.go
@@ -16,6 +16,7 @@ import (
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/prebid/prebid-server/util/jsonutil"
 	"github.com/prebid/prebid-server/util/maputil"
 )
 
@@ -95,6 +96,12 @@ func (this *FacebookAdapter) buildRequests(request *openrtb2.BidRequest) ([]*ada
 			continue
 		}
 
+		body, err = jsonutil.DropElement(body, "consented_providers_settings")
+		if err != nil {
+			errs = append(errs, err)
+			return reqs, errs
+		}
+
 		reqs = append(reqs, &adapters.RequestData{
 			Method:  "POST",
 			Uri:     this.URI,
diff --git a/adapters/between/between.go b/adapters/between/between.go
index 9d77a041..f710855a 100644
--- a/adapters/between/between.go
+++ b/adapters/between/between.go
@@ -20,10 +20,6 @@ type BetweenAdapter struct {
 	EndpointTemplate template.Template
 }
 
-// BetweenSSP requires bidfloor > 0.
-// If BidFloor of openrtb_ext.ExtImpBetween is zero, set it to defaultBidFloor value, see addImpProps
-const defaultBidfloor = 0.00001
-
 func (a *BetweenAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
 	var errors []error
 	if len(request.Imp) == 0 {
@@ -129,14 +125,6 @@ func buildImpBanner(imp *openrtb2.Imp) error {
 // Add Between required properties to Imp object
 func addImpProps(imp *openrtb2.Imp, secure *int8, betweenExt *openrtb_ext.ExtImpBetween) {
 	imp.Secure = secure
-	if betweenExt.BidFloor <= 0 {
-		imp.BidFloor = defaultBidfloor
-	} else {
-		imp.BidFloor = betweenExt.BidFloor
-	}
-	if betweenExt.BidFloorCur != "" {
-		imp.BidFloorCur = betweenExt.BidFloorCur
-	}
 }
 
 // Adding header fields to request header
diff --git a/adapters/pubmatic/pubmatic.go b/adapters/pubmatic/pubmatic.go
index 2e361fa5..dc1ee9d0 100644
--- a/adapters/pubmatic/pubmatic.go
+++ b/adapters/pubmatic/pubmatic.go
@@ -66,6 +66,10 @@ const (
 	INVALID_HEIGHT    = "Invalid Height"
 	INVALID_MEDIATYPE = "Invalid MediaType"
 	INVALID_ADSLOT    = "Invalid AdSlot"
+
+	dctrKeyName        = "key_val"
+	pmZoneIDKeyName    = "pmZoneId"
+	pmZoneIDKeyNameOld = "pmZoneID"
 )
 
 func PrepareLogMessage(tID, pubId, adUnitId, bidID, details string, args ...interface{}) string {
@@ -485,30 +489,43 @@ func parseImpressionObject(imp *openrtb2.Imp, wrapExt *string, pubID *string) er
 		imp.Banner = bannerCopy
 	}
 
+	extMap := make(map[string]interface{}, 0)
 	if pubmaticExt.Keywords != nil && len(pubmaticExt.Keywords) != 0 {
-		kvstr := makeKeywordStr(pubmaticExt.Keywords)
-		imp.Ext = json.RawMessage([]byte(kvstr))
-	} else {
-		imp.Ext = nil
+		addKeywordsToExt(pubmaticExt.Keywords, extMap)
+	}
+	//Give preference to direct values of 'dctr' & 'pmZoneId' params in extension
+	if pubmaticExt.Dctr != "" {
+		extMap[dctrKeyName] = pubmaticExt.Dctr
+	}
+	if pubmaticExt.PmZoneID != "" {
+		extMap[pmZoneIDKeyName] = pubmaticExt.PmZoneID
+	}
+
+	imp.Ext = nil
+	if len(extMap) > 0 {
+		ext, err := json.Marshal(extMap)
+		if err == nil {
+			imp.Ext = ext
+		}
 	}
 
 	return nil
 
 }
 
-func makeKeywordStr(keywords []*openrtb_ext.ExtImpPubmaticKeyVal) string {
-	eachKv := make([]string, 0, len(keywords))
+func addKeywordsToExt(keywords []*openrtb_ext.ExtImpPubmaticKeyVal, extMap map[string]interface{}) {
 	for _, keyVal := range keywords {
 		if len(keyVal.Values) == 0 {
 			logf("No values present for key = %s", keyVal.Key)
 			continue
 		} else {
-			eachKv = append(eachKv, fmt.Sprintf("\"%s\":\"%s\"", keyVal.Key, strings.Join(keyVal.Values[:], ",")))
+			key := keyVal.Key
+			if keyVal.Key == pmZoneIDKeyNameOld {
+				key = pmZoneIDKeyName
+			}
+			extMap[key] = strings.Join(keyVal.Values[:], ",")
 		}
 	}
-
-	kvStr := "{" + strings.Join(eachKv, ",") + "}"
-	return kvStr
 }
 
 func prepareImpressionExt(keywords map[string]string) string {
diff --git a/config/config.go b/config/config.go
index afb4ac8d..27e917e4 100644
--- a/config/config.go
+++ b/config/config.go
@@ -612,7 +612,7 @@ func (cfg *Configuration) setDerivedDefaults() {
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderGrid, "https://x.bidswitch.net/check_uuid/"+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dgrid%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BBSW_UUID%7D?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderGumGum, "https://rtb.gumgum.com/usync/prbds2s?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dgumgum%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderImprovedigital, "https://ad.360yield.com/server_match?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dimprovedigital%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7BPUB_USER_ID%7D")
-	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderIx, "https://ssum.casalemedia.com/usermatchredir?s=184674&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&cb="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dix%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D")
+	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderIx, "https://ssum.casalemedia.com/usermatchredir?s=194962&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&cb="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dix%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D")
 	// openrtb_ext.BidderInvibes doesn't have a good default.
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderJixie, "https://id.jixie.io/api/sync?pid=&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Djixie%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%25%25JXUID%25%25")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderKrushmedia, "https://cs.krushmedia.com/4e4abdd5ecc661643458a730b1aa927d.gif?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dkrushmedia%26uid%3D%5BUID%5D")
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 0d3d93cf..ba70305f 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -99,6 +99,16 @@ func (big *bidIDGenerator) New() (string, error) {
 	return rawUuid.String(), err
 }
 
+type deduplicateChanceGenerator interface {
+	Generate() bool
+}
+
+type randomDeduplicateBidBooleanGenerator struct{}
+
+func (randomDeduplicateBidBooleanGenerator) Generate() bool {
+	return rand.Intn(100) < 50
+}
+
 func NewExchange(adapters map[openrtb_ext.BidderName]adaptedBidder, cache prebid_cache_client.Client, cfg *config.Configuration, metricsEngine metrics.MetricsEngine, infos config.BidderInfos, gDPR gdpr.Permissions, currencyConverter *currency.RateConverter, categoriesFetcher stored_requests.CategoryFetcher) Exchange {
 	return &exchange{
 		adapterMap:          adapters,
@@ -205,7 +215,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *
 		//If includebrandcategory is present in ext then CE feature is on.
 		if requestExt.Prebid.Targeting != nil && requestExt.Prebid.Targeting.IncludeBrandCategory != nil {
 			var rejections []string
-			bidCategory, adapterBids, rejections, err = applyCategoryMapping(ctx, requestExt, adapterBids, e.categoriesFetcher, targData)
+			bidCategory, adapterBids, rejections, err = applyCategoryMapping(ctx, requestExt, adapterBids, e.categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{})
 			if err != nil {
 				return nil, fmt.Errorf("Error in category mapping : %s", err.Error())
 			}
@@ -612,7 +622,7 @@ func encodeBidResponseExt(bidResponseExt *openrtb_ext.ExtBidResponse) ([]byte, e
 	return buffer.Bytes(), err
 }
 
-func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtRequest, seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, categoriesFetcher stored_requests.CategoryFetcher, targData *targetData) (map[string]string, map[openrtb_ext.BidderName]*pbsOrtbSeatBid, []string, error) {
+func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtRequest, seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, categoriesFetcher stored_requests.CategoryFetcher, targData *targetData, booleanGenerator deduplicateChanceGenerator) (map[string]string, map[openrtb_ext.BidderName]*pbsOrtbSeatBid, []string, error) {
 	res := make(map[string]string)
 
 	type bidDedupe struct {
@@ -741,7 +751,7 @@ func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtReques
 					currBidPrice = 0
 				}
 				if dupeBidPrice == currBidPrice {
-					if rand.Intn(100) < 50 {
+					if booleanGenerator.Generate() {
 						dupeBidPrice = -1
 					} else {
 						currBidPrice = -1
@@ -756,11 +766,16 @@ func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtReques
 					} else {
 						// An older bid from a different seatBid we've already finished with
 						oldSeatBid := (seatBids)[dupe.bidderName]
+						rejections = updateRejections(rejections, dupe.bidID, "Bid was deduplicated")
 						if len(oldSeatBid.bids) == 1 {
 							seatBidsToRemove = append(seatBidsToRemove, dupe.bidderName)
-							rejections = updateRejections(rejections, dupe.bidID, "Bid was deduplicated")
 						} else {
-							oldSeatBid.bids = append(oldSeatBid.bids[:dupe.bidIndex], oldSeatBid.bids[dupe.bidIndex+1:]...)
+							// This is a very rare, but still possible case where bid needs to be removed from already processed bidder
+							// This happens when current processing bidder has a bid that has same deduplication key as a bid from already processed bidder
+							// and already processed bid was selected to be removed
+							// See example of input data in unit test `TestCategoryMappingTwoBiddersManyBidsEachNoCategorySamePrice`
+							// Need to remove bid by name, not index in this case
+							removeBidById(oldSeatBid, dupe.bidID)
 						}
 					}
 					delete(res, dupe.bidID)
@@ -798,6 +813,24 @@ func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtReques
 	return res, seatBids, rejections, nil
 }
 
+func removeBidById(seatBid *pbsOrtbSeatBid, bidID string) {
+	//Find index of bid to remove
+	dupeBidIndex := -1
+	for i, bid := range seatBid.bids {
+		if bid.bid.ID == bidID {
+			dupeBidIndex = i
+			break
+		}
+	}
+	if dupeBidIndex != -1 {
+		if dupeBidIndex < len(seatBid.bids)-1 {
+			seatBid.bids = append(seatBid.bids[:dupeBidIndex], seatBid.bids[dupeBidIndex+1:]...)
+		} else if dupeBidIndex == len(seatBid.bids)-1 {
+			seatBid.bids = seatBid.bids[:len(seatBid.bids)-1]
+		}
+	}
+}
+
 func updateRejections(rejections []string, bidID string, reason string) []string {
 	message := fmt.Sprintf("bid rejected [bid ID: %s] reason: %s", bidID, reason)
 	return append(rejections, message)
diff --git a/openrtb_ext/imp_appnexus.go b/openrtb_ext/imp_appnexus.go
index 9ef69d06..83d762b3 100644
--- a/openrtb_ext/imp_appnexus.go
+++ b/openrtb_ext/imp_appnexus.go
@@ -17,6 +17,7 @@ type ExtImpAppnexus struct {
 	UsePmtRule              *bool                   `json:"use_pmt_rule"`
 	// At this time we do no processing on the private sizes, so just leaving it as a JSON blob.
 	PrivateSizes json.RawMessage `json:"private_sizes"`
+	AdPodId      bool            `json:"generate_ad_pod_id"`
 }
 
 // ExtImpAppnexusKeyVal defines the contract for bidrequest.imp[i].ext.appnexus.keywords[i]
diff --git a/openrtb_ext/imp_between.go b/openrtb_ext/imp_between.go
index c868baeb..9fe912a8 100644
--- a/openrtb_ext/imp_between.go
+++ b/openrtb_ext/imp_between.go
@@ -1,8 +1,6 @@
 package openrtb_ext
 
 type ExtImpBetween struct {
-	Host        string  `json:"host"`
-	PublisherID string  `json:"publisher_id"`
-	BidFloor    float64 `json:"bid_floor"`
-	BidFloorCur string  `json:"bid_floor_cur"`
+	Host        string `json:"host"`
+	PublisherID string `json:"publisher_id"`
 }
diff --git a/openrtb_ext/imp_pubmatic.go b/openrtb_ext/imp_pubmatic.go
index fd97836d..cf5093a2 100644
--- a/openrtb_ext/imp_pubmatic.go
+++ b/openrtb_ext/imp_pubmatic.go
@@ -11,6 +11,8 @@ import "encoding/json"
 type ExtImpPubmatic struct {
 	PublisherId string                  `json:"publisherId"`
 	AdSlot      string                  `json:"adSlot"`
+	Dctr        string                  `json:"dctr"`
+	PmZoneID    string                  `json:"pmzoneid"`
 	WrapExt     json.RawMessage         `json:"wrapper,omitempty"`
 	Keywords    []*ExtImpPubmaticKeyVal `json:"keywords,omitempty"`
 }
diff --git a/static/bidder-params/appnexus.json b/static/bidder-params/appnexus.json
index 7da41a67..ce5ddad0 100644
--- a/static/bidder-params/appnexus.json
+++ b/static/bidder-params/appnexus.json
@@ -67,6 +67,10 @@
       "type": "boolean",
       "description": "Boolean to signal AppNexus to apply the relevant payment rule"
     },
+    "generate_ad_pod_id": {
+      "type": "boolean",
+      "description": "Boolean to signal AppNexus to add ad pod id to each request"
+    },
     "private_sizes" :{
       "type": "array",
       "items": {
diff --git a/static/bidder-params/between.json b/static/bidder-params/between.json
index 032d38fe..64863462 100644
--- a/static/bidder-params/between.json
+++ b/static/bidder-params/between.json
@@ -12,15 +12,6 @@
     "publisher_id": {
       "type": "string",
       "description": "Publisher ID from Between Exchange control panel"
-    },
-    "bid_floor": {
-      "type": "number",
-      "description": "The minimum price acceptable for a bid"
-    },
-    "bid_floor_cur": {
-      "type": "string",
-      "description": "Currency of bid floor",
-      "enum": ["USD", "EUR", "RUB"]
     }
   },
   "required": ["host", "publisher_id"]
diff --git a/static/bidder-params/pubmatic.json b/static/bidder-params/pubmatic.json
index 1b6a2f03..5d41b9fc 100644
--- a/static/bidder-params/pubmatic.json
+++ b/static/bidder-params/pubmatic.json
@@ -12,6 +12,14 @@
 			"type": "string",
 			"description": "An ID which identifies the ad slot"
 		},
+		"pmzoneid": {
+			"type": "string",
+			"description": "Comma separated zone id. Used im deal targeting & site section targeting. e.g drama,sport"
+		},
+		"dctr": {
+			"type": "string",
+			"description": "Deals Custom Targeting, pipe separated key-value pairs e.g key1=V1,V2,V3|key2=v1|key3=v3,v5"
+		},
 		"wrapper": {
 			"type": "object",
 			"description": "Specifies pubmatic openwrap configuration for a publisher",
diff --git a/util/jsonutil/jsonutil.go b/util/jsonutil/jsonutil.go
new file mode 100644
index 00000000..963477de
--- /dev/null
+++ b/util/jsonutil/jsonutil.go
@@ -0,0 +1,57 @@
+package jsonutil
+
+import (
+	"bytes"
+	"encoding/json"
+	"io"
+)
+
+var comma = []byte(",")[0]
+
+func DropElement(extension []byte, elementName string) ([]byte, error) {
+	buf := bytes.NewBuffer(extension)
+	dec := json.NewDecoder(buf)
+	var startIndex int64
+	var i interface{}
+	for {
+		token, err := dec.Token()
+		if err == io.EOF {
+			// io.EOF is a successful end
+			break
+		}
+		if err != nil {
+			return nil, err
+		}
+
+		if token == elementName {
+			err := dec.Decode(&i)
+			if err != nil {
+				return nil, err
+			}
+			endIndex := dec.InputOffset()
+
+			if dec.More() {
+				//if there were other elements before
+				if extension[startIndex] == comma {
+					startIndex++
+				}
+
+				for {
+					//structure has more elements, need to find index of comma
+					if extension[endIndex] == comma {
+						endIndex++
+						break
+					}
+					endIndex++
+				}
+			}
+
+			extension = append(extension[:startIndex], extension[endIndex:]...)
+			break
+		} else {
+			startIndex = dec.InputOffset()
+		}
+
+	}
+	return extension, nil
+}


Test patch:
diff --git a/adapters/adapterstest/test_json.go b/adapters/adapterstest/test_json.go
index 35a5a570..1ec2fc08 100644
--- a/adapters/adapterstest/test_json.go
+++ b/adapters/adapterstest/test_json.go
@@ -57,6 +57,7 @@ func RunJSONBidderTest(t *testing.T, rootDir string, bidder adapters.Bidder) {
 	runTests(t, fmt.Sprintf("%s/supplemental", rootDir), bidder, true, false, false)
 	runTests(t, fmt.Sprintf("%s/amp", rootDir), bidder, true, true, false)
 	runTests(t, fmt.Sprintf("%s/video", rootDir), bidder, false, false, true)
+	runTests(t, fmt.Sprintf("%s/videosupplemental", rootDir), bidder, true, false, true)
 }
 
 // runTests runs all the *.json files in a directory. If allowErrors is false, and one of the test files
diff --git a/adapters/appnexus/appnexus_test.go b/adapters/appnexus/appnexus_test.go
index ead45280..e67df953 100644
--- a/adapters/appnexus/appnexus_test.go
+++ b/adapters/appnexus/appnexus_test.go
@@ -69,7 +69,7 @@ func TestVideoSinglePod(t *testing.T) {
 	req.ID = "test_id"
 
 	reqExt := `{"prebid":{}}`
-	impExt := `{"bidder":{"placementId":123}}`
+	impExt := `{"bidder":{"placementId":123, "generate_ad_pod_id":true}}`
 	req.Ext = []byte(reqExt)
 
 	req.Imp = append(req.Imp, openrtb2.Imp{ID: "1_0", Ext: []byte(impExt)})
@@ -90,7 +90,7 @@ func TestVideoSinglePod(t *testing.T) {
 	error = json.Unmarshal(reqData.Ext, &reqDataExt)
 	assert.NoError(t, error, "Response ext unmarshalling error should be nil")
 
-	regMatch, matchErr := regexp.Match(`[0-9]19`, []byte(reqDataExt.Appnexus.AdPodId))
+	regMatch, matchErr := regexp.Match(`^[0-9]+$`, []byte(reqDataExt.Appnexus.AdPodId))
 	assert.NoError(t, matchErr, "Regex match error should be nil")
 	assert.True(t, regMatch, "AdPod id doesn't present in Appnexus extension or has incorrect format")
 }
@@ -140,7 +140,7 @@ func TestVideoSinglePodManyImps(t *testing.T) {
 	error = json.Unmarshal(reqData1.Ext, &reqDataExt1)
 	assert.NoError(t, error, "Response ext unmarshalling error should be nil")
 
-	adPodId1 := reqDataExt1.Appnexus.AdPodId
+	assert.Equal(t, "", reqDataExt1.Appnexus.AdPodId, "AdPod id should not be present in first request")
 
 	var reqData2 *openrtb2.BidRequest
 	error = json.Unmarshal(res[1].Body, &reqData2)
@@ -150,9 +150,7 @@ func TestVideoSinglePodManyImps(t *testing.T) {
 	error = json.Unmarshal(reqData2.Ext, &reqDataExt2)
 	assert.NoError(t, error, "Response ext unmarshalling error should be nil")
 
-	adPodId2 := reqDataExt2.Appnexus.AdPodId
-
-	assert.Equal(t, adPodId1, adPodId2, "AdPod id is not the same for the same pod")
+	assert.Equal(t, "", reqDataExt2.Appnexus.AdPodId, "AdPod id should not be present in second request")
 }
 
 func TestVideoTwoPods(t *testing.T) {
@@ -167,7 +165,7 @@ func TestVideoTwoPods(t *testing.T) {
 	req.ID = "test_id"
 
 	reqExt := `{"prebid":{}}`
-	impExt := `{"bidder":{"placementId":123}}`
+	impExt := `{"bidder":{"placementId":123, "generate_ad_pod_id": true}}`
 	req.Ext = []byte(reqExt)
 
 	req.Imp = append(req.Imp, openrtb2.Imp{ID: "1_0", Ext: []byte(impExt)})
@@ -219,7 +217,7 @@ func TestVideoTwoPodsManyImps(t *testing.T) {
 	req.ID = "test_id"
 
 	reqExt := `{"prebid":{}}`
-	impExt := `{"bidder":{"placementId":123}}`
+	impExt := `{"bidder":{"placementId":123, "generate_ad_pod_id":true}}`
 	req.Ext = []byte(reqExt)
 
 	req.Imp = append(req.Imp, openrtb2.Imp{ID: "1_0", Ext: []byte(impExt)})
diff --git a/adapters/appnexus/appnexustest/video/video-no-adpodid-two-imps-different-pod.json b/adapters/appnexus/appnexustest/video/video-no-adpodid-two-imps-different-pod.json
new file mode 100644
index 00000000..140cf0b0
--- /dev/null
+++ b/adapters/appnexus/appnexustest/video/video-no-adpodid-two-imps-different-pod.json
@@ -0,0 +1,94 @@
+{
+	"mockBidRequest": {
+	  "id": "test-request-id",
+	  "imp": [
+		{
+		  "id": "1_1",
+		  "video": {
+			"mimes": ["video/mp4"],
+			"minduration": 15,
+			"maxduration": 30,
+			"protocols": [2, 3, 5, 6, 7, 8],
+			"w": 940,
+			"h": 560
+		  },
+		  "ext": {
+			"bidder": {
+			  "placement_id": 1
+			}
+		  }
+		},
+		{
+		  "id": "2_1",
+		  "video": {
+			  "mimes": ["video/mp4"],
+			  "minduration": 15,
+			  "maxduration": 30,
+			  "protocols": [2, 3, 5, 6, 7, 8],
+			  "w": 940,
+			  "h": 560
+		  },
+		  "ext": {
+			  "bidder": {
+				  "placement_id": 1
+			  }
+		  }
+		}
+	  ]
+	},
+
+	"httpCalls": [
+	  {
+		"expectedRequest": {
+		  "uri": "http://ib.adnxs.com/openrtb2",
+		  "body": {
+			"id": "test-request-id",
+			"ext": {
+				"appnexus": {
+				  "hb_source": 6
+				},
+				"prebid": {}
+			},
+			"imp": [
+			  {
+					"id": "1_1",
+					"video": {
+						"mimes": ["video/mp4"],
+						"minduration": 15,
+						"maxduration": 30,
+						"protocols": [2, 3, 5, 6, 7, 8],
+						"w": 940,
+						"h": 560
+					},
+					"ext": {
+						"appnexus": {
+							"placement_id": 1
+						}
+					}
+				},
+			  {
+				"id": "2_1",
+				"video": {
+					"mimes": ["video/mp4"],
+					"minduration": 15,
+					"maxduration": 30,
+					"protocols": [2, 3, 5, 6, 7, 8],
+					"w": 940,
+					"h": 560
+				  },
+				"ext": {
+				  "appnexus": {
+					  "placement_id": 1
+				  }
+				}
+			  }
+			]
+		  }
+		},
+		"mockResponse": {
+		  "status": 200,
+		  "body": {}
+		}
+	  }
+	]
+  }
\ No newline at end of file
diff --git a/adapters/appnexus/appnexustest/video/video-same-adpodid-two-imps-same-pod.json b/adapters/appnexus/appnexustest/video/video-same-adpodid-two-imps-same-pod.json
new file mode 100644
index 00000000..5a453979
--- /dev/null
+++ b/adapters/appnexus/appnexustest/video/video-same-adpodid-two-imps-same-pod.json
@@ -0,0 +1,97 @@
+{
+	"mockBidRequest": {
+	  "id": "test-request-id",
+	  "imp": [
+		{
+		  "id": "1_1",
+		  "video": {
+			"mimes": ["video/mp4"],
+			"minduration": 15,
+			"maxduration": 30,
+			"protocols": [2, 3, 5, 6, 7, 8],
+			"w": 940,
+			"h": 560
+		  },
+		  "ext": {
+			"bidder": {
+			  "placement_id": 1,
+			  "generate_ad_pod_id": true
+			}
+		  }
+		},
+		{
+		  "id": "1_2",
+		  "video": {
+			  "mimes": ["video/mp4"],
+			  "minduration": 15,
+			  "maxduration": 30,
+			  "protocols": [2, 3, 5, 6, 7, 8],
+			  "w": 940,
+			  "h": 560
+		  },
+		  "ext": {
+			  "bidder": {
+				  "placement_id": 1,
+				  "generate_ad_pod_id": true
+			  }
+		  }
+		}
+	  ]
+	},
+
+	"httpCalls": [
+	  {
+		"expectedRequest": {
+		  "uri": "http://ib.adnxs.com/openrtb2",
+		  "body": {
+			"id": "test-request-id",
+			"ext": {
+				"appnexus": {
+				 "adpod_id": "5577006791947779410",
+				  "hb_source": 6
+				},
+				"prebid": {}
+			},
+			"imp": [
+			  {
+				"id": "1_1",
+				"video": {
+					"mimes": ["video/mp4"],
+					"minduration": 15,
+					"maxduration": 30,
+					"protocols": [2, 3, 5, 6, 7, 8],
+					"w": 940,
+					"h": 560
+				  },
+				"ext": {
+				  "appnexus": {
+					  "placement_id": 1
+				  }
+				}
+			  },
+				{
+					"id": "1_2",
+					"video": {
+						"mimes": ["video/mp4"],
+						"minduration": 15,
+						"maxduration": 30,
+						"protocols": [2, 3, 5, 6, 7, 8],
+						"w": 940,
+						"h": 560
+					},
+					"ext": {
+						"appnexus": {
+							"placement_id": 1
+						}
+					}
+				}
+			]
+		  }
+		},
+		"mockResponse": {
+		  "status": 200,
+		  "body": {}
+		}
+	  }
+	]
+  }
\ No newline at end of file
diff --git a/adapters/appnexus/appnexustest/videosupplemental/video-different-adpodid-two-imps-different-pods.json b/adapters/appnexus/appnexustest/videosupplemental/video-different-adpodid-two-imps-different-pods.json
new file mode 100644
index 00000000..3e2aabe4
--- /dev/null
+++ b/adapters/appnexus/appnexustest/videosupplemental/video-different-adpodid-two-imps-different-pods.json
@@ -0,0 +1,48 @@
+{
+	"mockBidRequest": {
+	  "id": "test-request-id",
+	  "imp": [
+		{
+		  "id": "1_1",
+		  "video": {
+			"mimes": ["video/mp4"],
+			"minduration": 15,
+			"maxduration": 30,
+			"protocols": [2, 3, 5, 6, 7, 8],
+			"w": 940,
+			"h": 560
+		  },
+		  "ext": {
+			"bidder": {
+			  "placement_id": 1,
+			  "generate_ad_pod_id": false
+			}
+		  }
+		},
+		{
+		  "id": "2_1",
+		  "video": {
+			  "mimes": ["video/mp4"],
+			  "minduration": 15,
+			  "maxduration": 30,
+			  "protocols": [2, 3, 5, 6, 7, 8],
+			  "w": 940,
+			  "h": 560
+		  },
+		  "ext": {
+			  "bidder": {
+				  "placement_id": 1,
+				  "generate_ad_pod_id": true
+			  }
+		  }
+		}
+	  ]
+	},
+
+	"expectedMakeRequestsErrors": [
+		{
+			"value": "generate ad pod option should be same for all pods in request",
+			"comparison": "literal"
+		}
+	]
+  }
\ No newline at end of file
diff --git a/adapters/appnexus/appnexustest/videosupplemental/video-different-adpodid-two-imps-same-pod.json b/adapters/appnexus/appnexustest/videosupplemental/video-different-adpodid-two-imps-same-pod.json
new file mode 100644
index 00000000..a8efb807
--- /dev/null
+++ b/adapters/appnexus/appnexustest/videosupplemental/video-different-adpodid-two-imps-same-pod.json
@@ -0,0 +1,48 @@
+{
+	"mockBidRequest": {
+	  "id": "test-request-id",
+	  "imp": [
+		{
+		  "id": "1_1",
+		  "video": {
+			"mimes": ["video/mp4"],
+			"minduration": 15,
+			"maxduration": 30,
+			"protocols": [2, 3, 5, 6, 7, 8],
+			"w": 940,
+			"h": 560
+		  },
+		  "ext": {
+			"bidder": {
+			  "placement_id": 1,
+			  "generate_ad_pod_id": false
+			}
+		  }
+		},
+		{
+		  "id": "1_2",
+		  "video": {
+			  "mimes": ["video/mp4"],
+			  "minduration": 15,
+			  "maxduration": 30,
+			  "protocols": [2, 3, 5, 6, 7, 8],
+			  "w": 940,
+			  "h": 560
+		  },
+		  "ext": {
+			  "bidder": {
+				  "placement_id": 1,
+				  "generate_ad_pod_id": true
+			  }
+		  }
+		}
+	  ]
+	},
+
+	"expectedMakeRequestsErrors": [
+		{
+			"value": "generate ad pod option should be same for all pods in request",
+			"comparison": "literal"
+		}
+	]
+  }
\ No newline at end of file
diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/video_consented_providers.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/video_consented_providers.json
new file mode 100644
index 00000000..5b72f70c
--- /dev/null
+++ b/adapters/audienceNetwork/audienceNetworktest/exemplary/video_consented_providers.json
@@ -0,0 +1,145 @@
+{
+    "mockBidRequest": {
+        "id": "test-req-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "video": {
+                    "mimes": ["video/mp4"],
+                    "minduration": 15,
+                    "maxduration": 30,
+                    "protocols": [2, 3, 5, 6, 7, 8],
+                    "linearity": 1,
+                    "w": 940,
+                    "h": 560
+                },
+                "ext": {
+                    "bidder": {
+                        "publisherid": "123",
+                        "placementid": "456"
+                    }
+                }
+            }
+        ],
+        "app": {
+            "id": "app-abc",
+            "bundle": "com.prebid"
+        },
+        "device": {
+            "ip": "152.193.6.74"
+        },
+        "user": {
+            "id": "db089de9-a62e-4861-a881-0ff15e052516",
+            "buyeruid": "v4_bidder_token",
+            "ext": {
+                "consent": "TESTCONSENT",
+                "consented_providers_settings": {
+                    "consented_providers": [1608,765,492,1365,5678,1545,2563,1411]
+                }
+            }
+        },
+        "tmax": 500
+    },
+    "httpcalls": [
+        {
+            "expectedRequest": {
+                "uri": "https://an.facebook.com/placementbid.ortb",
+                "headers": {
+                    "Accept": [
+                        "application/json"
+                    ],
+                    "Content-Type": [
+                        "application/json;charset=utf-8"
+                    ],
+                    "X-Fb-Pool-Routing-Token": [
+                        "v4_bidder_token"
+                    ]
+                },
+                "body": {
+                    "id": "test-imp-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "video": {
+                                "mimes": ["video/mp4"],
+                                "minduration": 15,
+                                "maxduration": 30,
+                                "protocols": [2, 3, 5, 6, 7, 8],
+                                "linearity": 1,
+                                "w": 0,
+                                "h": 0
+                            },
+                            "tagid": "123_456"
+                        }
+                    ],
+                    "app": {
+                        "id": "app-abc",
+                        "bundle": "com.prebid",
+                        "publisher": {
+                            "id": "123"
+                        }
+                    },
+                    "device": {
+                        "ip": "152.193.6.74"
+                    },
+                    "user": {
+                        "id": "db089de9-a62e-4861-a881-0ff15e052516",
+                        "buyeruid": "v4_bidder_token",
+                        "ext": {
+                            "consent": "TESTCONSENT"
+                        }
+                    },
+                    "tmax": 500,
+                    "ext": {
+                        "authentication_id": "4e24a2b23fbfb5e41a9093b921d6cddf497c24dd5f63879038cec2ab2f27d174",
+                        "platformid": "test-platform-id"
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-imp-id",
+                    "seatbid": [
+                        {
+                            "bid": [
+                                {
+                                    "id": "987",
+                                    "impid": "test-imp-id",
+                                    "price": 1.000000,
+                                    "adm": "{\"type\":\"ID\",\"bid_id\":\"987\",\"placement_id\":\"123_456\",\"resolved_placement_id\":\"123_456\",\"sdk_version\":\"5.5.0\",\"device_id\":\"abc\",\"template\":1,\"payload\":null,\"bid_time_token\":\"v4_bidder_token=\"}",
+                                    "nurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=0&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0",
+                                    "lurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=${AUCTION_LOSS}&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0",
+                                    "burl": "https://www.facebook.com/audiencenetwork/burl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&clearing_price=${AUCTION_PRICE}"
+                                }
+                            ]
+                        }
+                    ],
+                    "bidid": "654",
+                    "cur": "USD"
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "currency": "USD",
+            "bids": [
+                {
+                    "bid": {
+                        "id": "987",
+                        "impid": "test-imp-id",
+                        "price": 1,
+                        "adm": "{\"type\":\"ID\",\"bid_id\":\"987\",\"placement_id\":\"123_456\",\"resolved_placement_id\":\"123_456\",\"sdk_version\":\"5.5.0\",\"device_id\":\"abc\",\"template\":1,\"payload\":null,\"bid_time_token\":\"v4_bidder_token=\"}",
+                        "adid": "987",
+                        "crid": "987",
+                        "nurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=0&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0",
+                        "lurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=${AUCTION_LOSS}&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0",
+                        "burl": "https://www.facebook.com/audiencenetwork/burl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&clearing_price=${AUCTION_PRICE}"
+                    },
+                    "type": "video"
+                }
+            ]
+        }
+    ]
+}
diff --git a/adapters/between/betweentest/exemplary/multi-request.json b/adapters/between/betweentest/exemplary/multi-request.json
index 0a68f204..345e99dd 100644
--- a/adapters/between/betweentest/exemplary/multi-request.json
+++ b/adapters/between/betweentest/exemplary/multi-request.json
@@ -71,7 +71,6 @@
                   "publisher_id": "1"
                 }
               },
-              "bidfloor": 1e-05,
               "secure": 0
             },
             {
@@ -92,7 +91,6 @@
                   "publisher_id": "1"
                 }
               },
-              "bidfloor": 1e-05,
               "secure": 0
             }
           ],
diff --git a/adapters/between/betweentest/exemplary/secure-detect.json b/adapters/between/betweentest/exemplary/secure-detect.json
index ab636ecb..9df77f0d 100644
--- a/adapters/between/betweentest/exemplary/secure-detect.json
+++ b/adapters/between/betweentest/exemplary/secure-detect.json
@@ -46,7 +46,6 @@
                   "w": 728,
                   "h": 90
                 },
-                "bidfloor": 1e-05,
                 "secure": 1,
                 "ext": {
                   "bidder": {
diff --git a/adapters/between/betweentest/exemplary/simple-site-banner.json b/adapters/between/betweentest/exemplary/simple-site-banner.json
index d11f2033..bcb7b30e 100644
--- a/adapters/between/betweentest/exemplary/simple-site-banner.json
+++ b/adapters/between/betweentest/exemplary/simple-site-banner.json
@@ -21,9 +21,7 @@
         "ext": {
           "bidder": {
             "host": "127.0.0.1",
-            "publisher_id": "1",
-            "bid_floor_cur": "RUB",
-            "bid_floor": 1000
+            "publisher_id": "1"
           }
         }
       }
@@ -48,15 +46,11 @@
                 "w": 728,
                 "h": 90
               },
-              "bidfloor": 1000,
-              "bidfloorcur": "RUB",
               "secure": 0,
               "ext": {
                 "bidder": {
                   "host": "127.0.0.1",
-                  "publisher_id": "1",
-                  "bid_floor_cur": "RUB",
-                  "bid_floor": 1000
+                  "publisher_id": "1"
                 }
               }
             }
diff --git a/adapters/between/betweentest/supplemental/bad-dsp-request-example.json b/adapters/between/betweentest/supplemental/bad-dsp-request-example.json
index 4e9bbac2..0e523135 100644
--- a/adapters/between/betweentest/supplemental/bad-dsp-request-example.json
+++ b/adapters/between/betweentest/supplemental/bad-dsp-request-example.json
@@ -46,7 +46,6 @@
                 "w": 728,
                 "h": 90
               },
-              "bidfloor": 1e-05,
               "secure": 0,
               "ext": {
                 "bidder": {
diff --git a/adapters/between/betweentest/supplemental/bad-response-body.json b/adapters/between/betweentest/supplemental/bad-response-body.json
index fbd08206..a916136c 100644
--- a/adapters/between/betweentest/supplemental/bad-response-body.json
+++ b/adapters/between/betweentest/supplemental/bad-response-body.json
@@ -42,7 +42,6 @@
                 "w": 300,
                 "h": 250
               },
-              "bidfloor": 1e-05,
               "secure": 0,
               "ext": {
                 "bidder": {
diff --git a/adapters/between/betweentest/supplemental/dsp-server-internal-error-example.json b/adapters/between/betweentest/supplemental/dsp-server-internal-error-example.json
index 67e69798..f17644bd 100644
--- a/adapters/between/betweentest/supplemental/dsp-server-internal-error-example.json
+++ b/adapters/between/betweentest/supplemental/dsp-server-internal-error-example.json
@@ -46,7 +46,6 @@
                 "w": 728,
                 "h": 90
               },
-              "bidfloor": 1e-05,
               "secure": 0,
               "ext": {
                 "bidder": {
diff --git a/adapters/between/betweentest/supplemental/no-bids.json b/adapters/between/betweentest/supplemental/no-bids.json
index d73c3e62..d82828a9 100644
--- a/adapters/between/betweentest/supplemental/no-bids.json
+++ b/adapters/between/betweentest/supplemental/no-bids.json
@@ -46,7 +46,6 @@
                 "w": 728,
                 "h": 90
               },
-              "bidfloor": 1e-05,
               "secure": 0,
               "ext": {
                 "bidder": {
diff --git a/adapters/between/params_test.go b/adapters/between/params_test.go
index 85771869..f0594c87 100644
--- a/adapters/between/params_test.go
+++ b/adapters/between/params_test.go
@@ -41,16 +41,15 @@ func TestInvalidParams(t *testing.T) {
 
 var validParams = []string{
 	`{"host":"lbs-eu1.ads", "publisher_id": "1"}`,
-	`{"host":"lbs-eu1.ads", "publisher_id": "1", "bid_floor": 5.5}`,
-	`{"host":"lbs-eu1.ads", "publisher_id": "1", "bid_floor": 1, "bid_floor_cur": "RUB"}`,
+	`{"host":"lbs-ru1.ads", "publisher_id": "2"}`,
+	`{"host":"lbs-us-east1.ads", "publisher_id": "3"}`,
+	`{"host":"lbs-asia1.ads", "publisher_id": "4"}`,
 }
 
 var invalidParams = []string{
 	`{"host":"badhost.ads", "publisher_id": "1"}`,
 	`{"host":"lbs-eu1.ads", "publisher_id": 1}`,
-	`{"host":"lbs-eu1.ads", "publisher_id": "1"", "bid_floor": 5.5}`,
-	`{"host":"lbs-eu1.ads", "publisher_id": "1", "bid_floor": "5.5"}`,
-	`{"host":"lbs-eu1.ads", "publisher_id": "1", "bid_floor": 5.5, "bid_floor_cur": "cur"}`,
+	`{"host":"lbs-eu1.ads", "publisher_id": "1""}`,
 	``,
 	`null`,
 	`true`,
diff --git a/adapters/pubmatic/pubmatictest/exemplary/banner.json b/adapters/pubmatic/pubmatictest/exemplary/banner.json
index 1eb8a212..f525673c 100644
--- a/adapters/pubmatic/pubmatictest/exemplary/banner.json
+++ b/adapters/pubmatic/pubmatictest/exemplary/banner.json
@@ -61,7 +61,7 @@
                 "w": 300
             },
                 "ext": {
-                    "pmZoneID": "Zone1,Zone2",
+                    "pmZoneId": "Zone1,Zone2",
                     "preference": "sports,movies"
                 }
               }
diff --git a/adapters/pubmatic/pubmatictest/exemplary/video.json b/adapters/pubmatic/pubmatictest/exemplary/video.json
index 4c874535..1715e477 100644
--- a/adapters/pubmatic/pubmatictest/exemplary/video.json
+++ b/adapters/pubmatic/pubmatictest/exemplary/video.json
@@ -73,7 +73,7 @@
                     "maxbitrate": 10
                 },
                 "ext": {
-                    "pmZoneID": "Zone1,Zone2"
+                    "pmZoneId": "Zone1,Zone2"
                 }
               }
             ], 
diff --git a/adapters/pubmatic/pubmatictest/params/race/banner.json b/adapters/pubmatic/pubmatictest/params/race/banner.json
index 86ddc70b..ab88871e 100644
--- a/adapters/pubmatic/pubmatictest/params/race/banner.json
+++ b/adapters/pubmatic/pubmatictest/params/race/banner.json
@@ -1,6 +1,8 @@
 {
 	"publisherId": "156209",
 	"adSlot": "pubmatic_test2@300x250",
+	"pmzoneid": "drama,sport",
+	"dctr": "abBucket=4|adType=page|entity=|paidByCategory=|sku=|userLevel=free|platform=android|majorVersion=3.54|version=3.54.0|mobileApplication=true|showId=20166|show=Kisah Untuk Geri|genre=Drama|contentUrl=https://www.iflix.com/title/show/20166|rating=TV-MA|contentLanguage=id",
 	"keywords": {
 		"pmZoneID": "Zone1,Zone2",
 		"preference": "sports,movies"
diff --git a/adapters/pubmatic/pubmatictest/params/race/video.json b/adapters/pubmatic/pubmatictest/params/race/video.json
index 86ddc70b..ab88871e 100644
--- a/adapters/pubmatic/pubmatictest/params/race/video.json
+++ b/adapters/pubmatic/pubmatictest/params/race/video.json
@@ -1,6 +1,8 @@
 {
 	"publisherId": "156209",
 	"adSlot": "pubmatic_test2@300x250",
+	"pmzoneid": "drama,sport",
+	"dctr": "abBucket=4|adType=page|entity=|paidByCategory=|sku=|userLevel=free|platform=android|majorVersion=3.54|version=3.54.0|mobileApplication=true|showId=20166|show=Kisah Untuk Geri|genre=Drama|contentUrl=https://www.iflix.com/title/show/20166|rating=TV-MA|contentLanguage=id",
 	"keywords": {
 		"pmZoneID": "Zone1,Zone2",
 		"preference": "sports,movies"
diff --git a/adapters/pubmatic/pubmatictest/supplemental/app.json b/adapters/pubmatic/pubmatictest/supplemental/app.json
index 636433ca..e9f1048e 100644
--- a/adapters/pubmatic/pubmatictest/supplemental/app.json
+++ b/adapters/pubmatic/pubmatictest/supplemental/app.json
@@ -61,7 +61,7 @@
                 "w": 300
             },
                 "ext": {
-                    "pmZoneID": "Zone1,Zone2",
+                    "pmZoneId": "Zone1,Zone2",
                     "preference": "sports,movies"
                 }
               }
diff --git a/adapters/pubmatic/pubmatictest/supplemental/dctrAndPmZoneID.json b/adapters/pubmatic/pubmatictest/supplemental/dctrAndPmZoneID.json
new file mode 100644
index 00000000..d68517de
--- /dev/null
+++ b/adapters/pubmatic/pubmatictest/supplemental/dctrAndPmZoneID.json
@@ -0,0 +1,162 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "adSlot": "AdTag_Div1@300x250",
+            "publisherId": "  999  ",
+            "pmzoneid": "drama,sport",
+            "dctr": "abBucket=4|adType=page|entity=|paidByCategory=|sku=|userLevel=free|platform=android|majorVersion=3.54|version=3.54.0|mobileApplication=true|showId=20166|show=Kisah Untuk Geri|genre=Drama|contentUrl=https://www.iflix.com/title/show/20166|rating=TV-MA|contentLanguage=id",
+            "keywords": [
+              {
+                "key": "pmZoneID",
+                "value": [
+                  "Zone1",
+                  "Zone2"
+                ]
+              },
+              {
+                "key": "preference",
+                "value": [
+                  "sports",
+                  "movies"
+                ]
+              }
+            ],
+            "wrapper": {
+              "version": 1,
+              "profile": 5123
+            }
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
+    },
+    "site": {
+      "id": "siteID",
+      "publisher": {
+        "id": "1234"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://hbopenbid.pubmatic.com/translator?source=prebid-server",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "tagid": "AdTag_Div1",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ],
+                "h": 250,
+                "w": 300
+              },
+              "ext": {
+                "key_val": "abBucket=4|adType=page|entity=|paidByCategory=|sku=|userLevel=free|platform=android|majorVersion=3.54|version=3.54.0|mobileApplication=true|showId=20166|show=Kisah Untuk Geri|genre=Drama|contentUrl=https://www.iflix.com/title/show/20166|rating=TV-MA|contentLanguage=id",
+                "pmZoneId": "drama,sport",
+                "preference": "sports,movies"
+              }
+            }
+          ],
+          "device": {
+            "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
+          },
+          "site": {
+            "id": "siteID",
+            "publisher": {
+              "id": "999"
+            }
+          },
+          "ext": {
+            "wrapper": {
+              "profile": 5123,
+              "version": 1
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "958",
+              "bid": [
+                {
+                  "id": "7706636740145184841",
+                  "impid": "test-imp-id",
+                  "price": 0.500000,
+                  "adid": "29681110",
+                  "adm": "some-test-ad",
+                  "adomain": [
+                    "pubmatic.com"
+                  ],
+                  "crid": "29681110",
+                  "h": 250,
+                  "w": 300,
+                  "dealid": "test deal",
+                  "ext": {
+                    "dspid": 6,
+                    "deal_channel": 1
+                  }
+                }
+              ]
+            }
+          ],
+          "bidid": "5778926625248726496",
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "7706636740145184841",
+            "impid": "test-imp-id",
+            "price": 0.5,
+            "adid": "29681110",
+            "adm": "some-test-ad",
+            "adomain": [
+              "pubmatic.com"
+            ],
+            "crid": "29681110",
+            "w": 300,
+            "h": 250,
+            "dealid": "test deal",
+            "ext": {
+              "dspid": 6,
+              "deal_channel": 1
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/pubmatic/pubmatictest/supplemental/noAdSlot.json b/adapters/pubmatic/pubmatictest/supplemental/noAdSlot.json
index c66a7189..a277ef53 100644
--- a/adapters/pubmatic/pubmatictest/supplemental/noAdSlot.json
+++ b/adapters/pubmatic/pubmatictest/supplemental/noAdSlot.json
@@ -55,7 +55,7 @@
                         "h": 250
                     },
                     "ext": {
-                        "pmZoneID": "Zone1,Zone2",
+                        "pmZoneId": "Zone1,Zone2",
                         "preference": "sports,movies"
                     }
                 }],
diff --git a/adapters/pubmatic/pubmatictest/supplemental/pmZoneIDInKeywords.json b/adapters/pubmatic/pubmatictest/supplemental/pmZoneIDInKeywords.json
new file mode 100644
index 00000000..c9c335e8
--- /dev/null
+++ b/adapters/pubmatic/pubmatictest/supplemental/pmZoneIDInKeywords.json
@@ -0,0 +1,161 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "adSlot": "AdTag_Div1@300x250",
+            "publisherId": "  999  ",
+            "dctr": "key1=V1,V2,V3|key2=v1|key3=v3,v5",
+            "keywords": [
+              {
+                "key": "pmZoneID",
+                "value": [
+                  "Zone1",
+                  "Zone2"
+                ]
+              },
+              {
+                "key": "preference",
+                "value": [
+                  "sports",
+                  "movies"
+                ]
+              }
+            ],
+            "wrapper": {
+              "version": 1,
+              "profile": 5123
+            }
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
+    },
+    "site": {
+      "id": "siteID",
+      "publisher": {
+        "id": "1234"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://hbopenbid.pubmatic.com/translator?source=prebid-server",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "tagid": "AdTag_Div1",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ],
+                "h": 250,
+                "w": 300
+              },
+              "ext": {
+                "key_val": "key1=V1,V2,V3|key2=v1|key3=v3,v5",
+                "pmZoneId": "Zone1,Zone2",
+                "preference": "sports,movies"
+              }
+            }
+          ],
+          "device": {
+            "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
+          },
+          "site": {
+            "id": "siteID",
+            "publisher": {
+              "id": "999"
+            }
+          },
+          "ext": {
+            "wrapper": {
+              "profile": 5123,
+              "version": 1
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "958",
+              "bid": [
+                {
+                  "id": "7706636740145184841",
+                  "impid": "test-imp-id",
+                  "price": 0.500000,
+                  "adid": "29681110",
+                  "adm": "some-test-ad",
+                  "adomain": [
+                    "pubmatic.com"
+                  ],
+                  "crid": "29681110",
+                  "h": 250,
+                  "w": 300,
+                  "dealid": "test deal",
+                  "ext": {
+                    "dspid": 6,
+                    "deal_channel": 1
+                  }
+                }
+              ]
+            }
+          ],
+          "bidid": "5778926625248726496",
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "7706636740145184841",
+            "impid": "test-imp-id",
+            "price": 0.5,
+            "adid": "29681110",
+            "adm": "some-test-ad",
+            "adomain": [
+              "pubmatic.com"
+            ],
+            "crid": "29681110",
+            "w": 300,
+            "h": 250,
+            "dealid": "test deal",
+            "ext": {
+              "dspid": 6,
+              "deal_channel": 1
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/pubmatic/pubmatictest/supplemental/trimPublisherID.json b/adapters/pubmatic/pubmatictest/supplemental/trimPublisherID.json
index 6a344ee0..d207542a 100644
--- a/adapters/pubmatic/pubmatictest/supplemental/trimPublisherID.json
+++ b/adapters/pubmatic/pubmatictest/supplemental/trimPublisherID.json
@@ -71,7 +71,7 @@
                 "w": 300
               },
               "ext": {
-                "pmZoneID": "Zone1,Zone2",
+                "pmZoneId": "Zone1,Zone2",
                 "preference": "sports,movies"
               }
             }
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index c64b3935..a03c4786 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -1800,6 +1800,14 @@ func (big *mockBidIDGenerator) New() (string, error) {
 
 }
 
+type fakeRandomDeduplicateBidBooleanGenerator struct {
+	returnValue bool
+}
+
+func (m *fakeRandomDeduplicateBidBooleanGenerator) Generate() bool {
+	return m.returnValue
+}
+
 func newExtRequest() openrtb_ext.ExtRequest {
 	priceGran := openrtb_ext.PriceGranularity{
 		Precision: 2,
@@ -1899,7 +1907,7 @@ func TestCategoryMapping(t *testing.T) {
 
 	adapterBids[bidderName1] = &seatBid
 
-	bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData)
+	bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{})
 
 	assert.Equal(t, nil, err, "Category mapping error should be empty")
 	assert.Equal(t, 1, len(rejections), "There should be 1 bid rejection message")
@@ -1954,7 +1962,7 @@ func TestCategoryMappingNoIncludeBrandCategory(t *testing.T) {
 
 	adapterBids[bidderName1] = &seatBid
 
-	bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData)
+	bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{})
 
 	assert.Equal(t, nil, err, "Category mapping error should be empty")
 	assert.Empty(t, rejections, "There should be no bid rejection messages")
@@ -2006,7 +2014,7 @@ func TestCategoryMappingTranslateCategoriesNil(t *testing.T) {
 
 	adapterBids[bidderName1] = &seatBid
 
-	bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData)
+	bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{})
 
 	assert.Equal(t, nil, err, "Category mapping error should be empty")
 	assert.Equal(t, 1, len(rejections), "There should be 1 bid rejection message")
@@ -2088,7 +2096,7 @@ func TestCategoryMappingTranslateCategoriesFalse(t *testing.T) {
 
 	adapterBids[bidderName1] = &seatBid
 
-	bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData)
+	bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{})
 
 	assert.Equal(t, nil, err, "Category mapping error should be empty")
 	assert.Empty(t, rejections, "There should be no bid rejection messages")
@@ -2158,7 +2166,7 @@ func TestCategoryDedupe(t *testing.T) {
 
 		adapterBids[bidderName1] = &seatBid
 
-		bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData)
+		bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{})
 
 		assert.Equal(t, nil, err, "Category mapping error should be empty")
 		assert.Equal(t, 3, len(rejections), "There should be 2 bid rejection messages")
@@ -2238,7 +2246,7 @@ func TestNoCategoryDedupe(t *testing.T) {
 
 		adapterBids[bidderName1] = &seatBid
 
-		bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData)
+		bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{})
 
 		assert.Equal(t, nil, err, "Category mapping error should be empty")
 		assert.Equal(t, 2, len(rejections), "There should be 2 bid rejection messages")
@@ -2303,7 +2311,7 @@ func TestCategoryMappingBidderName(t *testing.T) {
 	adapterBids[bidderName1] = &seatBid1
 	adapterBids[bidderName2] = &seatBid2
 
-	bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData)
+	bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{})
 
 	assert.NoError(t, err, "Category mapping error should be empty")
 	assert.Empty(t, rejections, "There should be 0 bid rejection messages")
@@ -2357,7 +2365,7 @@ func TestCategoryMappingBidderNameNoCategories(t *testing.T) {
 	adapterBids[bidderName1] = &seatBid1
 	adapterBids[bidderName2] = &seatBid2
 
-	bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData)
+	bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{})
 
 	assert.NoError(t, err, "Category mapping error should be empty")
 	assert.Empty(t, rejections, "There should be 0 bid rejection messages")
@@ -2458,7 +2466,7 @@ func TestBidRejectionErrors(t *testing.T) {
 
 		adapterBids[bidderName] = &seatBid
 
-		bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &test.reqExt, adapterBids, categoriesFetcher, targData)
+		bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &test.reqExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{})
 
 		if len(test.expectedCatDur) > 0 {
 			// Bid deduplication case
@@ -2521,7 +2529,7 @@ func TestCategoryMappingTwoBiddersOneBidEachNoCategorySamePrice(t *testing.T) {
 		adapterBids[bidderNameApn1] = &seatBidApn1
 		adapterBids[bidderNameApn2] = &seatBidApn2
 
-		bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData)
+		bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{})
 
 		assert.NoError(t, err, "Category mapping error should be empty")
 		assert.Len(t, rejections, 1, "There should be 1 bid rejection message")
@@ -2540,9 +2548,154 @@ func TestCategoryMappingTwoBiddersOneBidEachNoCategorySamePrice(t *testing.T) {
 		} else {
 			assert.Nil(t, seatBidApn1.bids, "Appnexus_1 seat bid should not have any bids back")
 			assert.Len(t, seatBidApn2.bids, 1, "Appnexus_2 seat bid should have only one back")
+		}
+	}
+}
+
+func TestCategoryMappingTwoBiddersManyBidsEachNoCategorySamePrice(t *testing.T) {
+	// This test covers a very rare de-duplication case where bid needs to be removed from already processed bidder
+	// This happens when current processing bidder has a bid that has same de-duplication key as a bid from already processed bidder
+	// and already processed bid was selected to be removed
+
+	//In this test case bids bid_idApn1_1 and bid_idApn1_2 will be removed due to hardcoded "fakeRandomDeduplicateBidBooleanGenerator{true}"
+
+	// Also there are should be more than one bids in bidder to test how we remove single element from bids array.
+	// In case there is just one bid to remove - we remove the entire bidder.
 
+	categoriesFetcher, error := newCategoryFetcher("./test/category-mapping")
+	if error != nil {
+		t.Errorf("Failed to create a category Fetcher: %v", error)
+	}
+
+	requestExt := newExtRequestTranslateCategories(nil)
+
+	targData := &targetData{
+		priceGranularity: requestExt.Prebid.Targeting.PriceGranularity,
+		includeWinners:   true,
+	}
+
+	requestExt.Prebid.Targeting.DurationRangeSec = []int{30}
+	requestExt.Prebid.Targeting.IncludeBrandCategory.WithCategory = false
+
+	cats1 := []string{"IAB1-3"}
+	cats2 := []string{"IAB1-4"}
+
+	bidApn1_1 := openrtb2.Bid{ID: "bid_idApn1_1", ImpID: "imp_idApn1_1", Price: 10.0000, Cat: cats1, W: 1, H: 1}
+	bidApn1_2 := openrtb2.Bid{ID: "bid_idApn1_2", ImpID: "imp_idApn1_2", Price: 20.0000, Cat: cats1, W: 1, H: 1}
+
+	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 := pbsOrtbBid{&bidApn1_1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_Apn1_2 := pbsOrtbBid{&bidApn1_2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+
+	bid1_Apn2_1 := pbsOrtbBid{&bidApn2_1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_Apn2_2 := pbsOrtbBid{&bidApn2_2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+
+	innerBidsApn1 := []*pbsOrtbBid{
+		&bid1_Apn1_1,
+		&bid1_Apn1_2,
+	}
+
+	innerBidsApn2 := []*pbsOrtbBid{
+		&bid1_Apn2_1,
+		&bid1_Apn2_2,
+	}
+
+	adapterBids := make(map[openrtb_ext.BidderName]*pbsOrtbSeatBid)
+
+	seatBidApn1 := pbsOrtbSeatBid{bids: innerBidsApn1, currency: "USD"}
+	bidderNameApn1 := openrtb_ext.BidderName("appnexus1")
+
+	seatBidApn2 := pbsOrtbSeatBid{bids: innerBidsApn2, currency: "USD"}
+	bidderNameApn2 := openrtb_ext.BidderName("appnexus2")
+
+	adapterBids[bidderNameApn1] = &seatBidApn1
+	adapterBids[bidderNameApn2] = &seatBidApn2
+
+	_, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &fakeRandomDeduplicateBidBooleanGenerator{true})
+
+	assert.NoError(t, err, "Category mapping error should be empty")
+
+	//Total number of bids from all bidders in this case should be 2
+	bidsFromFirstBidder := adapterBids[bidderNameApn1]
+	bidsFromSecondBidder := adapterBids[bidderNameApn2]
+
+	totalNumberOfbids := 0
+
+	if bidsFromFirstBidder.bids != nil {
+		totalNumberOfbids += len(bidsFromFirstBidder.bids)
+	}
+
+	if bidsFromSecondBidder.bids != nil {
+		totalNumberOfbids += len(bidsFromSecondBidder.bids)
+	}
+
+	assert.Equal(t, 2, totalNumberOfbids, "2 bids total should be returned")
+
+	assert.Len(t, adapterBids[bidderNameApn1].bids, 0)
+	assert.Len(t, adapterBids[bidderNameApn2].bids, 2)
+
+	assert.Equal(t, "bid_idApn2_1", adapterBids[bidderNameApn2].bids[0].bid.ID, "Incorrect expected bid 1 id")
+	assert.Equal(t, "bid_idApn2_2", adapterBids[bidderNameApn2].bids[1].bid.ID, "Incorrect expected bid 2 id")
+
+	assert.Len(t, rejections, 2, "2 bids should be de-duplicated")
+	assert.Equal(t, "bid rejected [bid ID: bid_idApn1_1] reason: Bid was deduplicated", rejections[0], "Incorrect rejected bid 1")
+	assert.Equal(t, "bid rejected [bid ID: bid_idApn1_2] reason: Bid was deduplicated", rejections[1], "Incorrect rejected bid 2")
+
+}
+
+func TestRemoveBidById(t *testing.T) {
+	cats1 := []string{"IAB1-3"}
+
+	bidApn1_1 := openrtb2.Bid{ID: "bid_idApn1_1", ImpID: "imp_idApn1_1", Price: 10.0000, Cat: cats1, W: 1, H: 1}
+	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 := pbsOrtbBid{&bidApn1_1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_Apn1_2 := pbsOrtbBid{&bidApn1_2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_Apn1_3 := pbsOrtbBid{&bidApn1_3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+
+	type aTest struct {
+		desc      string
+		inBidName string
+		outBids   []*pbsOrtbBid
+	}
+	testCases := []aTest{
+		{
+			desc:      "remove element from the middle",
+			inBidName: "bid_idApn1_2",
+			outBids:   []*pbsOrtbBid{&bid1_Apn1_1, &bid1_Apn1_3},
+		},
+		{
+			desc:      "remove element from the end",
+			inBidName: "bid_idApn1_3",
+			outBids:   []*pbsOrtbBid{&bid1_Apn1_1, &bid1_Apn1_2},
+		},
+		{
+			desc:      "remove element from the beginning",
+			inBidName: "bid_idApn1_1",
+			outBids:   []*pbsOrtbBid{&bid1_Apn1_2, &bid1_Apn1_3},
+		},
+		{
+			desc:      "remove element that doesn't exist",
+			inBidName: "bid_idApn",
+			outBids:   []*pbsOrtbBid{&bid1_Apn1_1, &bid1_Apn1_2, &bid1_Apn1_3},
+		},
+	}
+	for _, test := range testCases {
+
+		innerBidsApn1 := []*pbsOrtbBid{
+			&bid1_Apn1_1,
+			&bid1_Apn1_2,
+			&bid1_Apn1_3,
 		}
 
+		seatBidApn1 := &pbsOrtbSeatBid{bids: innerBidsApn1, currency: "USD"}
+
+		removeBidById(seatBidApn1, test.inBidName)
+		assert.Len(t, seatBidApn1.bids, len(test.outBids), test.desc)
+		assert.ElementsMatch(t, seatBidApn1.bids, test.outBids, "Incorrect bids in response")
 	}
 
 }
diff --git a/util/jsonutil/jsonutil_test.go b/util/jsonutil/jsonutil_test.go
new file mode 100644
index 00000000..0b6ec34c
--- /dev/null
+++ b/util/jsonutil/jsonutil_test.go
@@ -0,0 +1,122 @@
+package jsonutil
+
+import (
+	"github.com/stretchr/testify/assert"
+	"strings"
+	"testing"
+)
+
+func TestDropElement(t *testing.T) {
+
+	tests := []struct {
+		description     string
+		input           []byte
+		elementToRemove string
+		output          []byte
+		errorExpected   bool
+		errorContains   string
+	}{
+		{
+			description:     "Drop Single Element After Another Element",
+			input:           []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"test": 1,"consented_providers": [1608,765,492]}}`),
+			elementToRemove: "consented_providers",
+			output:          []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"test": 1}}`),
+			errorExpected:   false,
+			errorContains:   "",
+		},
+		{
+			description:     "Drop Single Element Before Another Element",
+			input:           []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"consented_providers": [1608,765,492],"test": 1}}`),
+			elementToRemove: "consented_providers",
+			output:          []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"test": 1}}`),
+			errorExpected:   false,
+			errorContains:   "",
+		},
+		{
+			description:     "Drop Single Element",
+			input:           []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"consented_providers": [1545,2563,1411]}}`),
+			elementToRemove: "consented_providers",
+			output:          []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {}}`),
+			errorExpected:   false,
+			errorContains:   "",
+		},
+		{
+			description:     "Drop Single Element string",
+			input:           []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"consented_providers": "test"}}`),
+			elementToRemove: "consented_providers",
+			output:          []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {}}`),
+			errorExpected:   false,
+			errorContains:   "",
+		},
+		{
+			description:     "Drop Parent Element Between Two Elements",
+			input:           []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"consented_providers": [1608,765,492], "test": 1},"test": 123}`),
+			elementToRemove: "consented_providers_settings",
+			output:          []byte(`{"consent": "TESTCONSENT","test": 123}`),
+			errorExpected:   false,
+			errorContains:   "",
+		},
+		{
+			description:     "Drop Parent Element Before Element",
+			input:           []byte(`{"consented_providers_settings": {"consented_providers": [1608,765,492], "test": 1},"test": 123}`),
+			elementToRemove: "consented_providers_settings",
+			output:          []byte(`{"test": 123}`),
+			errorExpected:   false,
+			errorContains:   "",
+		},
+		{
+			description:     "Drop Parent Element After Element",
+			input:           []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"consented_providers": [1608,765,492], "test": 1}}`),
+			elementToRemove: "consented_providers_settings",
+			output:          []byte(`{"consent": "TESTCONSENT"}`),
+			errorExpected:   false,
+			errorContains:   "",
+		},
+		{
+			description:     "Drop Parent Element Only",
+			input:           []byte(`{"consented_providers_settings": {"consented_providers": [1608,765,492], "test": 1}}`),
+			elementToRemove: "consented_providers_settings",
+			output:          []byte(`{}`),
+			errorExpected:   false,
+			errorContains:   "",
+		},
+		{
+			description:     "Drop Element That Doesn't Exist",
+			input:           []byte(`{"consented_providers_settings": {"consented_providers": [1608,765,492], "test": 1}}`),
+			elementToRemove: "test2",
+			output:          []byte(`{"consented_providers_settings": {"consented_providers": [1608,765,492], "test": 1}}`),
+			errorExpected:   false,
+			errorContains:   "",
+		},
+		//Errors
+		{
+			description:     "Error Decode",
+			input:           []byte(`{"consented_providers_settings": {"consented_providers": ["123",1,,1365,5678,1545,2563,1411], "test": 1}}`),
+			elementToRemove: "consented_providers",
+			output:          []byte(``),
+			errorExpected:   true,
+			errorContains:   "looking for beginning of value",
+		},
+		{
+			description:     "Error Malformed",
+			input:           []byte(`{consented_providers_settings: {"consented_providers": [1365,5678,1545,2563,1411], "test": 1}}`),
+			elementToRemove: "consented_providers",
+			output:          []byte(``),
+			errorExpected:   true,
+			errorContains:   "invalid character",
+		},
+	}
+
+	for _, tt := range tests {
+		res, err := DropElement(tt.input, tt.elementToRemove)
+
+		if tt.errorExpected {
+			assert.Error(t, err, "Error should not be nil")
+			assert.True(t, strings.Contains(err.Error(), tt.errorContains))
+		} else {
+			assert.NoError(t, err, "Error should be nil")
+			assert.Equal(t, tt.output, res, "Result is incorrect")
+		}
+
+	}
+}