Repository: prebid/prebid-server
Pull request #1913 (2021-07-01 16:10:58)

TheMediaGrid: Add params.keywords processing
Update s2s TheMediaGrid adapter to read imp.ext.bidder.keywords and copy it in ext.keywords 

SyntaxNode: Looks like a recent push introduced new validation errors. We'll re-review once those are addressed.
TheMediaGrid: @guscarreon @SyntaxNode @hhhjort any chance you could take a look at the most recent changes that we've made as per Scott's comments? Any insight into why the auto tests are failing would be very useful as well.
SyntaxNode: > @guscarreon @SyntaxNode @hhhjort any chance you could take a look at the most recent changes that we've made as per Scott's comments? Any insight into why the auto tests are failing would be very useful as well.

I'm looking into it. I didn't see this locally either. I suspect it's a case where the segments array order is indeterminate, but will confirm.
SyntaxNode: > > @guscarreon @SyntaxNode @hhhjort any chance you could take a look at the most recent changes that we've made as per Scott's comments? Any insight into why the auto tests are failing would be very useful as well.
> 
> I'm looking into it. I didn't see this locally either. I suspect it's a case where the segments array order is indeterminate, but will confirm.

Found it.

The issue is caused by the indeterminate ordering of the segments when constructed from an array of strings, referred to as the "compatible alternate segment format" in the code. 

The ordering relies on the iteration order of keys within a map, which is purposefully inconsistent in Go. I don't believe the ordering matters to your bidding server, but it does matter to the json tests since arrays are always tested as order dependent. I'm not sure how easily or safely we can modify the test layer, but it easy to sort the keys to ensure deterministic order.

Change:
```
// compose compatible alternate segment format
for potentialSegmentName, potentialSegmentValues := range publisherItem {
	// values must be an array
	if valuesSlice, ok := potentialSegmentValues.([]interface{}); ok {
		for _, value := range valuesSlice {
			if valueAsString, ok := value.(string); ok {
				segments = append(segments, KeywordSegment{Name: potentialSegmentName, Value: valueAsString})
			}
		}
	}
}
```

To:
```
// ensure consistent ordering for publisher item map
publisherItemKeys := make([]string, 0, len(publisherItem))
for v := range publisherItem {
	publisherItemKeys = append(publisherItemKeys, v)
}
sort.Strings(publisherItemKeys)

// compose compatible alternate segment format
for _, potentialSegmentName := range publisherItemKeys {
	potentialSegmentValues := publisherItem[potentialSegmentName]
	// values must be an array
	if valuesSlice, ok := potentialSegmentValues.([]interface{}); ok {
		for _, value := range valuesSlice {
			if valueAsString, ok := value.(string); ok {
				segments = append(segments, KeywordSegment{Name: potentialSegmentName, Value: valueAsString})
			}
		}
	}
}
```

This will then consistently fail until the mixed test is updated to the following order:
```
"segments": [
    { "name": "segName1", "value": "segVal1" },
    { "name": "segName2", "value": "segVal2" },
    { "name": "bottom", "value": "bottomKey1" },
    { "name": "bottom", "value": "bottomKey2" },
    { "name": "segments", "value": "stringKey" }
]
```

After which the tests will consistently passes. I don't believe this is a new issue, but was missed earlier just by chance.

TheMediaGrid: @SyntaxNode @guscarreon @hhhjort @bsardo can anybody please review this PR? it's been a while

Patch:
diff --git a/adapters/bidder.go b/adapters/bidder.go
index dbe56f8e..e6022777 100644
--- a/adapters/bidder.go
+++ b/adapters/bidder.go
@@ -89,11 +89,13 @@ func NewBidderResponse() *BidderResponse {
 // openrtb_ext.ExtBidPrebid.
 //
 // TypedBid.Bid.Ext will become "response.seatbid[i].bid.ext.bidder" in the final OpenRTB response.
+// TypedBid.BidMeta will become "response.seatbid[i].bid.ext.prebid.meta" in the final OpenRTB response.
 // TypedBid.BidType will become "response.seatbid[i].bid.ext.prebid.type" in the final OpenRTB response.
 // TypedBid.BidVideo will become "response.seatbid[i].bid.ext.prebid.video" in the final OpenRTB response.
 // TypedBid.DealPriority is optionally provided by adapters and used internally by the exchange to support deal targeted campaigns.
 type TypedBid struct {
 	Bid          *openrtb2.Bid
+	BidMeta      *openrtb_ext.ExtBidPrebidMeta
 	BidType      openrtb_ext.BidType
 	BidVideo     *openrtb_ext.ExtBidPrebidVideo
 	DealPriority int
diff --git a/adapters/grid/grid.go b/adapters/grid/grid.go
index bc819fc0..850b21c5 100644
--- a/adapters/grid/grid.go
+++ b/adapters/grid/grid.go
@@ -4,18 +4,33 @@ import (
 	"encoding/json"
 	"fmt"
 	"net/http"
+	"sort"
+	"strings"
 
 	"github.com/mxmCherry/openrtb/v15/openrtb2"
 	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/prebid/prebid-server/util/maputil"
 )
 
 type GridAdapter struct {
 	endpoint string
 }
 
+type GridBidExt struct {
+	Bidder ExtBidder `json:"bidder"`
+}
+
+type ExtBidder struct {
+	Grid ExtBidderGrid `json:"grid"`
+}
+
+type ExtBidderGrid struct {
+	DemandSource string `json:"demandSource"`
+}
+
 type ExtImpDataAdServer struct {
 	Name   string `json:"name"`
 	AdSlot string `json:"adslot"`
@@ -33,6 +48,187 @@ type ExtImp struct {
 	Gpid   string                    `json:"gpid,omitempty"`
 }
 
+type KeywordSegment struct {
+	Name  string `json:"name"`
+	Value string `json:"value"`
+}
+
+type KeywordsPublisherItem struct {
+	Name     string           `json:"name"`
+	Segments []KeywordSegment `json:"segments"`
+}
+
+type KeywordsPublisher map[string][]KeywordsPublisherItem
+
+type Keywords map[string]KeywordsPublisher
+
+// buildConsolidatedKeywordsReqExt builds a new request.ext json incorporating request.site.keywords, request.user.keywords,
+// and request.imp[0].ext.keywords, and request.ext.keywords. Invalid keywords in request.imp[0].ext.keywords are not incorporated.
+// Invalid keywords in request.ext.keywords.site and request.ext.keywords.user are dropped.
+func buildConsolidatedKeywordsReqExt(openRTBUser, openRTBSite string, firstImpExt, requestExt json.RawMessage) (json.RawMessage, error) {
+	// unmarshal ext to object map
+	requestExtMap := parseExtToMap(requestExt)
+	firstImpExtMap := parseExtToMap(firstImpExt)
+	// extract `keywords` field
+	requestExtKeywordsMap := extractKeywordsMap(requestExtMap)
+	firstImpExtKeywordsMap := extractBidderKeywordsMap(firstImpExtMap)
+	// parse + merge keywords
+	keywords := parseKeywordsFromMap(requestExtKeywordsMap)                // request.ext.keywords
+	mergeKeywords(keywords, parseKeywordsFromMap(firstImpExtKeywordsMap))  // request.imp[0].ext.bidder.keywords
+	mergeKeywords(keywords, parseKeywordsFromOpenRTB(openRTBUser, "user")) // request.user.keywords
+	mergeKeywords(keywords, parseKeywordsFromOpenRTB(openRTBSite, "site")) // request.site.keywords
+
+	// overlay site + user keywords
+	if site, exists := keywords["site"]; exists && len(site) > 0 {
+		requestExtKeywordsMap["site"] = site
+	} else {
+		delete(requestExtKeywordsMap, "site")
+	}
+	if user, exists := keywords["user"]; exists && len(user) > 0 {
+		requestExtKeywordsMap["user"] = user
+	} else {
+		delete(requestExtKeywordsMap, "user")
+	}
+	// reconcile keywords with request.ext
+	if len(requestExtKeywordsMap) > 0 {
+		requestExtMap["keywords"] = requestExtKeywordsMap
+	} else {
+		delete(requestExtMap, "keywords")
+	}
+	// marshal final result
+	if len(requestExtMap) > 0 {
+		return json.Marshal(requestExtMap)
+	}
+	return nil, nil
+}
+func parseExtToMap(ext json.RawMessage) map[string]interface{} {
+	var root map[string]interface{}
+	if err := json.Unmarshal(ext, &root); err != nil {
+		return make(map[string]interface{})
+	}
+	return root
+}
+func extractKeywordsMap(ext map[string]interface{}) map[string]interface{} {
+	if keywords, exists := maputil.ReadEmbeddedMap(ext, "keywords"); exists {
+		return keywords
+	}
+	return make(map[string]interface{})
+}
+func extractBidderKeywordsMap(ext map[string]interface{}) map[string]interface{} {
+	if bidder, exists := maputil.ReadEmbeddedMap(ext, "bidder"); exists {
+		return extractKeywordsMap(bidder)
+	}
+	return make(map[string]interface{})
+}
+func parseKeywordsFromMap(extKeywords map[string]interface{}) Keywords {
+	keywords := make(Keywords)
+	for k, v := range extKeywords {
+		// keywords may only be provided in the site and user sections
+		if k != "site" && k != "user" {
+			continue
+		}
+		// the site or user sections must be an object
+		if section, ok := v.(map[string]interface{}); ok {
+			keywords[k] = parseKeywordsFromSection(section)
+		}
+	}
+	return keywords
+}
+func parseKeywordsFromSection(section map[string]interface{}) KeywordsPublisher {
+	keywordsPublishers := make(KeywordsPublisher)
+	for publisherKey, publisherValue := range section {
+		// publisher value must be a slice
+		publisherValueSlice, ok := publisherValue.([]interface{})
+		if !ok {
+			continue
+		}
+		for _, publisherValueItem := range publisherValueSlice {
+			// item must be an object
+			publisherItem, ok := publisherValueItem.(map[string]interface{})
+			if !ok {
+				continue
+			}
+			// publisher item must have a name
+			publisherName, ok := maputil.ReadEmbeddedString(publisherItem, "name")
+			if !ok {
+				continue
+			}
+			var segments []KeywordSegment
+			// extract valid segments
+			if segmentsSlice, exists := maputil.ReadEmbeddedSlice(publisherItem, "segments"); exists {
+				for _, segment := range segmentsSlice {
+					if segmentMap, ok := segment.(map[string]interface{}); ok {
+						name, hasName := maputil.ReadEmbeddedString(segmentMap, "name")
+						value, hasValue := maputil.ReadEmbeddedString(segmentMap, "value")
+						if hasName && hasValue {
+							segments = append(segments, KeywordSegment{Name: name, Value: value})
+						}
+					}
+				}
+			}
+			// ensure consistent ordering for publisher item map
+			publisherItemKeys := make([]string, 0, len(publisherItem))
+			for v := range publisherItem {
+				publisherItemKeys = append(publisherItemKeys, v)
+			}
+			sort.Strings(publisherItemKeys)
+			// compose compatible alternate segment format
+			for _, potentialSegmentName := range publisherItemKeys {
+				potentialSegmentValues := publisherItem[potentialSegmentName]
+				// values must be an array
+				if valuesSlice, ok := potentialSegmentValues.([]interface{}); ok {
+					for _, value := range valuesSlice {
+						if valueAsString, ok := value.(string); ok {
+							segments = append(segments, KeywordSegment{Name: potentialSegmentName, Value: valueAsString})
+						}
+					}
+				}
+			}
+			if len(segments) > 0 {
+				keywordsPublishers[publisherKey] = append(keywordsPublishers[publisherKey], KeywordsPublisherItem{Name: publisherName, Segments: segments})
+			}
+		}
+	}
+	return keywordsPublishers
+}
+func parseKeywordsFromOpenRTB(keywords, section string) Keywords {
+	keywordsSplit := strings.Split(keywords, ",")
+	segments := make([]KeywordSegment, 0, len(keywordsSplit))
+	for _, v := range keywordsSplit {
+		if v != "" {
+			segments = append(segments, KeywordSegment{Name: "keywords", Value: v})
+		}
+	}
+	if len(segments) > 0 {
+		return map[string]KeywordsPublisher{section: map[string][]KeywordsPublisherItem{"ortb2": {{Name: "keywords", Segments: segments}}}}
+	}
+	return make(Keywords)
+}
+func mergeKeywords(a, b Keywords) {
+	for key, values := range b {
+		if _, sectionExists := a[key]; !sectionExists {
+			a[key] = KeywordsPublisher{}
+		}
+		for publisherKey, publisherValues := range values {
+			a[key][publisherKey] = append(publisherValues, a[key][publisherKey]...)
+		}
+	}
+}
+
+func setImpExtKeywords(request *openrtb2.BidRequest) error {
+	userKeywords := ""
+	if request.User != nil {
+		userKeywords = request.User.Keywords
+	}
+	siteKeywords := ""
+	if request.Site != nil {
+		siteKeywords = request.Site.Keywords
+	}
+	var err error
+	request.Ext, err = buildConsolidatedKeywordsReqExt(userKeywords, siteKeywords, request.Imp[0].Ext, request.Ext)
+	return err
+}
+
 func processImp(imp *openrtb2.Imp) error {
 	// get the grid extension
 	var ext adapters.ExtImpBidder
@@ -73,12 +269,10 @@ func setImpExtData(imp openrtb2.Imp) openrtb2.Imp {
 func (a *GridAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
 	var errors = make([]error, 0)
 
-	// copy the request, because we are going to mutate it
-	requestCopy := *request
 	// this will contain all the valid impressions
 	var validImps []openrtb2.Imp
 	// pre-process the imps
-	for _, imp := range requestCopy.Imp {
+	for _, imp := range request.Imp {
 		if err := processImp(&imp); err == nil {
 			validImps = append(validImps, setImpExtData(imp))
 		} else {
@@ -92,9 +286,15 @@ func (a *GridAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapte
 		errors = append(errors, err)
 		return nil, errors
 	}
-	requestCopy.Imp = validImps
 
-	reqJSON, err := json.Marshal(requestCopy)
+	if err := setImpExtKeywords(request); err != nil {
+		errors = append(errors, err)
+		return nil, errors
+	}
+
+	request.Imp = validImps
+
+	reqJSON, err := json.Marshal(request)
 	if err != nil {
 		errors = append(errors, err)
 		return nil, errors
@@ -138,6 +338,7 @@ func (a *GridAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalReq
 
 	for _, sb := range bidResp.SeatBid {
 		for i := range sb.Bid {
+			bidMeta, err := getBidMeta(sb.Bid[i].Ext)
 			bidType, err := getMediaTypeForImp(sb.Bid[i].ImpID, internalRequest.Imp)
 			if err != nil {
 				return nil, []error{err}
@@ -146,6 +347,7 @@ func (a *GridAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalReq
 			bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
 				Bid:     &sb.Bid[i],
 				BidType: bidType,
+				BidMeta: bidMeta,
 			})
 		}
 	}
@@ -161,6 +363,21 @@ func Builder(bidderName openrtb_ext.BidderName, config config.Adapter) (adapters
 	return bidder, nil
 }
 
+func getBidMeta(ext json.RawMessage) (*openrtb_ext.ExtBidPrebidMeta, error) {
+	var bidExt GridBidExt
+
+	if err := json.Unmarshal(ext, &bidExt); err != nil {
+		return nil, err
+	}
+	var bidMeta *openrtb_ext.ExtBidPrebidMeta
+	if bidExt.Bidder.Grid.DemandSource != "" {
+		bidMeta = &openrtb_ext.ExtBidPrebidMeta{
+			NetworkName: bidExt.Bidder.Grid.DemandSource,
+		}
+	}
+	return bidMeta, nil
+}
+
 func getMediaTypeForImp(impID string, imps []openrtb2.Imp) (openrtb_ext.BidType, error) {
 	for _, imp := range imps {
 		if imp.ID == impID {
diff --git a/adapters/yieldmo/yieldmo.go b/adapters/yieldmo/yieldmo.go
index 7d7a8f22..eaaa489d 100644
--- a/adapters/yieldmo/yieldmo.go
+++ b/adapters/yieldmo/yieldmo.go
@@ -16,8 +16,18 @@ type YieldmoAdapter struct {
 	endpoint string
 }
 
+type ExtImpBidderYieldmo struct {
+	adapters.ExtImpBidder
+	Data *ExtData `json:"data,omitempty"`
+}
+
+type ExtData struct {
+	PbAdslot string `json:"pbadslot"`
+}
+
 type Ext struct {
 	PlacementId string `json:"placement_id"`
+	Gpid        string `json:"gpid,omitempty"`
 }
 
 func (a *YieldmoAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
@@ -62,7 +72,7 @@ func (a *YieldmoAdapter) makeRequest(request *openrtb2.BidRequest) (*adapters.Re
 func preprocess(request *openrtb2.BidRequest) error {
 	for i := 0; i < len(request.Imp); i++ {
 		var imp = request.Imp[i]
-		var bidderExt adapters.ExtImpBidder
+		var bidderExt ExtImpBidderYieldmo
 
 		if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
 			return &errortypes.BadInput{
@@ -81,6 +91,12 @@ func preprocess(request *openrtb2.BidRequest) error {
 		var impExt Ext
 		impExt.PlacementId = yieldmoExt.PlacementId
 
+		if bidderExt.Data != nil {
+			if bidderExt.Data.PbAdslot != "" {
+				impExt.Gpid = bidderExt.Data.PbAdslot
+			}
+		}
+
 		impExtJSON, err := json.Marshal(impExt)
 		if err != nil {
 			return &errortypes.BadInput{
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index a6b3479a..bcdf133c 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -135,7 +135,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 		deps.analytics.LogAuctionObject(&ao)
 	}()
 
-	req, errL := deps.parseRequest(r)
+	req, impExtInfoMap, errL := deps.parseRequest(r)
 
 	if errortypes.ContainsFatalError(errL) && writeError(errL, w, &labels) {
 		return
@@ -192,6 +192,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 		LegacyLabels:               labels,
 		Warnings:                   warnings,
 		GlobalPrivacyControlHeader: secGPC,
+		ImpExtInfoMap:              impExtInfoMap,
 	}
 
 	response, err := deps.ex.HoldAuction(ctx, auctionRequest, nil)
@@ -234,7 +235,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 // possible, it will return errors with messages that suggest improvements.
 //
 // If the errors list has at least one element, then no guarantees are made about the returned request.
-func (deps *endpointDeps) parseRequest(httpRequest *http.Request) (req *openrtb_ext.RequestWrapper, errs []error) {
+func (deps *endpointDeps) parseRequest(httpRequest *http.Request) (req *openrtb_ext.RequestWrapper, impExtInfoMap map[string]exchange.ImpExtInfo, errs []error) {
 	req = &openrtb_ext.RequestWrapper{}
 	req.BidRequest = &openrtb2.BidRequest{}
 	errs = nil
@@ -262,7 +263,7 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request) (req *openrtb_
 	defer cancel()
 
 	// Fetch the Stored Request data and merge it into the HTTP request.
-	if requestJson, errs = deps.processStoredRequests(ctx, requestJson); len(errs) > 0 {
+	if requestJson, impExtInfoMap, errs = deps.processStoredRequests(ctx, requestJson); len(errs) > 0 {
 		return
 	}
 
@@ -1316,15 +1317,15 @@ func getJsonSyntaxError(testJSON []byte) (bool, string) {
 	return false, ""
 }
 
-func (deps *endpointDeps) processStoredRequests(ctx context.Context, requestJson []byte) ([]byte, []error) {
+func (deps *endpointDeps) processStoredRequests(ctx context.Context, requestJson []byte) ([]byte, map[string]exchange.ImpExtInfo, []error) {
 	// Parse the Stored Request IDs from the BidRequest and Imps.
 	storedBidRequestId, hasStoredBidRequest, err := getStoredRequestId(requestJson)
 	if err != nil {
-		return nil, []error{err}
+		return nil, nil, []error{err}
 	}
 	imps, impIds, idIndices, errs := parseImpInfo(requestJson)
 	if len(errs) > 0 {
-		return nil, errs
+		return nil, nil, errs
 	}
 
 	// Fetch the Stored Request data
@@ -1334,7 +1335,7 @@ func (deps *endpointDeps) processStoredRequests(ctx context.Context, requestJson
 	}
 	storedRequests, storedImps, errs := deps.storedReqFetcher.FetchRequests(ctx, storedReqIds, impIds)
 	if len(errs) != 0 {
-		return nil, errs
+		return nil, nil, errs
 	}
 
 	// Apply the Stored BidRequest, if it exists
@@ -1352,7 +1353,7 @@ func (deps *endpointDeps) processStoredRequests(ctx context.Context, requestJson
 					err = fmt.Errorf("ext.prebid.storedrequest.id refers to Stored Request %s which contains Invalid JSON: %s", storedBidRequestId, Err)
 				}
 			}
-			return nil, []error{err}
+			return nil, nil, []error{err}
 		}
 	}
 
@@ -1369,7 +1370,7 @@ func (deps *endpointDeps) processStoredRequests(ctx context.Context, requestJson
 					err = fmt.Errorf("Invalid JSON in Default Request Settings: %s", Err)
 				}
 			}
-			return nil, []error{err}
+			return nil, nil, []error{err}
 		}
 		resolvedRequest = aliasedRequest
 	}
@@ -1377,8 +1378,10 @@ func (deps *endpointDeps) processStoredRequests(ctx context.Context, requestJson
 	// Apply any Stored Imps, if they exist. Since the JSON Merge Patch overrides arrays,
 	// and Prebid Server defers to the HTTP Request to resolve conflicts, it's safe to
 	// assume that the request.imp data did not change when applying the Stored BidRequest.
+	impExtInfoMap := make(map[string]exchange.ImpExtInfo, len(impIds))
 	for i := 0; i < len(impIds); i++ {
 		resolvedImp, err := jsonpatch.MergePatch(storedImps[impIds[i]], imps[idIndices[i]])
+
 		if err != nil {
 			hasErr, Err := getJsonSyntaxError(imps[idIndices[i]])
 			if hasErr {
@@ -1389,22 +1392,38 @@ func (deps *endpointDeps) processStoredRequests(ctx context.Context, requestJson
 					err = fmt.Errorf("imp.ext.prebid.storedrequest.id %s: Stored Imp has Invalid JSON: %s", impIds[i], Err)
 				}
 			}
-			return nil, []error{err}
+			return nil, nil, []error{err}
 		}
 		imps[idIndices[i]] = resolvedImp
+
+		impId, err := jsonparser.GetString(resolvedImp, "id")
+		if err != nil {
+			return nil, nil, []error{err}
+		}
+		// This is substantially faster for reading values from a json blob than the Go json package,
+		// but keep in mind that each jsonparser.GetXXX call re-parses the entire json.
+		// Based on performance measurements, the tipping point of efficiency is around 4 calls.
+		// At that point, please consider switching to EachKey to use a single pass.
+		includeVideoAttributes, err := jsonparser.GetBoolean(resolvedImp, "ext", "prebid", "options", "echovideoattrs")
+		if err != nil && err != jsonparser.KeyPathNotFoundError {
+			return nil, nil, []error{err}
+		}
+		if storedImps[impIds[i]] != nil {
+			impExtInfoMap[impId] = exchange.ImpExtInfo{EchoVideoAttrs: includeVideoAttributes, StoredImp: storedImps[impIds[i]]}
+		}
 	}
 	if len(impIds) > 0 {
 		newImpJson, err := json.Marshal(imps)
 		if err != nil {
-			return nil, []error{err}
+			return nil, nil, []error{err}
 		}
 		resolvedRequest, err = jsonparser.Set(resolvedRequest, newImpJson, "imp")
 		if err != nil {
-			return nil, []error{err}
+			return nil, nil, []error{err}
 		}
 	}
 
-	return resolvedRequest, nil
+	return resolvedRequest, impExtInfoMap, nil
 }
 
 // parseImpInfo parses the request JSON and returns several things about the Imps
diff --git a/exchange/bidder.go b/exchange/bidder.go
index 83466c7d..895cff93 100644
--- a/exchange/bidder.go
+++ b/exchange/bidder.go
@@ -55,6 +55,7 @@ type adaptedBidder interface {
 // pbsOrtbBid is a Bid returned by an adaptedBidder.
 //
 // pbsOrtbBid.bid.Ext will become "response.seatbid[i].bid.ext.bidder" in the final OpenRTB response.
+// pbsOrtbBid.bidMeta will become "response.seatbid[i].bid.ext.prebid.meta" in the final OpenRTB response.
 // pbsOrtbBid.bidType will become "response.seatbid[i].bid.ext.prebid.type" in the final OpenRTB response.
 // pbsOrtbBid.bidTargets does not need to be filled out by the Bidder. It will be set later by the exchange.
 // pbsOrtbBid.bidVideo is optional but should be filled out by the Bidder if bidType is video.
@@ -64,6 +65,7 @@ type adaptedBidder interface {
 // pbsOrtbBid.generatedBidID is unique bid id generated by prebid server if generate bid id option is enabled in config
 type pbsOrtbBid struct {
 	bid               *openrtb2.Bid
+	bidMeta           *openrtb_ext.ExtBidPrebidMeta
 	bidType           openrtb_ext.BidType
 	bidTargets        map[string]string
 	bidVideo          *openrtb_ext.ExtBidPrebidVideo
@@ -251,6 +253,7 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, request *openrtb2.B
 						}
 						seatBid.bids = append(seatBid.bids, &pbsOrtbBid{
 							bid:          bidResponse.Bids[i].Bid,
+							bidMeta:      bidResponse.Bids[i].BidMeta,
 							bidType:      bidResponse.Bids[i].BidType,
 							bidVideo:     bidResponse.Bids[i].BidVideo,
 							dealPriority: bidResponse.Bids[i].DealPriority,
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 6aaafcd1..1cedb5d9 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -14,11 +14,10 @@ import (
 	"strings"
 	"time"
 
-	"github.com/mxmCherry/openrtb/v15/openrtb2"
-	"github.com/prebid/prebid-server/stored_requests"
-
+	"github.com/buger/jsonparser"
 	"github.com/gofrs/uuid"
 	"github.com/golang/glog"
+	"github.com/mxmCherry/openrtb/v15/openrtb2"
 	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/currency"
@@ -27,6 +26,7 @@ import (
 	"github.com/prebid/prebid-server/metrics"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/prebid_cache_client"
+	"github.com/prebid/prebid-server/stored_requests"
 )
 
 type ContextKey string
@@ -135,6 +135,11 @@ func NewExchange(adapters map[openrtb_ext.BidderName]adaptedBidder, cache prebid
 	}
 }
 
+type ImpExtInfo struct {
+	EchoVideoAttrs bool
+	StoredImp      []byte
+}
+
 // AuctionRequest holds the bid request for the auction
 // and all other information needed to process that request
 type AuctionRequest struct {
@@ -145,6 +150,7 @@ type AuctionRequest struct {
 	StartTime                  time.Time
 	Warnings                   []error
 	GlobalPrivacyControlHeader string
+	ImpExtInfoMap              map[string]ImpExtInfo
 
 	// LegacyLabels is included here for temporary compatability with cleanOpenRTBRequests
 	// in HoldAuction until we get to factoring it away. Do not use for anything new.
@@ -303,7 +309,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *
 	}
 
 	// Build the response
-	return e.buildBidResponse(ctx, liveAdapters, adapterBids, r.BidRequest, adapterExtra, auc, bidResponseExt, cacheInstructions.returnCreative, errs)
+	return e.buildBidResponse(ctx, liveAdapters, adapterBids, r.BidRequest, adapterExtra, auc, bidResponseExt, cacheInstructions.returnCreative, r.ImpExtInfoMap, errs)
 }
 
 func (e *exchange) parseGDPRDefaultValue(bidRequest *openrtb2.BidRequest) gdpr.Signal {
@@ -452,6 +458,7 @@ func (e *exchange) getAllBids(
 			reqInfo := adapters.NewExtraRequestInfo(conversions)
 			reqInfo.PbsEntryPoint = bidderRequest.BidderLabels.RType
 			reqInfo.GlobalPrivacyControlHeader = globalPrivacyControlHeader
+
 			bids, err := e.adapterMap[bidderRequest.BidderCoreName].requestBid(ctx, bidderRequest.BidRequest, bidderRequest.BidderName, adjustmentFactor, conversions, &reqInfo, accountDebugAllowed, headerDebugAllowed)
 
 			// Add in time reporting
@@ -589,7 +596,7 @@ func errsToBidderWarnings(errs []error) []openrtb_ext.ExtBidderMessage {
 }
 
 // This piece takes all the bids supplied by the adapters and crafts an openRTB response to send back to the requester
-func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ext.BidderName, adapterBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, bidRequest *openrtb2.BidRequest, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, bidResponseExt *openrtb_ext.ExtBidResponse, returnCreative bool, errList []error) (*openrtb2.BidResponse, error) {
+func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ext.BidderName, adapterBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, bidRequest *openrtb2.BidRequest, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, bidResponseExt *openrtb_ext.ExtBidResponse, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, errList []error) (*openrtb2.BidResponse, error) {
 	bidResponse := new(openrtb2.BidResponse)
 	var err error
 
@@ -605,7 +612,7 @@ func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_
 	for _, a := range liveAdapters {
 		//while processing every single bib, do we need to handle categories here?
 		if adapterBids[a] != nil && len(adapterBids[a].bids) > 0 {
-			sb := e.makeSeatBid(adapterBids[a], a, adapterExtra, auc, returnCreative)
+			sb := e.makeSeatBid(adapterBids[a], a, adapterExtra, auc, returnCreative, impExtInfoMap)
 			seatBids = append(seatBids, *sb)
 			bidResponse.Cur = adapterBids[a].currency
 		}
@@ -899,14 +906,14 @@ func (e *exchange) makeExtBidResponse(adapterBids map[openrtb_ext.BidderName]*pb
 
 // Return an openrtb seatBid for a bidder
 // BuildBidResponse is responsible for ensuring nil bid seatbids are not included
-func (e *exchange) makeSeatBid(adapterBid *pbsOrtbSeatBid, adapter openrtb_ext.BidderName, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, returnCreative bool) *openrtb2.SeatBid {
+func (e *exchange) makeSeatBid(adapterBid *pbsOrtbSeatBid, adapter openrtb_ext.BidderName, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo) *openrtb2.SeatBid {
 	seatBid := &openrtb2.SeatBid{
 		Seat:  adapter.String(),
 		Group: 0, // Prebid cannot support roadblocking
 	}
 
 	var errList []error
-	seatBid.Bid, errList = e.makeBid(adapterBid.bids, auc, returnCreative)
+	seatBid.Bid, errList = e.makeBid(adapterBid.bids, auc, returnCreative, impExtInfoMap)
 	if len(errList) > 0 {
 		adapterExtra[adapter].Errors = append(adapterExtra[adapter].Errors, errsToBidderErrors(errList)...)
 	}
@@ -914,7 +921,7 @@ func (e *exchange) makeSeatBid(adapterBid *pbsOrtbSeatBid, adapter openrtb_ext.B
 	return seatBid
 }
 
-func (e *exchange) makeBid(bids []*pbsOrtbBid, auc *auction, returnCreative bool) ([]openrtb2.Bid, []error) {
+func (e *exchange) makeBid(bids []*pbsOrtbBid, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo) ([]openrtb2.Bid, []error) {
 	result := make([]openrtb2.Bid, 0, len(bids))
 	errs := make([]error, 0, 1)
 
@@ -925,6 +932,7 @@ func (e *exchange) makeBid(bids []*pbsOrtbBid, auc *auction, returnCreative bool
 			Events:            bid.bidEvents,
 			Targeting:         bid.bidTargets,
 			Type:              bid.bidType,
+			Meta:              bid.bidMeta,
 			Video:             bid.bidVideo,
 			BidId:             bid.generatedBidID,
 		}
@@ -935,7 +943,7 @@ func (e *exchange) makeBid(bids []*pbsOrtbBid, auc *auction, returnCreative bool
 			}
 		}
 
-		if bidExtJSON, err := makeBidExtJSON(bid.bid.Ext, bidExtPrebid); err != nil {
+		if bidExtJSON, err := makeBidExtJSON(bid.bid.Ext, bidExtPrebid, impExtInfoMap, bid.bid.ImpID); err != nil {
 			errs = append(errs, err)
 		} else {
 			result = append(result, *bid.bid)
@@ -949,19 +957,34 @@ func (e *exchange) makeBid(bids []*pbsOrtbBid, auc *auction, returnCreative bool
 	return result, errs
 }
 
-func makeBidExtJSON(ext json.RawMessage, prebid *openrtb_ext.ExtBidPrebid) (json.RawMessage, error) {
-	// no existing bid.ext. generate a bid.ext with just our prebid section populated.
-	if len(ext) == 0 {
-		bidExt := &openrtb_ext.ExtBid{Prebid: prebid}
-		return json.Marshal(bidExt)
-	}
-
-	// update existing bid.ext with our prebid section. if bid.ext.prebid already exists, it will be overwritten.
+func makeBidExtJSON(ext json.RawMessage, prebid *openrtb_ext.ExtBidPrebid, impExtInfoMap map[string]ImpExtInfo, impId string) (json.RawMessage, error) {
+	// update existing bid.ext with prebid section
+	// if bid.ext.prebid already exists, it will be overwritten.
+	// if echoVideoAttrs set to true stored video attributes will be added to bid.ext.storedrequestattributes
 	var extMap map[string]interface{}
-	if err := json.Unmarshal(ext, &extMap); err != nil {
-		return nil, err
+
+	if len(ext) != 0 {
+		if err := json.Unmarshal(ext, &extMap); err != nil {
+			return nil, err
+		}
+	} else {
+		extMap = make(map[string]interface{})
 	}
+
 	extMap[openrtb_ext.PrebidExtKey] = prebid
+
+	if impExtInfo, ok := impExtInfoMap[impId]; ok && impExtInfo.EchoVideoAttrs {
+
+		videoData, _, _, err := jsonparser.Get(impExtInfo.StoredImp, "video")
+		if err != nil && err != jsonparser.KeyPathNotFoundError {
+			return nil, err
+		}
+		//handler for case where EchoVideoAttrs is true, but video data is not found
+		if len(videoData) > 0 {
+			extMap[openrtb_ext.StoredRequestAttributes] = json.RawMessage(videoData)
+		}
+	}
+
 	return json.Marshal(extMap)
 }
 
diff --git a/openrtb_ext/bid.go b/openrtb_ext/bid.go
index 535ac76b..ce6e25fd 100644
--- a/openrtb_ext/bid.go
+++ b/openrtb_ext/bid.go
@@ -157,3 +157,7 @@ func min(x, y int) int {
 	}
 	return y
 }
+
+const (
+	StoredRequestAttributes = "storedrequestattributes"
+)
diff --git a/openrtb_ext/imp_grid.go b/openrtb_ext/imp_grid.go
index d38e610d..c4530426 100644
--- a/openrtb_ext/imp_grid.go
+++ b/openrtb_ext/imp_grid.go
@@ -1,6 +1,9 @@
 package openrtb_ext
 
+import "encoding/json"
+
 // ExtImpGrid defines the contract for bidrequest.imp[i].ext.grid
 type ExtImpGrid struct {
-	Uid int `json:"uid"`
+	Uid      int             `json:"uid"`
+	Keywords json.RawMessage `json:"keywords,omitempty"`
 }
diff --git a/static/bidder-params/grid.json b/static/bidder-params/grid.json
index 67f9b12f..42c88584 100644
--- a/static/bidder-params/grid.json
+++ b/static/bidder-params/grid.json
@@ -7,6 +7,18 @@
     "uid": {
       "type": "integer",
       "description": "An ID which identifies this placement of the impression"
+    },
+    "keywords": {
+      "type": "object",
+      "description": "Keywords",
+      "properties": {
+        "site": {
+          "type": "object"
+        },
+        "user": {
+          "type": "object"
+        }
+      }
     }
   },
   "required": []
diff --git a/util/jsonutil/jsonutil.go b/util/jsonutil/jsonutil.go
index 963477de..4fd56745 100644
--- a/util/jsonutil/jsonutil.go
+++ b/util/jsonutil/jsonutil.go
@@ -7,8 +7,10 @@ import (
 )
 
 var comma = []byte(",")[0]
+var colon = []byte(":")[0]
 
-func DropElement(extension []byte, elementName string) ([]byte, error) {
+func findElementIndexes(extension []byte, elementName string) (bool, int64, int64, error) {
+	found := false
 	buf := bytes.NewBuffer(extension)
 	dec := json.NewDecoder(buf)
 	var startIndex int64
@@ -20,14 +22,15 @@ func DropElement(extension []byte, elementName string) ([]byte, error) {
 			break
 		}
 		if err != nil {
-			return nil, err
+			return false, -1, -1, err
 		}
 
 		if token == elementName {
 			err := dec.Decode(&i)
 			if err != nil {
-				return nil, err
+				return false, -1, -1, err
 			}
+			found = true
 			endIndex := dec.InputOffset()
 
 			if dec.More() {
@@ -45,13 +48,43 @@ func DropElement(extension []byte, elementName string) ([]byte, error) {
 					endIndex++
 				}
 			}
-
-			extension = append(extension[:startIndex], extension[endIndex:]...)
-			break
+			return found, startIndex, endIndex, nil
 		} else {
 			startIndex = dec.InputOffset()
 		}
 
 	}
-	return extension, nil
+
+	return false, -1, -1, nil
+}
+
+func DropElement(extension []byte, elementName string) ([]byte, error) {
+	found, startIndex, endIndex, err := findElementIndexes(extension, elementName)
+	if found {
+		extension = append(extension[:startIndex], extension[endIndex:]...)
+	}
+	return extension, err
+}
+
+func FindElement(extension []byte, elementName string) (bool, []byte, error) {
+
+	found, startIndex, endIndex, err := findElementIndexes(extension, elementName)
+
+	if found && err == nil {
+		element := extension[startIndex:endIndex]
+		index := 0
+		for {
+			if index < len(element) && element[index] != colon {
+				index++
+			} else {
+				index++
+				break
+			}
+		}
+		element = element[index:]
+		return found, element, err
+	}
+
+	return found, nil, err
+
 }
diff --git a/util/maputil/maputil.go b/util/maputil/maputil.go
index 0d1d7dbb..595e9321 100644
--- a/util/maputil/maputil.go
+++ b/util/maputil/maputil.go
@@ -19,3 +19,12 @@ func ReadEmbeddedSlice(m map[string]interface{}, k string) ([]interface{}, bool)
 
 	return nil, false
 }
+
+// ReadEmbeddedString reads element k from the map m as a string
+func ReadEmbeddedString(m map[string]interface{}, k string) (string, bool) {
+	if v, ok := m[k]; ok {
+		vCasted, ok := v.(string)
+		return vCasted, ok
+	}
+	return "", false
+}


Test patch:
diff --git a/adapters/grid/gridtest/exemplary/simple-banner.json b/adapters/grid/gridtest/exemplary/simple-banner.json
index 1a5ea014..1161e482 100644
--- a/adapters/grid/gridtest/exemplary/simple-banner.json
+++ b/adapters/grid/gridtest/exemplary/simple-banner.json
@@ -4,6 +4,11 @@
         "site": {
             "page": "https://good.site/url"
         },
+        "ext": {
+            "keywords": {
+                "anyName": "anyVal"
+            }
+        },
         "imp": [{
             "id": "test-imp-id",
             "banner": {
@@ -28,6 +33,11 @@
                 "site": {
                     "page": "https://good.site/url"
                 },
+                "ext": {
+                    "keywords": {
+                        "anyName": "anyVal"
+                    }
+                },
                 "imp": [{
                     "id": "test-imp-id",
                     "banner": {
@@ -59,7 +69,12 @@
                         "cid": "987",
                         "crid": "12345678",
                         "h": 250,
-                        "w": 300
+                        "w": 300,
+                        "ext": {
+                            "bidder": {
+                                "grid": {"demandSource":"trustx"}
+                            }
+                        }
                     }]
                 }],
                 "cur": "USD"
@@ -79,7 +94,12 @@
                 "cid": "987",
                 "crid": "12345678",
                 "w": 300,
-                "h": 250
+                "h": 250,
+                "ext": {
+                    "bidder": {
+                        "grid": {"demandSource":"trustx"}
+                    }
+                }
             },
             "type": "banner"
         }]
diff --git a/adapters/grid/gridtest/exemplary/with-keywords.json b/adapters/grid/gridtest/exemplary/with-keywords.json
new file mode 100644
index 00000000..a8875220
--- /dev/null
+++ b/adapters/grid/gridtest/exemplary/with-keywords.json
@@ -0,0 +1,122 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "site": {
+            "page": "https://good.site/url"
+        },
+        "imp": [{
+            "id": "test-imp-id",
+            "banner": {
+                "format": [{
+                    "w": 300,
+                    "h": 250
+                }]
+            },
+            "ext": {
+                "bidder": {
+                    "uid": 1,
+                    "keywords": {
+                        "site": {
+                            "somePublisher": [
+                                {
+                                    "name": "someName",
+                                    "topic": ["stress", "fear"]
+                                }
+                            ]
+                        }
+                    }
+                }
+            }
+        }]
+    },
+
+    "httpCalls": [{
+        "expectedRequest": {
+            "uri": "http://localhost/prebid",
+            "body": {
+                "ext": {
+                    "keywords": {
+                        "site": {
+                            "somePublisher": [
+                                {
+                                    "name": "someName",
+                                    "segments": [
+                                        { "name":  "topic", "value": "stress" },
+                                        { "name":  "topic", "value": "fear" }
+                                    ]
+                                }
+                            ]
+                        }
+                    }
+                },
+                "id": "test-request-id",
+                "site": {
+                    "page": "https://good.site/url"
+                },
+                "imp": [{
+                    "id": "test-imp-id",
+                    "banner": {
+                        "format": [{
+                            "w": 300,
+                            "h": 250
+                        }]
+                    },
+                    "ext": {
+                        "bidder": {
+                            "uid": 1,
+                            "keywords": {
+                                "site": {
+                                    "somePublisher": [
+                                        {
+                                            "name": "someName",
+                                            "topic": ["stress", "fear"]
+                                        }
+                                    ]
+                                }
+                            }
+                        }
+                    }
+                }]
+            }
+        },
+        "mockResponse": {
+            "status": 200,
+            "body": {
+                "id": "test-request-id",
+                "seatbid": [{
+                    "seat": "grid",
+                    "bid": [{
+                        "id": "randomid",
+                        "impid": "test-imp-id",
+                        "price": 0.500000,
+                        "adid": "12345678",
+                        "adm": "some-test-ad",
+                        "cid": "987",
+                        "crid": "12345678",
+                        "h": 250,
+                        "w": 300
+                    }]
+                }],
+                "cur": "USD"
+            }
+        }
+    }],
+
+    "expectedBidResponses": [{
+        "currency": "USD",
+        "bids": [{
+            "bid": {
+                "id": "randomid",
+                "impid": "test-imp-id",
+                "price": 0.5,
+                "adm": "some-test-ad",
+                "adid": "12345678",
+                "cid": "987",
+                "crid": "12345678",
+                "w": 300,
+                "h": 250
+            },
+            "type": "banner"
+        }]
+    }]
+}
diff --git a/adapters/grid/gridtest/exemplary/with-mixed-keywords.json b/adapters/grid/gridtest/exemplary/with-mixed-keywords.json
new file mode 100644
index 00000000..49831991
--- /dev/null
+++ b/adapters/grid/gridtest/exemplary/with-mixed-keywords.json
@@ -0,0 +1,308 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "site": {
+            "page": "https://good.site/url",
+            "keywords": "siteKey1,siteKey2"
+        },
+        "ext": {
+            "keywords": {
+                "stringKey": "stringVal",
+                "wrongKeys1": {
+                    "someKey1": "someVal1",
+                    "someKey2": "someVal2",
+                    "someKey3": ["someVal31", "someVal32"],
+                    "someKey4": { "key1": "val1", "key2": "val2" }
+                },
+                "anotherKeys": [
+                    {"someKey1":  "someVal1"},
+                    {"someKey2":  "someVal2"}
+                ],
+                "site": {
+                    "stringSiteKey": "stringSiteVal",
+                    "wrongSiteKeys1": {
+                        "someKey1": "someVal1",
+                        "someKey2": "someVal2",
+                        "someKey3": ["someVal31", "someVal32"],
+                        "someKey4": { "key1": "val1", "key2": "val2" }
+                    },
+                    "anotherSiteKeys": [
+                        {"someKey1":  "someVal1"},
+                        {"someKey2":  "someVal2"},
+                        "someStrKey",
+                        ["someVal31", "someVal32"],
+                        {
+                            "name":  "someName3",
+                            "keyName": ["keyVal", { "name": "wrongKey" }],
+                            "wrongKeyName": "stKeyVal",
+                            "wrongKeyName2": { "name": "someName", "value": "stKeyVal" }
+                        }
+                    ],
+                    "pub": [
+                        "k1",
+                        "k2"
+                    ],
+                    "somePublisher": [
+                        {
+                            "name": "someName2",
+                            "topic": ["anyKey"]
+                        }
+                    ],
+                    "formatedSitePublisher": [
+                        {
+                            "name": "formatedPub2",
+                            "segments": [
+                                { "name": "segName1", "value": "segVal1" },
+                                { "name": "segName2", "value": "segVal2" }
+                            ]
+                        },
+                        {
+                            "name": "notFormatedPub",
+                            "topic2": ["notFormatedKw"]
+                        }
+                    ]
+                },
+                "user": {
+                    "formatedUserPublisher": [
+                        {
+                            "name": "formatedPub2",
+                            "segments": [
+                                { "name": "segName1", "value": "segVal1" },
+                                { "name": "segName2", "value": "segVal2" }
+                            ]
+                        }
+                    ]
+                }
+            }
+        },
+        "user": {
+            "keywords": "userKey1"
+        },
+        "imp": [{
+            "id": "test-imp-id",
+            "banner": {
+                "format": [{
+                    "w": 300,
+                    "h": 250
+                }]
+            },
+            "ext": {
+                "bidder": {
+                    "uid": 1,
+                    "keywords": {
+                        "site": {
+                            "somePublisher": [
+                                {
+                                    "name": "someName",
+                                    "topic": ["stress", "fear"]
+                                }
+                            ]
+                        },
+                        "user": {
+                            "formatedPublisher": [
+                                {
+                                    "name": "formatedPub1",
+                                    "segments": [
+                                        { "name": "segName1", "value": "segVal1" },
+                                        { "name": "segName2", "value": "segVal2" },
+                                        ["someKeyword"],
+                                        "stringKey"
+                                    ],
+                                    "bottom": ["bottomKey1", "bottomKey2"]
+                                }
+                            ]
+                        }
+                    }
+                }
+            }
+        }]
+    },
+
+    "httpCalls": [{
+        "expectedRequest": {
+            "uri": "http://localhost/prebid",
+            "body": {
+                "ext": {
+                    "keywords": {
+                        "site": {
+                            "anotherSiteKeys": [
+                                {
+                                    "name":  "someName3",
+                                    "segments": [
+                                        { "name": "keyName", "value": "keyVal" }
+                                    ]
+                                }
+                            ],
+                            "somePublisher": [
+                                {
+                                    "name": "someName",
+                                    "segments": [
+                                        { "name": "topic", "value": "stress" },
+                                        { "name": "topic", "value": "fear" }
+                                    ]
+                                },
+                                {
+                                    "name": "someName2",
+                                    "segments": [
+                                        { "name": "topic", "value": "anyKey" }
+                                    ]
+                                }
+                            ],
+                            "formatedSitePublisher": [
+                                {
+                                    "name": "formatedPub2",
+                                    "segments": [
+                                        { "name": "segName1", "value": "segVal1" },
+                                        { "name": "segName2", "value": "segVal2" }
+                                    ]
+                                },
+                                {
+                                    "name": "notFormatedPub",
+                                    "segments": [
+                                        { "name": "topic2", "value": "notFormatedKw" }
+                                    ]
+                                }
+                            ],
+                            "ortb2": [
+                                {
+                                    "name": "keywords",
+                                    "segments": [
+                                        { "name": "keywords", "value": "siteKey1" },
+                                        { "name": "keywords", "value": "siteKey2" }
+                                    ]
+                                }
+                            ]
+                        },
+                        "user": {
+                            "ortb2": [
+                                {
+                                    "name": "keywords",
+                                    "segments": [
+                                        { "name": "keywords", "value": "userKey1" }
+                                    ]
+                                }
+                            ],
+                            "formatedUserPublisher": [
+                                {
+                                    "name": "formatedPub2",
+                                    "segments": [
+                                        { "name": "segName1", "value": "segVal1" },
+                                        { "name": "segName2", "value": "segVal2" }
+                                    ]
+                                }
+                            ],
+                            "formatedPublisher": [
+                                {
+                                    "name": "formatedPub1",
+                                    "segments": [
+                                        { "name": "segName1", "value": "segVal1" },
+                                        { "name": "segName2", "value": "segVal2" },
+                                        { "name": "bottom", "value": "bottomKey1" },
+                                        { "name": "bottom", "value": "bottomKey2" },
+                                        { "name": "segments", "value": "stringKey" }
+                                    ]
+                                }
+                            ]
+                        },
+                        "wrongKeys1": {
+                            "someKey1": "someVal1",
+                            "someKey2": "someVal2",
+                            "someKey3": ["someVal31", "someVal32"],
+                            "someKey4": { "key1": "val1", "key2": "val2" }
+                        },
+                        "stringKey": "stringVal",
+                        "anotherKeys": [
+                            {"someKey1":  "someVal1"},
+                            {"someKey2":  "someVal2"}
+                        ]
+                    }
+                },
+                "id": "test-request-id",
+                "site": {
+                    "page": "https://good.site/url",
+                    "keywords": "siteKey1,siteKey2"
+                },
+                "user": {
+                    "keywords": "userKey1"
+                },
+                "imp": [{
+                    "id": "test-imp-id",
+                    "banner": {
+                        "format": [{
+                            "w": 300,
+                            "h": 250
+                        }]
+                    },
+                    "ext": {
+                        "bidder": {
+                            "uid": 1,
+                            "keywords": {
+                                "site": {
+                                    "somePublisher": [
+                                        {
+                                            "name": "someName",
+                                            "topic": ["stress", "fear"]
+                                        }
+                                    ]
+                                },
+                                "user": {
+                                    "formatedPublisher": [
+                                        {
+                                            "name": "formatedPub1",
+                                            "segments": [
+                                                { "name": "segName1", "value": "segVal1" },
+                                                { "name": "segName2", "value": "segVal2" },
+                                                ["someKeyword"],
+                                                "stringKey"
+                                            ],
+                                            "bottom": ["bottomKey1", "bottomKey2"]
+                                        }
+                                    ]
+                                }
+                            }
+                        }
+                    }
+                }]
+            }
+        },
+        "mockResponse": {
+            "status": 200,
+            "body": {
+                "id": "test-request-id",
+                "seatbid": [{
+                    "seat": "grid",
+                    "bid": [{
+                        "id": "randomid",
+                        "impid": "test-imp-id",
+                        "price": 0.500000,
+                        "adid": "12345678",
+                        "adm": "some-test-ad",
+                        "cid": "987",
+                        "crid": "12345678",
+                        "h": 250,
+                        "w": 300
+                    }]
+                }],
+                "cur": "USD"
+            }
+        }
+    }],
+
+    "expectedBidResponses": [{
+        "currency": "USD",
+        "bids": [{
+            "bid": {
+                "id": "randomid",
+                "impid": "test-imp-id",
+                "price": 0.5,
+                "adm": "some-test-ad",
+                "adid": "12345678",
+                "cid": "987",
+                "crid": "12345678",
+                "w": 300,
+                "h": 250
+            },
+            "type": "banner"
+        }]
+    }]
+}
diff --git a/adapters/grid/gridtest/exemplary/with-siteuser-keywords.json b/adapters/grid/gridtest/exemplary/with-siteuser-keywords.json
new file mode 100644
index 00000000..7a4822f8
--- /dev/null
+++ b/adapters/grid/gridtest/exemplary/with-siteuser-keywords.json
@@ -0,0 +1,120 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "site": {
+            "page": "https://good.site/url",
+            "keywords": "siteKey1,siteKey2"
+        },
+        "user": {
+            "keywords": "userKey1"
+        },
+        "imp": [{
+            "id": "test-imp-id",
+            "banner": {
+                "format": [{
+                    "w": 300,
+                    "h": 250
+                }]
+            },
+            "ext": {
+                "bidder": {
+                    "uid": 1
+                }
+            }
+        }]
+    },
+
+    "httpCalls": [{
+        "expectedRequest": {
+            "uri": "http://localhost/prebid",
+            "body": {
+                "ext": {
+                    "keywords": {
+                        "site": {
+                            "ortb2": [
+                                {
+                                    "name": "keywords",
+                                    "segments": [
+                                        { "name": "keywords", "value": "siteKey1" },
+                                        { "name": "keywords", "value": "siteKey2" }
+                                    ]
+                                }
+                            ]
+                        },
+                        "user": {
+                            "ortb2": [
+                                {
+                                    "name": "keywords",
+                                    "segments": [
+                                        { "name": "keywords", "value": "userKey1" }
+                                    ]
+                                }
+                            ]
+                        }
+                    }
+                },
+                "id": "test-request-id",
+                "site": {
+                    "page": "https://good.site/url",
+                    "keywords": "siteKey1,siteKey2"
+                },
+                "user": {
+                    "keywords": "userKey1"
+                },
+                "imp": [{
+                    "id": "test-imp-id",
+                    "banner": {
+                        "format": [{
+                            "w": 300,
+                            "h": 250
+                        }]
+                    },
+                    "ext": {
+                        "bidder": {
+                            "uid": 1
+                        }
+                    }
+                }]
+            }
+        },
+        "mockResponse": {
+            "status": 200,
+            "body": {
+                "id": "test-request-id",
+                "seatbid": [{
+                    "seat": "grid",
+                    "bid": [{
+                        "id": "randomid",
+                        "impid": "test-imp-id",
+                        "price": 0.500000,
+                        "adid": "12345678",
+                        "adm": "some-test-ad",
+                        "cid": "987",
+                        "crid": "12345678",
+                        "h": 250,
+                        "w": 300
+                    }]
+                }],
+                "cur": "USD"
+            }
+        }
+    }],
+
+    "expectedBidResponses": [{
+        "currency": "USD",
+        "bids": [{
+            "bid": {
+                "id": "randomid",
+                "impid": "test-imp-id",
+                "price": 0.5,
+                "adm": "some-test-ad",
+                "adid": "12345678",
+                "cid": "987",
+                "crid": "12345678",
+                "w": 300,
+                "h": 250
+            },
+            "type": "banner"
+        }]
+    }]
+}
diff --git a/adapters/grid/gridtest/params/race/banner.json b/adapters/grid/gridtest/params/race/banner.json
index 7e347f11..f64051bd 100644
--- a/adapters/grid/gridtest/params/race/banner.json
+++ b/adapters/grid/gridtest/params/race/banner.json
@@ -1,4 +1,14 @@
 {
-  "uid": 1
+  "uid": 1,
+  "keywords": {
+    "site": {
+      "somePublisher": [
+        {
+          "name": "someName",
+          "topic": ["stress", "fear"]
+        }
+      ]
+    }
+  }
 }
 
diff --git a/adapters/yieldmo/yieldmotest/exemplary/with_gpid.json b/adapters/yieldmo/yieldmotest/exemplary/with_gpid.json
new file mode 100644
index 00000000..bd9e9110
--- /dev/null
+++ b/adapters/yieldmo/yieldmotest/exemplary/with_gpid.json
@@ -0,0 +1,105 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "placementId": "123"
+          },
+	  "data": {
+		"adserver": {
+			"name": "adserver_name",
+			"adslot": "adserver_adslot"
+		},
+		"pbadslot": "pbadslot_1"
+	  }
+        }
+      }
+    ],
+    "site": {
+      "id": "fake-site-id"
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://ads.yieldmo.com/openrtb2",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ]
+              },
+              "ext": {
+                "placement_id": "123",
+		"gpid": "pbadslot_1"
+              }
+            }
+          ],
+          "site": {
+            "id": "fake-site-id"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "yieldmo",
+              "bid": [
+                {
+                  "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
+                  "impid": "test-imp-id",
+                  "price": 0.500000,
+                  "adm": "some-test-ad",
+                  "crid": "crid_10",
+                  "h": 250,
+                  "w": 300
+                }
+              ]
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "EUR",
+      "bids": [
+        {
+          "bid": {
+            "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
+            "impid": "test-imp-id",
+            "price": 0.5,
+            "adm": "some-test-ad",
+            "crid": "crid_10",
+            "w": 300,
+            "h": 250
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index 4835dd92..ec373094 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -18,7 +18,6 @@ import (
 	"time"
 
 	"github.com/buger/jsonparser"
-	jsonpatch "github.com/evanphx/json-patch"
 	"github.com/mxmCherry/openrtb/v15/native1"
 	nativeRequests "github.com/mxmCherry/openrtb/v15/native1/request"
 	"github.com/mxmCherry/openrtb/v15/openrtb2"
@@ -1020,8 +1019,10 @@ func TestStoredRequests(t *testing.T) {
 		hardcodedResponseIPValidator{response: true},
 	}
 
+	testStoreVideoAttr := []bool{true, true, false, false}
+
 	for i, requestData := range testStoredRequests {
-		newRequest, errList := deps.processStoredRequests(context.Background(), json.RawMessage(requestData))
+		newRequest, impExtInfoMap, errList := deps.processStoredRequests(context.Background(), json.RawMessage(requestData))
 		if len(errList) != 0 {
 			for _, err := range errList {
 				if err != nil {
@@ -1032,9 +1033,50 @@ func TestStoredRequests(t *testing.T) {
 			}
 		}
 		expectJson := json.RawMessage(testFinalRequests[i])
-		if !jsonpatch.Equal(newRequest, expectJson) {
-			t.Errorf("Error in processStoredRequests, test %d failed on compare\nFound:\n%s\nExpected:\n%s", i, string(newRequest), string(expectJson))
+
+		assert.JSONEq(t, string(expectJson), string(newRequest), "Incorrect result request %d", i)
+
+		expectedImp := testStoredImpIds[i]
+		expectedStoredImp := json.RawMessage(testStoredImps[i])
+		if len(impExtInfoMap[expectedImp].StoredImp) > 0 {
+			assert.JSONEq(t, string(expectedStoredImp), string(impExtInfoMap[expectedImp].StoredImp), "Incorrect expected stored imp %d", i)
+
 		}
+		assert.Equalf(t, testStoreVideoAttr[i], impExtInfoMap[expectedImp].EchoVideoAttrs, "EchoVideoAttrs value is incorrect")
+	}
+}
+
+func TestStoredRequestsVideoErrors(t *testing.T) {
+	deps := &endpointDeps{
+		&nobidExchange{},
+		newParamsValidator(t),
+		&mockStoredReqFetcher{},
+		empty_fetcher.EmptyFetcher{},
+		empty_fetcher.EmptyFetcher{},
+		&config.Configuration{MaxRequestSize: maxSize},
+		newTestMetrics(),
+		analyticsConf.NewPBSAnalytics(&config.Analytics{}),
+		map[string]string{},
+		false,
+		[]byte{},
+		openrtb_ext.BuildBidderMap(),
+		nil,
+		nil,
+		hardcodedResponseIPValidator{response: true},
+	}
+
+	// tests processStoredRequests function behavior in parsing incorrect input related to echovideoattrs feature
+	// this test now has 2 scenarios:
+	// 1) expected value we get using jsonparser.GetBoolean is integer
+	// 2) imp id is not found using jsonparser.GetString
+	// this loop iterates over testStoredRequestsErrors where input json has incorrect set up
+	// testStoredRequestsErrorsResults variable contains error message for every iteration
+
+	for i, requestData := range testStoredRequestsErrors {
+		_, _, errList := deps.processStoredRequests(context.Background(), json.RawMessage(requestData))
+
+		assert.NotEmpty(t, errList, "processStoredRequests should return error")
+		assert.Contains(t, errList[0].Error(), testStoredRequestsErrorsResults[i], "Incorrect error")
 	}
 }
 
@@ -2902,7 +2944,24 @@ var testStoredRequestData = map[string]json.RawMessage{
 // third below has valid JSON and matches schema
 var testStoredImpData = map[string]json.RawMessage{
 	"1": json.RawMessage(`{
-"id": "adUnit1",
+			"id": "adUnit1",
+			"ext": {
+				"appnexus": {
+					"placementId": "abc",
+					"position": "above",
+					"reserve": 0.35
+				},
+				"rubicon": {
+					"accountId": "abc"
+				}
+			},
+			"video":{
+				"w":200,
+				"h":300
+			}
+		}`),
+	"2": json.RawMessage(`{
+			"id": "adUnit1",
 			"ext": {
 				"appnexus": {
 					"placementId": "abc",
@@ -2915,7 +2974,7 @@ var testStoredImpData = map[string]json.RawMessage{
 			}
 		}`),
 	"7": json.RawMessage(`{
-"id": "adUnit1",
+			"id": "adUnit1",
 			"ext": {
 				"appnexus": {
 					"placementId": 12345678,
@@ -2944,6 +3003,15 @@ var testStoredImpData = map[string]json.RawMessage{
 				}
 			}
 		}`),
+	"10": json.RawMessage(`{
+			"ext": {
+				"appnexus": {
+					"placementId": 12345678,
+					"position": "above",
+					"reserve": 0.35
+				}
+			}
+		}`),
 }
 
 // Incoming requests with stored request IDs
@@ -2952,10 +3020,17 @@ var testStoredRequests = []string{
 		"id": "ThisID",
 		"imp": [
 			{
+				"video":{
+					"h":300,
+					"w":200
+				},
 				"ext": {
 					"prebid": {
 						"storedrequest": {
 							"id": "1"
+						},
+						"options": {
+							"echovideoattrs": true
 						}
 					}
 				}
@@ -2980,6 +3055,9 @@ var testStoredRequests = []string{
 					"prebid": {
 						"storedrequest": {
 							"id": "1"
+						},
+						"options": {
+							"echovideoattrs": true
 						}
 					},
 					"appnexus": {
@@ -3008,7 +3086,10 @@ var testStoredRequests = []string{
 				"ext": {
 					"prebid": {
 						"storedrequest": {
-							"id": "1"
+							"id": "2"
+						},
+						"options": {
+							"echovideoattrs": false
 						}
 					}
 				}
@@ -3059,25 +3140,22 @@ var testStoredRequests = []string{
 	}`,
 }
 
-// The expected requests after stored request processing
-var testFinalRequests = []string{
+var testStoredRequestsErrors = []string{
 	`{
 		"id": "ThisID",
 		"imp": [
 			{
-				"id": "adUnit1",
+				"video":{
+					"h":300,
+					"w":200
+				},
 				"ext": {
-					"appnexus": {
-						"placementId": "abc",
-						"position": "above",
-						"reserve": 0.35
-					},
-					"rubicon": {
-						"accountId": "abc"
-					},
 					"prebid": {
 						"storedrequest": {
 							"id": "1"
+						},
+						"options": {
+							"echovideoattrs": 1
 						}
 					}
 				}
@@ -3092,22 +3170,22 @@ var testFinalRequests = []string{
 				}
 			}
 		}
-	}`,
-	`{
+	}`, `{
 		"id": "ThisID",
 		"imp": [
 			{
-				"id": "adUnit2",
+				"video":{
+					"h":300,
+					"w":200
+				},
 				"ext": {
 					"prebid": {
 						"storedrequest": {
-							"id": "1"
+							"id": "10"
+						},
+						"options": {
+							"echovideoattrs": true
 						}
-					},
-					"appnexus": {
-						"placementId": "def",
-						"position": "above",
-						"trafficSourceCode": "mysite.com"
 					}
 				}
 			}
@@ -3122,56 +3200,220 @@ var testFinalRequests = []string{
 			}
 		}
 	}`,
+}
+
+var testStoredRequestsErrorsResults = []string{
+	"Value is not a boolean: 1",
+	"Key path not found",
+}
+
+// The expected requests after stored request processing
+var testFinalRequests = []string{
 	`{
 		"id": "ThisID",
 		"imp": [
 			{
-				"id": "adUnit1",
-				"ext": {
-					"appnexus": {
-						"placementId": "abc",
-						"position": "above",
-						"reserve": 0.35
-					},
-					"rubicon": {
-						"accountId": "abc"
+				"video":{
+					"h":300,
+					"w":200
+				},
+				"ext":{
+					"appnexus":{
+						"placementId":"abc",
+						"position":"above",
+						"reserve":0.35
 					},
-					"prebid": {
-						"storedrequest": {
-							"id": "1"
-						}
+					"prebid":{
+						"storedrequest":{
+							"id":"1"
+						},
+					"options":{
+						"echovideoattrs":true
 					}
+				},
+				"rubicon":{
+					"accountId":"abc"
 				}
+			},
+			"id":"adUnit1"
 			}
 		],
-		"tmax": 500,
 		"ext": {
 			"prebid": {
+				"cache": {
+					"markup": 1
+				},
 				"targeting": {
-					"pricegranularity": "low"
+			}
+		}
+}
+	}`,
+	`{
+		"id": "ThisID",
+		"imp": [
+			{
+				"video":{
+					"w":200,
+					"h":300
+				},
+				"ext":{
+					"appnexus":{
+						"placementId":"def",
+						"position":"above",
+						"trafficSourceCode":"mysite.com"
+					},
+					"prebid":{
+						"storedrequest":{
+							"id":"1"
+						},
+						"options":{
+							"echovideoattrs":true
+						}
+					}
 				},
-				"storedrequest": {
-					"id": "2"
+				"id":"adUnit2"
+			}
+		],
+		"ext": {
+			"prebid": {
+				"cache": {
+					"markup": 1
+				},
+				"targeting": {
 				}
 			}
 		}
 	}`,
+	`{
+  		"ext": {
+  		  "prebid": {
+  		    "storedrequest": {
+  		      "id": "2"
+  		    },
+  		    "targeting": {
+  		      "pricegranularity": "low"
+  		    }
+  		  }
+  		},
+  		"id": "ThisID",
+  		"imp": [
+  		  {
+  		    "ext": {
+  		      "appnexus": {
+  		        "placementId": "abc",
+  		        "position": "above",
+  		        "reserve": 0.35
+  		      },
+  		      "prebid": {
+  		        "storedrequest": {
+  		          "id": "2"
+  		        },
+  		        "options":{
+					"echovideoattrs":false
+				}
+  		      },
+  		      "rubicon": {
+  		        "accountId": "abc"
+  		      }
+  		    },
+  		    "id": "adUnit1"
+  		  }
+  		],
+  		"tmax": 500
+	}
+`,
 	`{
 	"id": "ThisID",
 	"imp": [
 		{
-			"id": "some-static-imp",
-			"video":{
-				"mimes":["video/mp4"]
+    		"id": "some-static-imp",
+    		"video": {
+    		  "mimes": [
+    		    "video/mp4"
+    		  ]
+    		},
+    		"ext": {
+    		  "appnexus": {
+    		    "placementId": "abc",
+    		    "position": "below"
+    		  }
+    		}
+  		},
+  		{
+  		  "ext": {
+  		    "appnexus": {
+  		      "placementId": "abc",
+  		      "position": "above",
+  		      "reserve": 0.35
+  		    },
+  		    "prebid": {
+  		      "storedrequest": {
+  		        "id": "1"
+  		      }
+  		    },
+  		    "rubicon": {
+  		      "accountId": "abc"
+  		    }
+  		  },
+  		  "id": "adUnit1",
+		  "video":{
+				"w":200,
+				"h":300
+          }
+  		}
+	],
+	"ext": {
+		"prebid": {
+			"cache": {
+				"markup": 1
 			},
-			"ext": {
-				"appnexus": {
-					"placementId": "abc",
-					"position": "below"
-				}
+			"targeting": {
 			}
-		},
-		{
+		}
+	}
+}`,
+}
+
+var testStoredImpIds = []string{
+	"adUnit1", "adUnit2", "adUnit1", "some-static-imp",
+}
+
+var testStoredImps = []string{
+	`{
+		"id": "adUnit1",
+        "ext": {
+        	"appnexus": {
+        		"placementId": "abc",
+        		"position": "above",
+        		"reserve": 0.35
+        	},
+        	"rubicon": {
+        		"accountId": "abc"
+        	}
+        },
+		"video":{
+        	"w":200,
+        	"h":300
+		}
+	}`,
+	`{
+		"id": "adUnit1",
+        "ext": {
+        	"appnexus": {
+        		"placementId": "abc",
+        		"position": "above",
+        		"reserve": 0.35
+        	},
+        	"rubicon": {
+        		"accountId": "abc"
+        	}
+        },
+		"video":{
+        	"w":200,
+        	"h":300
+		}
+	}`,
+	`{
 			"id": "adUnit1",
 			"ext": {
 				"appnexus": {
@@ -3181,25 +3423,10 @@ var testFinalRequests = []string{
 				},
 				"rubicon": {
 					"accountId": "abc"
-				},
-				"prebid": {
-					"storedrequest": {
-						"id": "1"
-					}
 				}
 			}
-		}
-	],
-	"ext": {
-		"prebid": {
-			"cache": {
-				"markup": 1
-			},
-			"targeting": {
-			}
-		}
-	}
-}`,
+		}`,
+	``,
 }
 
 type mockStoredReqFetcher struct {
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index 989e6707..aa444352 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -149,7 +149,7 @@ func TestCharacterEscape(t *testing.T) {
 	var errList []error
 
 	// 	4) Build bid response
-	bidResp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, nil, nil, true, errList)
+	bidResp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, nil, nil, true, nil, errList)
 
 	// 	5) Assert we have no errors and one '&' character as we are supposed to
 	if err != nil {
@@ -1307,7 +1307,7 @@ func TestGetBidCacheInfoEndToEnd(t *testing.T) {
 	var errList []error
 
 	// 	4) Build bid response
-	bid_resp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, auc, nil, true, errList)
+	bid_resp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, adapterExtra, auc, nil, true, nil, errList)
 
 	// 	5) Assert we have no errors and the bid response we expected
 	assert.NoError(t, err, "[TestGetBidCacheInfo] buildBidResponse() threw an error")
@@ -1400,7 +1400,7 @@ func TestBidReturnsCreative(t *testing.T) {
 
 	//Run tests
 	for _, test := range testCases {
-		resultingBids, resultingErrs := e.makeBid(sampleBids, sampleAuction, test.inReturnCreative)
+		resultingBids, resultingErrs := e.makeBid(sampleBids, sampleAuction, test.inReturnCreative, nil)
 
 		assert.Equal(t, 0, len(resultingErrs), "%s. Test should not return errors \n", test.description)
 		assert.Equal(t, test.expectedCreativeMarkup, resultingBids[0].AdM, "%s. Ad markup string doesn't match expected \n", test.description)
@@ -1685,12 +1685,64 @@ func TestBidResponseCurrency(t *testing.T) {
 	}
 	// Run tests
 	for i := range testCases {
-		actualBidResp, err := e.buildBidResponse(context.Background(), liveAdapters, testCases[i].adapterBids, bidRequest, adapterExtra, nil, bidResponseExt, true, errList)
+		actualBidResp, err := e.buildBidResponse(context.Background(), liveAdapters, testCases[i].adapterBids, bidRequest, adapterExtra, nil, bidResponseExt, true, nil, errList)
 		assert.NoError(t, err, fmt.Sprintf("[TEST_FAILED] e.buildBidResponse resturns error in test: %s Error message: %s \n", testCases[i].description, err))
 		assert.Equalf(t, testCases[i].expectedBidResponse, actualBidResp, fmt.Sprintf("[TEST_FAILED] Objects must be equal for test: %s \n Expected: >>%s<< \n Actual: >>%s<< ", testCases[i].description, testCases[i].expectedBidResponse.Ext, actualBidResp.Ext))
 	}
 }
 
+func TestBidResponseImpExtInfo(t *testing.T) {
+	// Init objects
+	cfg := &config.Configuration{Adapters: make(map[string]config.Adapter, 1)}
+	cfg.Adapters["appnexus"] = config.Adapter{Endpoint: "http://ib.adnxs.com"}
+
+	e := NewExchange(nil, nil, cfg, &metricsConf.DummyMetricsEngine{}, nil, gdpr.AlwaysAllow{}, nil, nilCategoryFetcher{}).(*exchange)
+
+	liveAdapters := make([]openrtb_ext.BidderName, 1)
+	liveAdapters[0] = "appnexus"
+
+	bidRequest := &openrtb2.BidRequest{
+		ID: "some-request-id",
+		Imp: []openrtb2.Imp{{
+			ID:    "some-impression-id",
+			Video: &openrtb2.Video{},
+			Ext:   json.RawMessage(`{"appnexus": {"placementId": 10433394}}`),
+		}},
+		Ext: json.RawMessage(``),
+	}
+
+	var errList []error
+
+	sampleBid := &openrtb2.Bid{
+		ID:    "some-imp-id",
+		ImpID: "some-impression-id",
+		W:     300,
+		H:     250,
+		Ext:   nil,
+	}
+	aPbsOrtbBidArr := []*pbsOrtbBid{{bid: sampleBid, bidType: openrtb_ext.BidTypeVideo}}
+
+	adapterBids := map[openrtb_ext.BidderName]*pbsOrtbSeatBid{
+		openrtb_ext.BidderName("appnexus"): {
+			bids: aPbsOrtbBidArr,
+		},
+	}
+
+	impExtInfo := make(map[string]ImpExtInfo, 1)
+	impExtInfo["some-impression-id"] = ImpExtInfo{
+		true,
+		[]byte(`{"video":{"h":480,"mimes":["video/mp4"]}}`)}
+
+	expectedBidResponseExt := `{"prebid":{"type":"video"},"storedrequestattributes":{"h":480,"mimes":["video/mp4"]}}`
+
+	actualBidResp, err := e.buildBidResponse(context.Background(), liveAdapters, adapterBids, bidRequest, nil, nil, nil, true, impExtInfo, errList)
+	assert.NoError(t, err, fmt.Sprintf("imp ext info was not passed through correctly: %s", err))
+
+	resBidExt := string(actualBidResp.SeatBid[0].Bid[0].Ext)
+	assert.Equalf(t, expectedBidResponseExt, resBidExt, "Expected bid response extension is incorrect")
+
+}
+
 // TestRaceIntegration runs an integration test using all the sample params from
 // adapters/{bidder}/{bidder}test/params/race/*.json files.
 //
@@ -2359,10 +2411,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 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
-	bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, ""}
-	bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, nil, 0, false, ""}
-	bid1_4 := pbsOrtbBid{&bid4, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_1 := pbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_2 := pbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, ""}
+	bid1_3 := pbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, nil, 0, false, ""}
+	bid1_4 := pbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
 
 	innerBids := []*pbsOrtbBid{
 		&bid1_1,
@@ -2414,10 +2466,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 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
-	bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, ""}
-	bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, nil, 0, false, ""}
-	bid1_4 := pbsOrtbBid{&bid4, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, nil, 0, false, ""}
+	bid1_1 := pbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_2 := pbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, ""}
+	bid1_3 := pbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, nil, 0, false, ""}
+	bid1_4 := pbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, nil, 0, false, ""}
 
 	innerBids := []*pbsOrtbBid{
 		&bid1_1,
@@ -2468,9 +2520,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 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
-	bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, ""}
-	bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_1 := pbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_2 := pbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, ""}
+	bid1_3 := pbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
 
 	innerBids := []*pbsOrtbBid{
 		&bid1_1,
@@ -2550,9 +2602,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 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
-	bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, ""}
-	bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_1 := pbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_2 := pbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, ""}
+	bid1_3 := pbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
 
 	innerBids := []*pbsOrtbBid{
 		&bid1_1,
@@ -2602,11 +2654,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 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
-	bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, nil, 0, false, ""}
-	bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
-	bid1_4 := pbsOrtbBid{&bid4, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
-	bid1_5 := pbsOrtbBid{&bid5, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_1 := pbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_2 := pbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, nil, 0, false, ""}
+	bid1_3 := pbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_4 := pbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_5 := pbsOrtbBid{&bid5, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
 
 	selectedBids := make(map[string]int)
 	expectedCategories := map[string]string{
@@ -2681,11 +2733,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 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
-	bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
-	bid1_3 := pbsOrtbBid{&bid3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
-	bid1_4 := pbsOrtbBid{&bid4, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
-	bid1_5 := pbsOrtbBid{&bid5, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_1 := pbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_2 := pbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_3 := pbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_4 := pbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_5 := pbsOrtbBid{&bid5, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
 
 	selectedBids := make(map[string]int)
 	expectedCategories := map[string]string{
@@ -2761,8 +2813,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 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
-	bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_1 := pbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_2 := pbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
 
 	innerBids1 := []*pbsOrtbBid{
 		&bid1_1,
@@ -2815,8 +2867,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 := pbsOrtbBid{&bid1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
-	bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_1 := pbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_2 := pbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
 
 	innerBids1 := []*pbsOrtbBid{
 		&bid1_1,
@@ -2927,7 +2979,7 @@ func TestBidRejectionErrors(t *testing.T) {
 		innerBids := []*pbsOrtbBid{}
 		for _, bid := range test.bids {
 			currentBid := pbsOrtbBid{
-				bid, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: test.duration}, nil, 0, false, ""}
+				bid, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: test.duration}, nil, 0, false, ""}
 			innerBids = append(innerBids, &currentBid)
 		}
 
@@ -2975,8 +3027,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 := pbsOrtbBid{&bidApn1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
-	bid1_Apn2 := pbsOrtbBid{&bidApn2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_Apn1 := pbsOrtbBid{&bidApn1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_Apn2 := pbsOrtbBid{&bidApn2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
 
 	innerBidsApn1 := []*pbsOrtbBid{
 		&bid1_Apn1,
@@ -3055,11 +3107,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 := 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_1 := pbsOrtbBid{&bidApn1_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_Apn1_2 := pbsOrtbBid{&bidApn1_2, nil, "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, ""}
+	bid1_Apn2_1 := pbsOrtbBid{&bidApn2_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_Apn2_2 := pbsOrtbBid{&bidApn2_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
 
 	innerBidsApn1 := []*pbsOrtbBid{
 		&bid1_Apn1_1,
@@ -3137,9 +3189,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 := 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, ""}
+	bid1_Apn1_1 := pbsOrtbBid{&bidApn1_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_Apn1_2 := pbsOrtbBid{&bidApn1_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_Apn1_3 := pbsOrtbBid{&bidApn1_3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
 
 	type aTest struct {
 		desc      string
@@ -3264,7 +3316,7 @@ func TestApplyDealSupport(t *testing.T) {
 			},
 		}
 
-		bid := pbsOrtbBid{&openrtb2.Bid{ID: "123456"}, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, test.dealPriority, false, ""}
+		bid := pbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, test.dealPriority, false, ""}
 		bidCategory := map[string]string{
 			bid.bid.ID: test.targ["hb_pb_cat_dur"],
 		}
@@ -3431,7 +3483,7 @@ func TestUpdateHbPbCatDur(t *testing.T) {
 	}
 
 	for _, test := range testCases {
-		bid := pbsOrtbBid{&openrtb2.Bid{ID: "123456"}, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, test.dealPriority, false, ""}
+		bid := pbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, test.dealPriority, false, ""}
 		bidCategory := map[string]string{
 			bid.bid.ID: test.targ["hb_pb_cat_dur"],
 		}
@@ -3443,6 +3495,113 @@ func TestUpdateHbPbCatDur(t *testing.T) {
 	}
 }
 
+func TestMakeBidExtJSON(t *testing.T) {
+
+	type aTest struct {
+		description        string
+		ext                json.RawMessage
+		extBidPrebid       openrtb_ext.ExtBidPrebid
+		impExtInfo         map[string]ImpExtInfo
+		expectedBidExt     string
+		expectedErrMessage string
+	}
+
+	testCases := []aTest{
+		{
+			description:        "Valid extension, non empty extBidPrebid and valid imp ext info",
+			ext:                json.RawMessage(`{"video":{"h":100}}`),
+			extBidPrebid:       openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("video")},
+			impExtInfo:         map[string]ImpExtInfo{"test_imp_id": {true, []byte(`{"video":{"h":480,"mimes":["video/mp4"]}}`)}},
+			expectedBidExt:     `{"prebid":{"type":"video"},"storedrequestattributes":{"h":480,"mimes":["video/mp4"]},"video":{"h":100}}`,
+			expectedErrMessage: "",
+		},
+		{
+			description:        "Empty extension, non empty extBidPrebid and valid imp ext info",
+			ext:                nil,
+			extBidPrebid:       openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("video")},
+			impExtInfo:         map[string]ImpExtInfo{"test_imp_id": {true, []byte(`{"video":{"h":480,"mimes":["video/mp4"]}}`)}},
+			expectedBidExt:     `{"prebid":{"type":"video"},"storedrequestattributes":{"h":480,"mimes":["video/mp4"]}}`,
+			expectedErrMessage: "",
+		},
+		{
+			description:        "Valid extension, non empty extBidPrebid and imp ext info not found",
+			ext:                json.RawMessage(`{"video":{"h":100}}`),
+			extBidPrebid:       openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("video")},
+			impExtInfo:         map[string]ImpExtInfo{"another_imp_id": {true, []byte(`{"video":{"h":480,"mimes":["video/mp4"]}}`)}},
+			expectedBidExt:     `{"prebid":{"type":"video"},"video":{"h":100}}`,
+			expectedErrMessage: "",
+		},
+		{
+			description:        "Valid extension, empty extBidPrebid and valid imp ext info",
+			ext:                json.RawMessage(`{"video":{"h":100}}`),
+			extBidPrebid:       openrtb_ext.ExtBidPrebid{},
+			impExtInfo:         map[string]ImpExtInfo{"test_imp_id": {true, []byte(`{"video":{"h":480,"mimes":["video/mp4"]}}`)}},
+			expectedBidExt:     `{"prebid":{"type":""},"storedrequestattributes":{"h":480,"mimes":["video/mp4"]},"video":{"h":100}}`,
+			expectedErrMessage: "",
+		},
+		{
+			description:        "Valid extension, non empty extBidPrebid and empty imp ext info",
+			ext:                json.RawMessage(`{"video":{"h":100}}`),
+			extBidPrebid:       openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("video")},
+			impExtInfo:         nil,
+			expectedBidExt:     `{"prebid":{"type":"video"},"video":{"h":100}}`,
+			expectedErrMessage: "",
+		},
+		{
+			description:        "Valid extension, non empty extBidPrebid and valid imp ext info without video attr",
+			ext:                json.RawMessage(`{"video":{"h":100}}`),
+			extBidPrebid:       openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("video")},
+			impExtInfo:         map[string]ImpExtInfo{"test_imp_id": {true, []byte(`{"banner":{"h":480}}`)}},
+			expectedBidExt:     `{"prebid":{"type":"video"},"video":{"h":100}}`,
+			expectedErrMessage: "",
+		},
+		{
+			description:        "Valid extension with prebid, non empty extBidPrebid and valid imp ext info without video attr",
+			ext:                json.RawMessage(`{"prebid":{"targeting":100}}`),
+			extBidPrebid:       openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("video")},
+			impExtInfo:         map[string]ImpExtInfo{"test_imp_id": {true, []byte(`{"banner":{"h":480}}`)}},
+			expectedBidExt:     `{"prebid":{"type":"video"}}`,
+			expectedErrMessage: "",
+		},
+		{
+			description:        "Valid extension with prebid, non empty extBidPrebid and valid imp ext info with video attr",
+			ext:                json.RawMessage(`{"prebid":{"targeting":100}}`),
+			extBidPrebid:       openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("video")},
+			impExtInfo:         map[string]ImpExtInfo{"test_imp_id": {true, []byte(`{"video":{"h":480,"mimes":["video/mp4"]}}`)}},
+			expectedBidExt:     `{"prebid":{"type":"video"}, "storedrequestattributes":{"h":480,"mimes":["video/mp4"]}}`,
+			expectedErrMessage: "",
+		},
+		//Error cases
+		{
+			description:        "Invalid extension, valid extBidPrebid and valid imp ext info",
+			ext:                json.RawMessage(`{invalid json}`),
+			extBidPrebid:       openrtb_ext.ExtBidPrebid{Type: openrtb_ext.BidType("video")},
+			impExtInfo:         map[string]ImpExtInfo{"test_imp_id": {true, []byte(`{"video":{"h":480,"mimes":["video/mp4"]}}`)}},
+			expectedBidExt:     ``,
+			expectedErrMessage: "invalid character",
+		},
+		{
+			description:        "Valid extension, empty extBidPrebid and invalid imp ext info",
+			ext:                json.RawMessage(`{"video":{"h":100}}`),
+			extBidPrebid:       openrtb_ext.ExtBidPrebid{},
+			impExtInfo:         map[string]ImpExtInfo{"test_imp_id": {true, []byte(`{"video":{!}}`)}},
+			expectedBidExt:     ``,
+			expectedErrMessage: "invalid character",
+		},
+	}
+
+	for _, test := range testCases {
+		result, err := makeBidExtJSON(test.ext, &test.extBidPrebid, test.impExtInfo, "test_imp_id")
+
+		if test.expectedErrMessage == "" {
+			assert.JSONEq(t, test.expectedBidExt, string(result), "Incorrect result")
+			assert.NoError(t, err, "Error should not be returned")
+		} else {
+			assert.Contains(t, err.Error(), test.expectedErrMessage, "incorrect error message")
+		}
+	}
+}
+
 type exchangeSpec struct {
 	GDPREnabled       bool                   `json:"gdpr_enabled"`
 	IncomingRequest   exchangeRequest        `json:"incomingRequest"`
diff --git a/util/jsonutil/jsonutil_test.go b/util/jsonutil/jsonutil_test.go
index 0b6ec34c..d8738e17 100644
--- a/util/jsonutil/jsonutil_test.go
+++ b/util/jsonutil/jsonutil_test.go
@@ -7,7 +7,6 @@ import (
 )
 
 func TestDropElement(t *testing.T) {
-
 	tests := []struct {
 		description     string
 		input           []byte
@@ -17,7 +16,7 @@ func TestDropElement(t *testing.T) {
 		errorContains   string
 	}{
 		{
-			description:     "Drop Single Element After Another Element",
+			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}}`),
@@ -25,7 +24,7 @@ func TestDropElement(t *testing.T) {
 			errorContains:   "",
 		},
 		{
-			description:     "Drop Single Element Before Another Element",
+			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}}`),
@@ -33,7 +32,7 @@ func TestDropElement(t *testing.T) {
 			errorContains:   "",
 		},
 		{
-			description:     "Drop Single Element",
+			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": {}}`),
@@ -41,7 +40,7 @@ func TestDropElement(t *testing.T) {
 			errorContains:   "",
 		},
 		{
-			description:     "Drop Single Element string",
+			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": {}}`),
@@ -49,7 +48,7 @@ func TestDropElement(t *testing.T) {
 			errorContains:   "",
 		},
 		{
-			description:     "Drop Parent Element Between Two Elements",
+			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}`),
@@ -57,7 +56,7 @@ func TestDropElement(t *testing.T) {
 			errorContains:   "",
 		},
 		{
-			description:     "Drop Parent Element Before Element",
+			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}`),
@@ -65,7 +64,7 @@ func TestDropElement(t *testing.T) {
 			errorContains:   "",
 		},
 		{
-			description:     "Drop Parent Element After Element",
+			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"}`),
@@ -73,7 +72,7 @@ func TestDropElement(t *testing.T) {
 			errorContains:   "",
 		},
 		{
-			description:     "Drop Parent Element Only",
+			description:     "Drop parent element only",
 			input:           []byte(`{"consented_providers_settings": {"consented_providers": [1608,765,492], "test": 1}}`),
 			elementToRemove: "consented_providers_settings",
 			output:          []byte(`{}`),
@@ -81,7 +80,7 @@ func TestDropElement(t *testing.T) {
 			errorContains:   "",
 		},
 		{
-			description:     "Drop Element That Doesn't Exist",
+			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}}`),
@@ -90,7 +89,7 @@ func TestDropElement(t *testing.T) {
 		},
 		//Errors
 		{
-			description:     "Error Decode",
+			description:     "Error decode",
 			input:           []byte(`{"consented_providers_settings": {"consented_providers": ["123",1,,1365,5678,1545,2563,1411], "test": 1}}`),
 			elementToRemove: "consented_providers",
 			output:          []byte(``),
@@ -98,7 +97,7 @@ func TestDropElement(t *testing.T) {
 			errorContains:   "looking for beginning of value",
 		},
 		{
-			description:     "Error Malformed",
+			description:     "Error malformed",
 			input:           []byte(`{consented_providers_settings: {"consented_providers": [1365,5678,1545,2563,1411], "test": 1}}`),
 			elementToRemove: "consented_providers",
 			output:          []byte(``),
@@ -120,3 +119,188 @@ func TestDropElement(t *testing.T) {
 
 	}
 }
+
+func TestFindElement(t *testing.T) {
+	tests := []struct {
+		description   string
+		input         []byte
+		elementToFind string
+		output        []byte
+		elementFound  bool
+	}{
+		{
+			description:   "Find array element",
+			input:         []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"test": 1,"consented_providers":[1608,765,492]}}`),
+			elementToFind: "consented_providers",
+			output:        []byte(`[1608,765,492]`),
+			elementFound:  true,
+		},
+		{
+			description:   "Find object element",
+			input:         []byte(`{"consent": "TESTCONSENT","consented_providers_settings":{"test": 1,"consented_providers": [1608,765,492]}}`),
+			elementToFind: "consented_providers_settings",
+			output:        []byte(`{"test": 1,"consented_providers": [1608,765,492]}`),
+			elementFound:  true,
+		},
+		{
+			description:   "Find element that doesn't exist",
+			input:         []byte(`{"consent": "TESTCONSENT","consented_providers_settings":{"test": 1,"consented_providers": [1608,765,492]}}`),
+			elementToFind: "test_element",
+			output:        []byte(nil),
+			elementFound:  false,
+		},
+	}
+
+	for _, tt := range tests {
+		exists, res, err := FindElement(tt.input, tt.elementToFind)
+		assert.NoError(t, err, "Error should be nil")
+		assert.Equal(t, tt.output, res, "Result is incorrect")
+
+		if tt.elementFound {
+			assert.True(t, exists, "Element must be found")
+		} else {
+			assert.False(t, exists, "Element must not be found")
+		}
+	}
+}
+
+func TestFindElementIndexes(t *testing.T) {
+	tests := []struct {
+		description   string
+		input         []byte
+		elementToFind string
+		startIndex    int64
+		endIndex      int64
+		found         bool
+		errorExpected bool
+		errorContains string
+	}{
+		{
+			description:   "Find single element after another element",
+			input:         []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"test": 1,"consented_providers": [1608,765,492]}}`),
+			elementToFind: "consented_providers",
+			startIndex:    68,
+			endIndex:      106,
+			found:         true,
+			errorExpected: false,
+			errorContains: "",
+		},
+		{
+			description:   "Find single element before another element",
+			input:         []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"consented_providers": [1608,765,492],"test": 1}}`),
+			elementToFind: "consented_providers",
+			startIndex:    59,
+			endIndex:      97,
+			found:         true,
+			errorExpected: false,
+			errorContains: "",
+		},
+		{
+			description:   "Find single element",
+			input:         []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"consented_providers": [1545,2563,1411]}}`),
+			elementToFind: "consented_providers",
+			startIndex:    59,
+			endIndex:      98,
+			found:         true,
+			errorExpected: false,
+			errorContains: "",
+		},
+		{
+			description:   "Find single element string",
+			input:         []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"consented_providers": "test"}}`),
+			elementToFind: "consented_providers",
+			startIndex:    59,
+			endIndex:      88,
+			found:         true,
+			errorExpected: false,
+			errorContains: "",
+		},
+		{
+			description:   "Find parent element between two elements",
+			input:         []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"consented_providers": [1608,765,492], "test": 1},"test": 123}`),
+			elementToFind: "consented_providers_settings",
+			startIndex:    26,
+			endIndex:      109,
+			found:         true,
+			errorExpected: false,
+			errorContains: "",
+		},
+		{
+			description:   "Find parent element before element",
+			input:         []byte(`{"consented_providers_settings": {"consented_providers": [1608,765,492], "test": 1},"test": 123}`),
+			elementToFind: "consented_providers_settings",
+			startIndex:    1,
+			endIndex:      84,
+			found:         true,
+			errorExpected: false,
+			errorContains: "",
+		},
+		{
+			description:   "Find parent element after element",
+			input:         []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"consented_providers": [1608,765,492], "test": 1}}`),
+			elementToFind: "consented_providers_settings",
+			startIndex:    25,
+			endIndex:      108,
+			found:         true,
+			errorExpected: false,
+			errorContains: "",
+		},
+		{
+			description:   "Find parent element only",
+			input:         []byte(`{"consented_providers_settings": {"consented_providers": [1608,765,492], "test": 1}}`),
+			elementToFind: "consented_providers_settings",
+			startIndex:    1,
+			endIndex:      83,
+			found:         true,
+			errorExpected: false,
+			errorContains: "",
+		},
+		{
+			description:   "Find element that doesn't exist",
+			input:         []byte(`{"consented_providers_settings": {"consented_providers": [1608,765,492], "test": 1}}`),
+			elementToFind: "test2",
+			startIndex:    -1,
+			endIndex:      -1,
+			found:         false,
+			errorExpected: false,
+			errorContains: "",
+		},
+		//Errors
+		{
+			description:   "Error decode",
+			input:         []byte(`{"consented_providers_settings": {"consented_providers": ["123",1,,1365,5678,1545,2563,1411], "test": 1}}`),
+			elementToFind: "consented_providers",
+			startIndex:    -1,
+			endIndex:      -1,
+			found:         false,
+			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}}`),
+			elementToFind: "consented_providers",
+			startIndex:    -1,
+			endIndex:      -1,
+			found:         false,
+			errorExpected: true,
+			errorContains: "invalid character",
+		},
+	}
+
+	for _, tt := range tests {
+		found, start, end, err := findElementIndexes(tt.input, tt.elementToFind)
+
+		assert.Equal(t, tt.found, found, "Incorrect value of element existence")
+
+		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.startIndex, start, "Result is incorrect")
+			assert.Equal(t, tt.endIndex, end, "Result is incorrect")
+		}
+
+	}
+}
diff --git a/util/maputil/maputil_test.go b/util/maputil/maputil_test.go
index 2e6955ce..abce102f 100644
--- a/util/maputil/maputil_test.go
+++ b/util/maputil/maputil_test.go
@@ -111,3 +111,56 @@ func TestReadEmbeddedSlice(t *testing.T) {
 		assert.Equal(t, test.expectedOK, resultOK, test.description+":ok")
 	}
 }
+
+func TestReadEmbeddedString(t *testing.T) {
+	testCases := []struct {
+		description    string
+		value          map[string]interface{}
+		key            string
+		expectedString string
+		expectedOK     bool
+	}{
+		{
+			description:    "Nil",
+			value:          nil,
+			key:            "",
+			expectedString: "",
+			expectedOK:     false,
+		},
+		{
+			description:    "Empty",
+			value:          map[string]interface{}{},
+			key:            "foo",
+			expectedString: "",
+			expectedOK:     false,
+		},
+		{
+			description:    "Success",
+			value:          map[string]interface{}{"foo": "stringValue"},
+			key:            "foo",
+			expectedString: "stringValue",
+			expectedOK:     true,
+		},
+		{
+			description:    "Not Found",
+			value:          map[string]interface{}{"foo": "stringValue"},
+			key:            "notFound",
+			expectedString: "",
+			expectedOK:     false,
+		},
+		{
+			description:    "Wrong Type",
+			value:          map[string]interface{}{"foo": []interface{}{42}},
+			key:            "foo",
+			expectedString: "",
+			expectedOK:     false,
+		},
+	}
+
+	for _, test := range testCases {
+		resultString, resultOK := ReadEmbeddedString(test.value, test.key)
+
+		assert.Equal(t, test.expectedString, resultString, test.description+":string")
+		assert.Equal(t, test.expectedOK, resultOK, test.description+":ok")
+	}
+}