Repository: prebid/prebid-server
Pull request #1529 (2020-10-08 21:39:52)

Emxd 3336 add app video ctv
Adding video support to EMX Digital adapter

EMXDigital: Juts following up here, the checks seem to be failing due to an independent/non-related part of the package `pubstack_module_test`

` FAIL: TestPubstackModuleSuccess (0.17s)
    pubstack_module_test.go:223: 
        	Error Trace:	pubstack_module_test.go:223
        	            				pubstack_module_test.go:205
        	Error:      	Should receive an event, but did NOT
        	Test:       	TestPubstackModuleSuccess
        	Messages:   	setUID events are only published when logging a setUID object with setUID feature on
FAIL`

EMX Digital adapter looks to be okay and has > 90% testing coverage
bsardo: I'll take a look at that test failure since I recently worked on that test.
EMXDigital: @bsardo looks like all the checks are passing now 👍
EMXDigital: @SyntaxNode Updated code based on feedback and discussion with team. Ready for another round

Patch:
diff --git a/adapters/emx_digital/emx_digital.go b/adapters/emx_digital/emx_digital.go
index ccaa96e6..4bdb25fd 100644
--- a/adapters/emx_digital/emx_digital.go
+++ b/adapters/emx_digital/emx_digital.go
@@ -6,6 +6,7 @@ import (
 	"net/http"
 	"net/url"
 	"strconv"
+	"strings"
 	"time"
 
 	"github.com/mxmCherry/openrtb"
@@ -110,7 +111,6 @@ func unpackImpExt(imp *openrtb.Imp) (*openrtb_ext.ExtImpEmxDigital, error) {
 }
 
 func buildImpBanner(imp *openrtb.Imp) error {
-	imp.Ext = nil
 
 	if imp.Banner == nil {
 		return &errortypes.BadInput{
@@ -137,6 +137,40 @@ func buildImpBanner(imp *openrtb.Imp) error {
 	return nil
 }
 
+func buildImpVideo(imp *openrtb.Imp) error {
+
+	if len(imp.Video.MIMEs) == 0 {
+		return &errortypes.BadInput{
+			Message: fmt.Sprintf("Video: missing required field mimes"),
+		}
+	}
+
+	if imp.Video.H == 0 && imp.Video.W == 0 {
+		return &errortypes.BadInput{
+			Message: fmt.Sprintf("Video: Need at least one size to build request"),
+		}
+	}
+
+	if imp.Video.Protocols != nil {
+		imp.Video.Protocols = cleanProtocol(imp.Video.Protocols)
+	}
+
+	return nil
+}
+
+// not supporting VAST protocol 7 (VAST 4.0);
+func cleanProtocol(protocols []openrtb.Protocol) []openrtb.Protocol {
+	newitems := make([]openrtb.Protocol, 0, len(protocols))
+
+	for _, i := range protocols {
+		if i != openrtb.ProtocolVAST40 {
+			newitems = append(newitems, i)
+		}
+	}
+
+	return newitems
+}
+
 // Add EMX required properties to Imp object
 func addImpProps(imp *openrtb.Imp, secure *int8, emxExt *openrtb_ext.ExtImpEmxDigital) {
 	imp.TagID = emxExt.TagID
@@ -170,14 +204,22 @@ func preprocess(request *openrtb.BidRequest) []error {
 	errors := make([]error, 0, impsCount)
 	resImps := make([]openrtb.Imp, 0, impsCount)
 	secure := int8(0)
-
+	domain := ""
 	if request.Site != nil && request.Site.Page != "" {
-		pageURL, err := url.Parse(request.Site.Page)
-		if err == nil && pageURL.Scheme == "https" {
-			secure = int8(1)
+		domain = request.Site.Page
+	} else if request.App != nil {
+		if request.App.Domain != "" {
+			domain = request.App.Domain
+		} else if request.App.StoreURL != "" {
+			domain = request.App.StoreURL
 		}
 	}
 
+	pageURL, err := url.Parse(domain)
+	if err == nil && pageURL.Scheme == "https" {
+		secure = int8(1)
+	}
+
 	for _, imp := range request.Imp {
 		emxExt, err := unpackImpExt(&imp)
 		if err != nil {
@@ -187,10 +229,17 @@ func preprocess(request *openrtb.BidRequest) []error {
 
 		addImpProps(&imp, &secure, emxExt)
 
-		if err := buildImpBanner(&imp); err != nil {
+		if imp.Video != nil {
+			if err := buildImpVideo(&imp); err != nil {
+				errors = append(errors, err)
+				continue
+			}
+		} else if err := buildImpBanner(&imp); err != nil {
 			errors = append(errors, err)
 			continue
+
 		}
+
 		resImps = append(resImps, imp)
 	}
 
@@ -229,7 +278,7 @@ func (a *EmxDigitalAdapter) MakeBids(internalRequest *openrtb.BidRequest, extern
 
 			bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
 				Bid:     &sb.Bid[i],
-				BidType: "banner",
+				BidType: getBidType(sb.Bid[i].AdM),
 			})
 		}
 	}
@@ -238,6 +287,24 @@ func (a *EmxDigitalAdapter) MakeBids(internalRequest *openrtb.BidRequest, extern
 
 }
 
+func getBidType(bidAdm string) openrtb_ext.BidType {
+	if bidAdm != "" && ContainsAny(bidAdm, []string{"<?xml", "<vast"}) {
+		return openrtb_ext.BidTypeVideo
+	}
+	return openrtb_ext.BidTypeBanner
+}
+
+func ContainsAny(raw string, keys []string) bool {
+	lowerCased := strings.ToLower(raw)
+	for i := 0; i < len(keys); i++ {
+		if strings.Contains(lowerCased, keys[i]) {
+			return true
+		}
+	}
+	return false
+
+}
+
 func NewEmxDigitalBidder(endpoint string) *EmxDigitalAdapter {
 	return &EmxDigitalAdapter{
 		endpoint: endpoint,
diff --git a/config/config.go b/config/config.go
index a50f127e..218ffca3 100755
--- a/config/config.go
+++ b/config/config.go
@@ -954,7 +954,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.eplanning.endpoint", "http://rtb.e-planning.net/pbs/1")
 	v.SetDefault("adapters.gamma.endpoint", "https://hb.gammaplatform.com/adx/request/")
 	v.SetDefault("adapters.gamoshi.endpoint", "https://rtb.gamoshi.io")
-	v.SetDefault("adapters.grid.endpoint", "http://grid.bidswitch.net/sp_bid?sp=prebid")
+	v.SetDefault("adapters.grid.endpoint", "https://grid.bidswitch.net/sp_bid?sp=prebid")
 	v.SetDefault("adapters.gumgum.endpoint", "https://g2.gumgum.com/providers/prbds2s/bid")
 	v.SetDefault("adapters.improvedigital.endpoint", "http://ad.360yield.com/pbs")
 	v.SetDefault("adapters.inmobi.endpoint", "https://api.w.inmobi.com/showad/openrtb/bidder/prebid")
diff --git a/config/stored_requests.go b/config/stored_requests.go
index 61db7eb0..24956946 100644
--- a/config/stored_requests.go
+++ b/config/stored_requests.go
@@ -22,20 +22,31 @@ const (
 )
 
 // Section returns the config section this type is defined in
-func (sr *StoredRequests) Section() string {
+func (dataType DataType) Section() string {
 	return map[DataType]string{
 		RequestDataType:    "stored_requests",
 		CategoryDataType:   "categories",
 		VideoDataType:      "stored_video_req",
 		AMPRequestDataType: "stored_amp_req",
 		AccountDataType:    "accounts",
-	}[sr.dataType]
+	}[dataType]
+}
+
+// Section returns the config section
+func (sr *StoredRequests) Section() string {
+	return sr.dataType.Section()
 }
 
+// DataType returns the DataType associated with this config
 func (sr *StoredRequests) DataType() DataType {
 	return sr.dataType
 }
 
+// SetDataType sets the DataType on this config. Needed for tests.
+func (sr *StoredRequests) SetDataType(dataType DataType) {
+	sr.dataType = dataType
+}
+
 // StoredRequests struct defines options for stored requests for each data type
 // including some amp stored_requests options
 type StoredRequests struct {
@@ -132,7 +143,7 @@ func (cfg *StoredRequests) validate(errs configErrors) configErrors {
 	if cfg.DataType() == AccountDataType && cfg.Postgres.ConnectionInfo.Database != "" {
 		errs = append(errs, fmt.Errorf("%s.postgres: retrieving accounts via postgres not available, use accounts.files", cfg.Section()))
 	} else {
-		errs = cfg.Postgres.validate(cfg.Section(), errs)
+		errs = cfg.Postgres.validate(cfg.DataType(), errs)
 	}
 
 	// Categories do not use cache so none of the following checks apply
@@ -156,7 +167,7 @@ func (cfg *StoredRequests) validate(errs configErrors) configErrors {
 			errs = append(errs, fmt.Errorf("%s: postgres.initialize_caches.query must be empty if in_memory_cache=none", cfg.Section()))
 		}
 	}
-	errs = cfg.InMemoryCache.validate(cfg.Section(), errs)
+	errs = cfg.InMemoryCache.validate(cfg.DataType(), errs)
 	return errs
 }
 
@@ -169,12 +180,12 @@ type PostgresConfig struct {
 	PollUpdates         PostgresUpdatePolling    `mapstructure:"poll_for_updates"`
 }
 
-func (cfg *PostgresConfig) validate(section string, errs configErrors) configErrors {
+func (cfg *PostgresConfig) validate(dataType DataType, errs configErrors) configErrors {
 	if cfg.ConnectionInfo.Database == "" {
 		return errs
 	}
 
-	return cfg.PollUpdates.validate(section, errs)
+	return cfg.PollUpdates.validate(dataType, errs)
 }
 
 // PostgresConnection has options which put types to the Postgres Connection string. See:
@@ -269,7 +280,8 @@ type PostgresCacheInitializer struct {
 	AmpQuery string `mapstructure:"amp_query"`
 }
 
-func (cfg *PostgresCacheInitializer) validate(section string, errs configErrors) configErrors {
+func (cfg *PostgresCacheInitializer) validate(dataType DataType, errs configErrors) configErrors {
+	section := dataType.Section()
 	if cfg.Query == "" {
 		return errs
 	}
@@ -305,7 +317,8 @@ type PostgresUpdatePolling struct {
 	AmpQuery string `mapstructure:"amp_query"`
 }
 
-func (cfg *PostgresUpdatePolling) validate(section string, errs configErrors) configErrors {
+func (cfg *PostgresUpdatePolling) validate(dataType DataType, errs configErrors) configErrors {
+	section := dataType.Section()
 	if cfg.Query == "" {
 		return errs
 	}
@@ -384,32 +397,57 @@ type InMemoryCache struct {
 	// TTL is the maximum number of seconds that an unused value will stay in the cache.
 	// TTL <= 0 can be used for "no ttl". Elements will still be evicted based on the Size.
 	TTL int `mapstructure:"ttl_seconds"`
+	// Size is the max total cache size allowed for single caches
+	Size int `mapstructure:"size_bytes"`
 	// RequestCacheSize is the max number of bytes allowed in the cache for Stored Requests. Values <= 0 will have no limit
 	RequestCacheSize int `mapstructure:"request_cache_size_bytes"`
 	// ImpCacheSize is the max number of bytes allowed in the cache for Stored Imps. Values <= 0 will have no limit
 	ImpCacheSize int `mapstructure:"imp_cache_size_bytes"`
 }
 
-func (cfg *InMemoryCache) validate(section string, errs configErrors) configErrors {
+func (cfg *InMemoryCache) validate(dataType DataType, errs configErrors) configErrors {
+	section := dataType.Section()
 	switch cfg.Type {
 	case "none":
 		// No errors for no config options
 	case "unbounded":
 		if cfg.TTL != 0 {
-			errs = append(errs, fmt.Errorf("%s: in_memory_cache must be 0 for unbounded caches. Got %d", section, cfg.TTL))
+			errs = append(errs, fmt.Errorf("%s: in_memory_cache.ttl_seconds is not supported for unbounded caches. Got %d", section, cfg.TTL))
 		}
-		if cfg.RequestCacheSize != 0 {
-			errs = append(errs, fmt.Errorf("%s: in_memory_cache.request_cache_size_bytes must be 0 for unbounded caches. Got %d", section, cfg.RequestCacheSize))
-		}
-		if cfg.ImpCacheSize != 0 {
-			errs = append(errs, fmt.Errorf("%s: in_memory_cache.imp_cache_size_bytes must be 0 for unbounded caches. Got %d", section, cfg.ImpCacheSize))
+		if dataType == AccountDataType {
+			// single cache
+			if cfg.Size != 0 {
+				errs = append(errs, fmt.Errorf("%s: in_memory_cache.size_bytes is not supported for unbounded caches. Got %d", section, cfg.Size))
+			}
+		} else {
+			// dual (request and imp) caches
+			if cfg.RequestCacheSize != 0 {
+				errs = append(errs, fmt.Errorf("%s: in_memory_cache.request_cache_size_bytes is not supported for unbounded caches. Got %d", section, cfg.RequestCacheSize))
+			}
+			if cfg.ImpCacheSize != 0 {
+				errs = append(errs, fmt.Errorf("%s: in_memory_cache.imp_cache_size_bytes is not supported for unbounded caches. Got %d", section, cfg.ImpCacheSize))
+			}
 		}
 	case "lru":
-		if cfg.RequestCacheSize <= 0 {
-			errs = append(errs, fmt.Errorf("%s: in_memory_cache.request_cache_size_bytes must be >= 0 when in_memory_cache.type=lru. Got %d", section, cfg.RequestCacheSize))
-		}
-		if cfg.ImpCacheSize <= 0 {
-			errs = append(errs, fmt.Errorf("%s: in_memory_cache.imp_cache_size_bytes must be >= 0 when in_memory_cache.type=lru. Got %d", section, cfg.ImpCacheSize))
+		if dataType == AccountDataType {
+			// single cache
+			if cfg.Size <= 0 {
+				errs = append(errs, fmt.Errorf("%s: in_memory_cache.size_bytes must be >= 0 when in_memory_cache.type=lru. Got %d", section, cfg.Size))
+			}
+			if cfg.RequestCacheSize > 0 || cfg.ImpCacheSize > 0 {
+				glog.Warningf("%s: in_memory_cache.request_cache_size_bytes and imp_cache_size_bytes do not apply to this section and will be ignored", section)
+			}
+		} else {
+			// dual (request and imp) caches
+			if cfg.RequestCacheSize <= 0 {
+				errs = append(errs, fmt.Errorf("%s: in_memory_cache.request_cache_size_bytes must be >= 0 when in_memory_cache.type=lru. Got %d", section, cfg.RequestCacheSize))
+			}
+			if cfg.ImpCacheSize <= 0 {
+				errs = append(errs, fmt.Errorf("%s: in_memory_cache.imp_cache_size_bytes must be >= 0 when in_memory_cache.type=lru. Got %d", section, cfg.ImpCacheSize))
+			}
+			if cfg.Size > 0 {
+				glog.Warningf("%s: in_memory_cache.size_bytes does not apply in this section and will be ignored", section)
+			}
 		}
 	default:
 		errs = append(errs, fmt.Errorf("%s: in_memory_cache.type %s is invalid", section, cfg.Type))
diff --git a/endpoints/auction.go b/endpoints/auction.go
index c6fd5712..0be77297 100644
--- a/endpoints/auction.go
+++ b/endpoints/auction.go
@@ -310,10 +310,7 @@ func sortBidsAddKeywordsMobile(bids pbs.PBSBidSlice, pbs_req *pbs.PBSRequest, pr
 		// after sorting we need to add the ad targeting keywords
 		for i, bid := range bar {
 			// We should eventually check for the error and do something.
-			roundedCpm, err := exchange.GetCpmStringValue(bid.Price, openrtb_ext.PriceGranularityFromString(priceGranularitySetting))
-			if err != nil {
-				glog.Error(err.Error())
-			}
+			roundedCpm := exchange.GetPriceBucket(bid.Price, openrtb_ext.PriceGranularityFromString(priceGranularitySetting))
 
 			hbSize := ""
 			if bid.Width != 0 && bid.Height != 0 {
diff --git a/endpoints/openrtb2/sample-requests/video/video_valid_sample_appendbiddernames.json b/endpoints/openrtb2/sample-requests/video/video_valid_sample_appendbiddernames.json
new file mode 100644
index 00000000..4850fe91
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/video/video_valid_sample_appendbiddernames.json
@@ -0,0 +1,86 @@
+{
+  "description": "Video endpoint valid request with AppendBidderNames.",
+  "requestPayload": {
+    "appendbiddernames": true,
+    "storedrequestid": "80ce30c53c16e6ede735f123ef6e32361bfc7b22",
+    "podconfig": {
+      "durationrangesec": [
+        30
+      ],
+      "requireexactduration": true,
+      "pods": [{
+          "podid": 1,
+          "adpoddurationsec": 180,
+          "configid": "fba10607-0c12-43d1-ad07-b8a513bc75d6"
+        },
+        {
+          "podid": 2,
+          "adpoddurationsec": 150,
+          "configid": "8b452b41-2681-4a20-9086-6f16ffad7773"
+        }
+      ]
+    },
+    "site": {
+      "page": "prebid.com"
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 0
+      }
+    },
+    "user": {
+      "yob": 1991,
+      "gender": "F",
+      "keywords": "Hotels, Travelling",
+      "ext": {
+        "prebid": {
+          "buyeruids": {
+            "appnexus": "unique_id_an",
+            "rubicon": "unique_id_rubi"
+          }
+        }
+      }
+    },
+    "device": {
+      "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2",
+      "ip": "123.145.167.10",
+      "devicetype": 1,
+      "ifa": "AA000DFE74168477C70D291f574D344790E0BB11",
+      "lmt": 44,
+      "os": "mac os",
+      "w": 640,
+      "h": 480,
+      "didsha1": "didsha1",
+      "didmd5": "didmd5",
+      "dpidsha1": "dpidsha1",
+      "dpidmd5": "dpidmd5",
+      "macsha1": "macsha1",
+      "macmd5": "macmd5"
+    },
+    "includebrandcategory": {
+      "primaryadserver": 1,
+      "publisher": ""
+    },
+    "video": {
+      "w": 640,
+      "h": 480,
+      "mimes": [
+        "video/mp4"
+      ],
+      "protocols": [
+        2, 3, 5, 6
+      ]
+    },
+    "content": {
+      "episode": 6,
+      "title": "episodeName",
+      "series": "TvName",
+      "season": "season3",
+      "len": 900,
+      "livestream": 0
+    },
+    "cacheconfig": {
+      "ttl": 42
+    }
+  }
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go
index ab5634c7..e277e362 100644
--- a/endpoints/openrtb2/video_auction.go
+++ b/endpoints/openrtb2/video_auction.go
@@ -624,6 +624,7 @@ func createBidExtension(videoRequest *openrtb_ext.BidRequestVideo) ([]byte, erro
 		IncludeBrandCategory: inclBrandCat,
 		DurationRangeSec:     durationRangeSec,
 		IncludeBidderKeys:    true,
+		AppendBidderNames:    videoRequest.AppendBidderNames,
 	}
 
 	vastXml := openrtb_ext.ExtRequestPrebidCacheVAST{}
diff --git a/exchange/auction.go b/exchange/auction.go
index aa446ddb..aa36feb2 100644
--- a/exchange/auction.go
+++ b/exchange/auction.go
@@ -11,7 +11,6 @@ import (
 	"time"
 
 	uuid "github.com/gofrs/uuid"
-	"github.com/golang/glog"
 	"github.com/mxmCherry/openrtb"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/openrtb_ext"
@@ -94,7 +93,7 @@ func (d *DebugLog) PutDebugLogError(cache prebid_cache_client.Client, timeout in
 	return nil
 }
 
-func newAuction(seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, numImps int) *auction {
+func newAuction(seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, numImps int, preferDeals bool) *auction {
 	winningBids := make(map[string]*pbsOrtbBid, numImps)
 	winningBidsByBidder := make(map[string]map[openrtb_ext.BidderName]*pbsOrtbBid, numImps)
 
@@ -103,7 +102,7 @@ func newAuction(seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, numImps int
 			for _, bid := range seatBid.bids {
 				cpm := bid.bid.Price
 				wbid, ok := winningBids[bid.bid.ImpID]
-				if !ok || cpm > wbid.bid.Price {
+				if !ok || isNewWinningBid(bid.bid, wbid.bid, preferDeals) {
 					winningBids[bid.bid.ImpID] = bid
 				}
 				if bidMap, ok := winningBidsByBidder[bid.bid.ImpID]; ok {
@@ -125,15 +124,24 @@ func newAuction(seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, numImps int
 	}
 }
 
+// isNewWinningBid calculates if the new bid (nbid) will win against the current winning bid (wbid) given preferDeals.
+func isNewWinningBid(bid, wbid *openrtb.Bid, preferDeals bool) bool {
+	if preferDeals {
+		if len(wbid.DealID) > 0 && len(bid.DealID) == 0 {
+			return false
+		}
+		if len(wbid.DealID) == 0 && len(bid.DealID) > 0 {
+			return true
+		}
+	}
+	return bid.Price > wbid.Price
+}
+
 func (a *auction) setRoundedPrices(priceGranularity openrtb_ext.PriceGranularity) {
 	roundedPrices := make(map[*pbsOrtbBid]string, 5*len(a.winningBids))
 	for _, topBidsPerImp := range a.winningBidsByBidder {
 		for _, topBidPerBidder := range topBidsPerImp {
-			roundedPrice, err := GetCpmStringValue(topBidPerBidder.bid.Price, priceGranularity)
-			if err != nil {
-				glog.Errorf(`Error rounding price according to granularity. This shouldn't happen unless /openrtb2 input validation is buggy. Granularity was "%v".`, priceGranularity)
-			}
-			roundedPrices[topBidPerBidder] = roundedPrice
+			roundedPrices[topBidPerBidder] = GetPriceBucket(topBidPerBidder.bid.Price, priceGranularity)
 		}
 	}
 	a.roundedPrices = roundedPrices
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 62fb9a2c..5c424769 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -96,6 +96,7 @@ func NewExchange(client *http.Client, cache prebid_cache_client.Client, cfg *con
 
 func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, usersyncs IdFetcher, labels pbsmetrics.Labels, account *config.Account, categoriesFetcher *stored_requests.CategoryFetcher, debugLog *DebugLog) (*openrtb.BidResponse, error) {
 
+	var err error
 	requestExt, err := extractBidRequestExt(bidRequest)
 	if err != nil {
 		return nil, err
@@ -114,34 +115,11 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque
 
 	bidAdjustmentFactors := getExtBidAdjustmentFactors(requestExt)
 
-	for _, impInRequest := range bidRequest.Imp {
-		var impLabels pbsmetrics.ImpLabels = pbsmetrics.ImpLabels{
-			BannerImps: impInRequest.Banner != nil,
-			VideoImps:  impInRequest.Video != nil,
-			AudioImps:  impInRequest.Audio != nil,
-			NativeImps: impInRequest.Native != nil,
-		}
-		e.me.RecordImps(impLabels)
-	}
+	recordImpMetrics(bidRequest, e.me)
 
 	// Make our best guess if GDPR applies
-	usersyncIfAmbiguous := e.UsersyncIfAmbiguous
-	var geo *openrtb.Geo = nil
-	if bidRequest.User != nil && bidRequest.User.Geo != nil {
-		geo = bidRequest.User.Geo
-	} else if bidRequest.Device != nil && bidRequest.Device.Geo != nil {
-		geo = bidRequest.Device.Geo
-	}
-	if geo != nil {
-		// If we have a country set, and it is on the list, we assume GDPR applies if not set on the request.
-		// Otherwise we assume it does not apply as long as it appears "valid" (is 3 characters long).
-		if _, found := e.privacyConfig.GDPR.EEACountriesMap[strings.ToUpper(geo.Country)]; found {
-			usersyncIfAmbiguous = false
-		} else if len(geo.Country) == 3 {
-			// The country field is formatted properly as a three character country code
-			usersyncIfAmbiguous = true
-		}
-	}
+	usersyncIfAmbiguous := e.parseUsersyncIfAmbiguous(bidRequest)
+
 	// Slice of BidRequests, each a copy of the original cleaned to only contain bidder data for the named bidder
 	blabels := make(map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels)
 	cleanRequests, aliases, privacyLabels, errs := cleanOpenRTBRequests(ctx, bidRequest, requestExt, usersyncs, blabels, labels, e.gDPR, usersyncIfAmbiguous, e.privacyConfig)
@@ -161,14 +139,13 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque
 
 	adapterBids, adapterExtra, anyBidsReturned := e.getAllBids(auctionCtx, cleanRequests, aliases, bidAdjustmentFactors, blabels, conversions)
 
-	var auc *auction = nil
-	var bidResponseExt *openrtb_ext.ExtBidResponse = nil
+	var auc *auction
+	var cacheErrs []error
 	if anyBidsReturned {
 
 		var bidCategory map[string]string
 		//If includebrandcategory is present in ext then CE feature is on.
 		if requestExt.Prebid.Targeting != nil && requestExt.Prebid.Targeting.IncludeBrandCategory != nil {
-			var err error
 			var rejections []string
 			bidCategory, adapterBids, rejections, err = applyCategoryMapping(ctx, requestExt, adapterBids, *categoriesFetcher, targData)
 			if err != nil {
@@ -179,9 +156,9 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque
 			}
 		}
 
-		auc = newAuction(adapterBids, len(bidRequest.Imp))
-
 		if targData != nil {
+			// A non-nil auction is only needed if targeting is active. (It is used below this block to extract cache keys)
+			auc = newAuction(adapterBids, len(bidRequest.Imp), targData.preferDeals)
 			auc.setRoundedPrices(targData.priceGranularity)
 
 			if requestExt.Prebid.SupportDeals {
@@ -189,42 +166,33 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque
 				errs = append(errs, dealErrs...)
 			}
 
-			if debugLog != nil && debugLog.Enabled {
-				bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, bidRequest, debugInfo, errs)
-				if bidRespExtBytes, err := json.Marshal(bidResponseExt); err == nil {
-					debugLog.Data.Response = string(bidRespExtBytes)
-				} else {
-					debugLog.Data.Response = "Unable to marshal response ext for debugging"
-					errs = append(errs, errors.New(debugLog.Data.Response))
-				}
-			}
-
 			cacheErrs := auc.doCache(ctx, e.cache, targData, bidRequest, 60, &account.CacheTTL, bidCategory, debugLog)
 			if len(cacheErrs) > 0 {
 				errs = append(errs, cacheErrs...)
 			}
 			targData.setTargeting(auc, bidRequest.App != nil, bidCategory)
 
-			// Ensure caching errors are added if the bid response ext has already been created
-			if bidResponseExt != nil && len(cacheErrs) > 0 {
-				bidderCacheErrs := errsToBidderErrors(cacheErrs)
-				bidResponseExt.Errors[openrtb_ext.PrebidExtKey] = append(bidResponseExt.Errors[openrtb_ext.PrebidExtKey], bidderCacheErrs...)
-			}
 		}
+	}
 
+	bidResponseExt := e.makeExtBidResponse(adapterBids, adapterExtra, bidRequest, debugInfo, errs)
+
+	// Ensure caching errors are added in case auc.doCache was called and errors were returned
+	if len(cacheErrs) > 0 {
+		bidderCacheErrs := errsToBidderErrors(cacheErrs)
+		bidResponseExt.Errors[openrtb_ext.PrebidExtKey] = append(bidResponseExt.Errors[openrtb_ext.PrebidExtKey], bidderCacheErrs...)
 	}
 
-	if !anyBidsReturned {
-		if debugLog != nil && debugLog.Enabled {
+	if debugLog != nil && debugLog.Enabled {
+		if bidRespExtBytes, err := json.Marshal(bidResponseExt); err == nil {
+			debugLog.Data.Response = string(bidRespExtBytes)
+		} else {
+			debugLog.Data.Response = "Unable to marshal response ext for debugging"
+			errs = append(errs, err)
+		}
+		if !anyBidsReturned {
 			if rawUUID, err := uuid.NewV4(); err == nil {
 				debugLog.CacheKey = rawUUID.String()
-
-				bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, bidRequest, debugInfo, errs)
-				if bidRespExtBytes, err := json.Marshal(bidResponseExt); err == nil {
-					debugLog.Data.Response = string(bidRespExtBytes)
-				} else {
-					debugLog.Data.Response = "Unable to marshal response ext for debugging"
-				}
 			} else {
 				errs = append(errs, err)
 			}
@@ -235,6 +203,41 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque
 	return e.buildBidResponse(ctx, liveAdapters, adapterBids, bidRequest, adapterExtra, auc, bidResponseExt, cacheInstructions.returnCreative, errs)
 }
 
+func (e *exchange) parseUsersyncIfAmbiguous(bidRequest *openrtb.BidRequest) bool {
+	usersyncIfAmbiguous := e.UsersyncIfAmbiguous
+	var geo *openrtb.Geo = nil
+
+	if bidRequest.User != nil && bidRequest.User.Geo != nil {
+		geo = bidRequest.User.Geo
+	} else if bidRequest.Device != nil && bidRequest.Device.Geo != nil {
+		geo = bidRequest.Device.Geo
+	}
+	if geo != nil {
+		// If we have a country set, and it is on the list, we assume GDPR applies if not set on the request.
+		// Otherwise we assume it does not apply as long as it appears "valid" (is 3 characters long).
+		if _, found := e.privacyConfig.GDPR.EEACountriesMap[strings.ToUpper(geo.Country)]; found {
+			usersyncIfAmbiguous = false
+		} else if len(geo.Country) == 3 {
+			// The country field is formatted properly as a three character country code
+			usersyncIfAmbiguous = true
+		}
+	}
+
+	return usersyncIfAmbiguous
+}
+
+func recordImpMetrics(bidRequest *openrtb.BidRequest, metricsEngine pbsmetrics.MetricsEngine) {
+	for _, impInRequest := range bidRequest.Imp {
+		var impLabels pbsmetrics.ImpLabels = pbsmetrics.ImpLabels{
+			BannerImps: impInRequest.Banner != nil,
+			VideoImps:  impInRequest.Video != nil,
+			AudioImps:  impInRequest.Audio != nil,
+			NativeImps: impInRequest.Native != nil,
+		}
+		metricsEngine.RecordImps(impLabels)
+	}
+}
+
 type DealTierInfo struct {
 	Prefix      string `json:"prefix"`
 	MinDealTier int    `json:"minDealTier"`
@@ -479,6 +482,7 @@ func errsToBidderErrors(errs []error) []openrtb_ext.ExtBidderError {
 // 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 *openrtb.BidRequest, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, bidResponseExt *openrtb_ext.ExtBidResponse, returnCreative bool, errList []error) (*openrtb.BidResponse, error) {
 	bidResponse := new(openrtb.BidResponse)
+	var err error
 
 	bidResponse.ID = bidRequest.ID
 	if len(liveAdapters) == 0 {
@@ -500,21 +504,19 @@ func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_
 
 	bidResponse.SeatBid = seatBids
 
-	if bidResponseExt == nil {
-		contextDebugValue := ctx.Value(DebugContextKey)
-		var debugInfo bool
-		if contextDebugValue != nil {
-			debugInfo = contextDebugValue.(bool)
-		}
-		bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, bidRequest, debugInfo, errList)
-	}
+	bidResponse.Ext, err = encodeBidResponseExt(bidResponseExt)
+
+	return bidResponse, err
+}
+
+func encodeBidResponseExt(bidResponseExt *openrtb_ext.ExtBidResponse) ([]byte, error) {
 	buffer := &bytes.Buffer{}
 	enc := json.NewEncoder(buffer)
+
 	enc.SetEscapeHTML(false)
 	err := enc.Encode(bidResponseExt)
-	bidResponse.Ext = buffer.Bytes()
 
-	return bidResponse, err
+	return buffer.Bytes(), err
 }
 
 func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtRequest, seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, categoriesFetcher stored_requests.CategoryFetcher, targData *targetData) (map[string]string, map[openrtb_ext.BidderName]*pbsOrtbSeatBid, []string, error) {
@@ -535,6 +537,7 @@ func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtReques
 
 	//If ext.prebid.targeting.includebrandcategory is present in ext then competitive exclusion feature is on.
 	var includeBrandCategory = brandCatExt != nil //if not present - category will no be appended
+	appendBidderNames := requestExt.Prebid.Targeting.AppendBidderNames
 
 	var primaryAdServer string
 	var publisher string
@@ -600,7 +603,7 @@ func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtReques
 
 			// TODO: consider should we remove bids with zero duration here?
 
-			pb, _ = GetCpmStringValue(bid.bid.Price, targData.priceGranularity)
+			pb = GetPriceBucket(bid.bid.Price, targData.priceGranularity)
 
 			newDur := duration
 			if len(requestExt.Prebid.Targeting.DurationRangeSec) > 0 {
@@ -630,6 +633,10 @@ func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtReques
 				dupeKey = categoryDuration
 			}
 
+			if appendBidderNames {
+				categoryDuration = fmt.Sprintf("%s_%s", categoryDuration, bidderName.String())
+			}
+
 			if dupe, ok := dedupe[dupeKey]; ok {
 
 				dupeBidPrice, err := strconv.ParseFloat(dupe.bidPrice, 64)
diff --git a/exchange/price_granularity.go b/exchange/price_granularity.go
index 27b0302d..242d420f 100644
--- a/exchange/price_granularity.go
+++ b/exchange/price_granularity.go
@@ -7,33 +7,32 @@ import (
 	"github.com/prebid/prebid-server/openrtb_ext"
 )
 
-// DEFAULT_PRECISION should be taken care of in openrtb_ext/request.go, but throwing an additional safety check here.
-
-// GetCpmStringValue is the externally facing function for computing CPM buckets
-func GetCpmStringValue(cpm float64, config openrtb_ext.PriceGranularity) (string, error) {
+// GetPriceBucket is the externally facing function for computing CPM buckets
+func GetPriceBucket(cpm float64, config openrtb_ext.PriceGranularity) string {
 	cpmStr := ""
 	bucketMax := 0.0
 	increment := 0.0
 	precision := config.Precision
-	// calculate max of highest bucket
+
 	for i := 0; i < len(config.Ranges); i++ {
 		if config.Ranges[i].Max > bucketMax {
 			bucketMax = config.Ranges[i].Max
 		}
-	} // calculate which bucket cpm is in
-	if cpm > bucketMax {
-		// If we are over max, just return that
-		return strconv.FormatFloat(bucketMax, 'f', precision, 64), nil
-	}
-	for i := 0; i < len(config.Ranges); i++ {
+		// find what range cpm is in
 		if cpm >= config.Ranges[i].Min && cpm <= config.Ranges[i].Max {
 			increment = config.Ranges[i].Increment
 		}
 	}
-	if increment > 0 {
+
+	if cpm > bucketMax {
+		// We are over max, just return that
+		cpmStr = strconv.FormatFloat(bucketMax, 'f', precision, 64)
+	} else if increment > 0 {
+		// If increment exists, get cpm string value
 		cpmStr = getCpmTarget(cpm, increment, precision)
 	}
-	return cpmStr, nil
+
+	return cpmStr
 }
 
 func getCpmTarget(cpm float64, increment float64, precision int) string {
diff --git a/exchange/targeting.go b/exchange/targeting.go
index 47bfeb65..87bec6f3 100644
--- a/exchange/targeting.go
+++ b/exchange/targeting.go
@@ -23,6 +23,7 @@ type targetData struct {
 	includeCacheBids  bool
 	includeCacheVast  bool
 	includeFormat     bool
+	preferDeals       bool
 	// cacheHost and cachePath exist to supply cache host and path as targeting parameters
 	cacheHost string
 	cachePath string
diff --git a/exchange/utils.go b/exchange/utils.go
index c011be52..63e56e84 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -543,6 +543,7 @@ func getExtTargetData(requestExt *openrtb_ext.ExtRequest, cacheInstructions *ext
 			includeCacheBids:  cacheInstructions.cacheBids,
 			includeCacheVast:  cacheInstructions.cacheVAST,
 			includeFormat:     requestExt.Prebid.Targeting.IncludeFormat,
+			preferDeals:       requestExt.Prebid.Targeting.PreferDeals,
 		}
 	}
 	return targData
diff --git a/openrtb_ext/bid_request_video.go b/openrtb_ext/bid_request_video.go
index 18865108..09edf51d 100644
--- a/openrtb_ext/bid_request_video.go
+++ b/openrtb_ext/bid_request_video.go
@@ -144,6 +144,13 @@ type BidRequestVideo struct {
 	// Description:
 	//   Indicates that the response should update key to include prefix and tier
 	SupportDeals bool `json:"supportdeals,omitempty"`
+
+	// Attribute:
+	//   appendbiddernames
+	// Type:
+	//   boolean, optional
+	//  Flag indicating if the bidder name will be added to the hb_pb_cat_dur. Default is false.
+	AppendBidderNames bool `json:"appendbiddernames,omitempty"`
 }
 
 type PodConfig struct {
diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go
index 177e907e..1199edc7 100644
--- a/openrtb_ext/request.go
+++ b/openrtb_ext/request.go
@@ -8,6 +8,7 @@ import (
 // FirstPartyDataContextExtKey defines the field name within bidrequest.ext reserved
 // for first party data support.
 const FirstPartyDataContextExtKey string = "context"
+const MaxDecimalFigures int = 15
 
 // ExtRequest defines the contract for bidrequest.ext
 type ExtRequest struct {
@@ -101,6 +102,8 @@ type ExtRequestTargeting struct {
 	IncludeBrandCategory *ExtIncludeBrandCategory `json:"includebrandcategory"`
 	IncludeFormat        bool                     `json:"includeformat"`
 	DurationRangeSec     []int                    `json:"durationrangesec"`
+	PreferDeals          bool                     `json:"preferdeals"`
+	AppendBidderNames    bool                     `json:"appendbiddernames,omitempty"`
 }
 
 type ExtIncludeBrandCategory struct {
@@ -178,6 +181,9 @@ func (pg *PriceGranularity) UnmarshalJSON(b []byte) error {
 	if pgraw.Precision < 0 {
 		return errors.New("Price granularity error: precision must be non-negative")
 	}
+	if pgraw.Precision > MaxDecimalFigures {
+		return errors.New("Price granularity error: precision of more than 15 significant figures is not supported")
+	}
 	if len(pgraw.Ranges) > 0 {
 		var prevMax float64 = 0
 		for i, gr := range pgraw.Ranges {
@@ -189,9 +195,6 @@ func (pg *PriceGranularity) UnmarshalJSON(b []byte) error {
 			}
 			// Enforce that we don't read "min" from the request
 			pgraw.Ranges[i].Min = prevMax
-			if pgraw.Ranges[i].Min < prevMax {
-				return errors.New("Price granularity error: overlapping granularity ranges")
-			}
 			prevMax = gr.Max
 		}
 		*pg = PriceGranularity(pgraw)
diff --git a/pbsmetrics/config/metrics.go b/pbsmetrics/config/metrics.go
index 6a36f9e7..903b17bf 100644
--- a/pbsmetrics/config/metrics.go
+++ b/pbsmetrics/config/metrics.go
@@ -104,6 +104,20 @@ func (me *MultiMetricsEngine) RecordRequestTime(labels pbsmetrics.Labels, length
 	}
 }
 
+// RecordStoredDataFetchTime across all engines
+func (me *MultiMetricsEngine) RecordStoredDataFetchTime(labels pbsmetrics.StoredDataLabels, length time.Duration) {
+	for _, thisME := range *me {
+		thisME.RecordStoredDataFetchTime(labels, length)
+	}
+}
+
+// RecordStoredDataError across all engines
+func (me *MultiMetricsEngine) RecordStoredDataError(labels pbsmetrics.StoredDataLabels) {
+	for _, thisME := range *me {
+		thisME.RecordStoredDataError(labels)
+	}
+}
+
 // RecordAdapterPanic across all engines
 func (me *MultiMetricsEngine) RecordAdapterPanic(labels pbsmetrics.AdapterLabels) {
 	for _, thisME := range *me {
@@ -244,6 +258,14 @@ func (me *DummyMetricsEngine) RecordLegacyImps(labels pbsmetrics.Labels, numImps
 func (me *DummyMetricsEngine) RecordRequestTime(labels pbsmetrics.Labels, length time.Duration) {
 }
 
+// RecordStoredDataFetchTime as a noop
+func (me *DummyMetricsEngine) RecordStoredDataFetchTime(labels pbsmetrics.StoredDataLabels, length time.Duration) {
+}
+
+// RecordStoredDataError as a noop
+func (me *DummyMetricsEngine) RecordStoredDataError(labels pbsmetrics.StoredDataLabels) {
+}
+
 // RecordAdapterPanic as a noop
 func (me *DummyMetricsEngine) RecordAdapterPanic(labels pbsmetrics.AdapterLabels) {
 }
diff --git a/pbsmetrics/go_metrics.go b/pbsmetrics/go_metrics.go
index 26f6ce07..3842bfd2 100644
--- a/pbsmetrics/go_metrics.go
+++ b/pbsmetrics/go_metrics.go
@@ -27,6 +27,8 @@ type Metrics struct {
 	RequestsQueueTimer             map[RequestType]map[bool]metrics.Timer
 	PrebidCacheRequestTimerSuccess metrics.Timer
 	PrebidCacheRequestTimerError   metrics.Timer
+	StoredDataFetchTimer           map[StoredDataType]map[StoredDataFetchType]metrics.Timer
+	StoredDataErrorMeter           map[StoredDataType]map[StoredDataError]metrics.Meter
 	StoredReqCacheMeter            map[CacheResult]metrics.Meter
 	StoredImpCacheMeter            map[CacheResult]metrics.Meter
 	DNSLookupTimer                 metrics.Timer
@@ -131,6 +133,8 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa
 		RequestsQueueTimer:             make(map[RequestType]map[bool]metrics.Timer),
 		PrebidCacheRequestTimerSuccess: blankTimer,
 		PrebidCacheRequestTimerError:   blankTimer,
+		StoredDataFetchTimer:           make(map[StoredDataType]map[StoredDataFetchType]metrics.Timer),
+		StoredDataErrorMeter:           make(map[StoredDataType]map[StoredDataError]metrics.Meter),
 		StoredReqCacheMeter:            make(map[CacheResult]metrics.Meter),
 		StoredImpCacheMeter:            make(map[CacheResult]metrics.Meter),
 		AmpNoCookieMeter:               blankMeter,
@@ -183,6 +187,17 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa
 		newMetrics.PrivacyTCFRequestVersion[v] = blankMeter
 	}
 
+	for _, dt := range StoredDataTypes() {
+		newMetrics.StoredDataFetchTimer[dt] = make(map[StoredDataFetchType]metrics.Timer)
+		newMetrics.StoredDataErrorMeter[dt] = make(map[StoredDataError]metrics.Meter)
+		for _, ft := range StoredDataFetchTypes() {
+			newMetrics.StoredDataFetchTimer[dt][ft] = blankTimer
+		}
+		for _, e := range StoredDataErrors() {
+			newMetrics.StoredDataErrorMeter[dt][e] = blankMeter
+		}
+	}
+
 	//to minimize memory usage, queuedTimeout metric is now supported for video endpoint only
 	//boolean value represents 2 general request statuses: accepted and rejected
 	newMetrics.RequestsQueueTimer["video"] = make(map[bool]metrics.Timer)
@@ -218,6 +233,17 @@ func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, d
 	newMetrics.PrebidCacheRequestTimerSuccess = metrics.GetOrRegisterTimer("prebid_cache_request_time.ok", registry)
 	newMetrics.PrebidCacheRequestTimerError = metrics.GetOrRegisterTimer("prebid_cache_request_time.err", registry)
 
+	for _, dt := range StoredDataTypes() {
+		for _, ft := range StoredDataFetchTypes() {
+			timerName := fmt.Sprintf("stored_%s_fetch_time.%s", string(dt), string(ft))
+			newMetrics.StoredDataFetchTimer[dt][ft] = metrics.GetOrRegisterTimer(timerName, registry)
+		}
+		for _, e := range StoredDataErrors() {
+			meterName := fmt.Sprintf("stored_%s_error.%s", string(dt), string(e))
+			newMetrics.StoredDataErrorMeter[dt][e] = metrics.GetOrRegisterMeter(meterName, registry)
+		}
+	}
+
 	newMetrics.AmpNoCookieMeter = metrics.GetOrRegisterMeter("amp_no_cookie_requests", registry)
 	newMetrics.CookieSyncMeter = metrics.GetOrRegisterMeter("cookie_sync_requests", registry)
 	newMetrics.userSyncBadRequest = metrics.GetOrRegisterMeter("usersync.bad_requests", registry)
@@ -444,6 +470,16 @@ func (me *Metrics) RecordRequestTime(labels Labels, length time.Duration) {
 	}
 }
 
+// RecordStoredDataFetchTime implements a part of the MetricsEngine interface
+func (me *Metrics) RecordStoredDataFetchTime(labels StoredDataLabels, length time.Duration) {
+	me.StoredDataFetchTimer[labels.DataType][labels.DataFetchType].Update(length)
+}
+
+// RecordStoredDataError implements a part of the MetricsEngine interface
+func (me *Metrics) RecordStoredDataError(labels StoredDataLabels) {
+	me.StoredDataErrorMeter[labels.DataType][labels.Error].Mark(1)
+}
+
 // RecordAdapterPanic implements a part of the MetricsEngine interface
 func (me *Metrics) RecordAdapterPanic(labels AdapterLabels) {
 	am, ok := me.AdapterMetrics[labels.Adapter]
diff --git a/pbsmetrics/metrics.go b/pbsmetrics/metrics.go
index 8133bc73..7f9d66b5 100644
--- a/pbsmetrics/metrics.go
+++ b/pbsmetrics/metrics.go
@@ -51,6 +51,60 @@ type PrivacyLabels struct {
 	LMTEnforced    bool
 }
 
+type StoredDataType string
+
+const (
+	AccountDataType  StoredDataType = "account"
+	AMPDataType      StoredDataType = "amp"
+	CategoryDataType StoredDataType = "category"
+	RequestDataType  StoredDataType = "request"
+	VideoDataType    StoredDataType = "video"
+)
+
+func StoredDataTypes() []StoredDataType {
+	return []StoredDataType{
+		AccountDataType,
+		AMPDataType,
+		CategoryDataType,
+		RequestDataType,
+		VideoDataType,
+	}
+}
+
+type StoredDataFetchType string
+
+const (
+	FetchAll   StoredDataFetchType = "all"
+	FetchDelta StoredDataFetchType = "delta"
+)
+
+func StoredDataFetchTypes() []StoredDataFetchType {
+	return []StoredDataFetchType{
+		FetchAll,
+		FetchDelta,
+	}
+}
+
+type StoredDataLabels struct {
+	DataType      StoredDataType
+	DataFetchType StoredDataFetchType
+	Error         StoredDataError
+}
+
+type StoredDataError string
+
+const (
+	StoredDataErrorNetwork   StoredDataError = "network"
+	StoredDataErrorUndefined StoredDataError = "undefined"
+)
+
+func StoredDataErrors() []StoredDataError {
+	return []StoredDataError{
+		StoredDataErrorNetwork,
+		StoredDataErrorUndefined,
+	}
+}
+
 // Label typecasting. Se below the type definitions for possible values
 
 // DemandSource : Demand source enumeration
@@ -314,6 +368,8 @@ type MetricsEngine interface {
 	RecordUserIDSet(userLabels UserLabels) // Function should verify bidder values
 	RecordStoredReqCacheResult(cacheResult CacheResult, inc int)
 	RecordStoredImpCacheResult(cacheResult CacheResult, inc int)
+	RecordStoredDataFetchTime(labels StoredDataLabels, length time.Duration)
+	RecordStoredDataError(labels StoredDataLabels)
 	RecordPrebidCacheRequestTime(success bool, length time.Duration)
 	RecordRequestQueueTime(success bool, requestType RequestType, length time.Duration)
 	RecordTimeoutNotice(sucess bool)
diff --git a/pbsmetrics/metrics_mock.go b/pbsmetrics/metrics_mock.go
index 42a2d1b4..63b05196 100644
--- a/pbsmetrics/metrics_mock.go
+++ b/pbsmetrics/metrics_mock.go
@@ -42,6 +42,16 @@ func (me *MetricsEngineMock) RecordRequestTime(labels Labels, length time.Durati
 	me.Called(labels, length)
 }
 
+// RecordStoredDataFetchTime mock
+func (me *MetricsEngineMock) RecordStoredDataFetchTime(labels StoredDataLabels, length time.Duration) {
+	me.Called(labels, length)
+}
+
+// RecordStoredDataError mock
+func (me *MetricsEngineMock) RecordStoredDataError(labels StoredDataLabels) {
+	me.Called(labels)
+}
+
 // RecordAdapterPanic mock
 func (me *MetricsEngineMock) RecordAdapterPanic(labels AdapterLabels) {
 	me.Called(labels)
diff --git a/pbsmetrics/prometheus/preload.go b/pbsmetrics/prometheus/preload.go
index 4f62a18a..52620bc6 100644
--- a/pbsmetrics/prometheus/preload.go
+++ b/pbsmetrics/prometheus/preload.go
@@ -7,17 +7,19 @@ import (
 
 func preloadLabelValues(m *Metrics) {
 	var (
-		actionValues          = actionsAsString()
-		adapterErrorValues    = adapterErrorsAsString()
-		adapterValues         = adaptersAsString()
-		bidTypeValues         = []string{markupDeliveryAdm, markupDeliveryNurl}
-		boolValues            = boolValuesAsString()
-		cacheResultValues     = cacheResultsAsString()
-		connectionErrorValues = []string{connectionAcceptError, connectionCloseError}
-		cookieValues          = cookieTypesAsString()
-		requestStatusValues   = requestStatusesAsString()
-		requestTypeValues     = requestTypesAsString()
-		sourceValues          = []string{sourceRequest}
+		actionValues              = actionsAsString()
+		adapterErrorValues        = adapterErrorsAsString()
+		adapterValues             = adaptersAsString()
+		bidTypeValues             = []string{markupDeliveryAdm, markupDeliveryNurl}
+		boolValues                = boolValuesAsString()
+		cacheResultValues         = cacheResultsAsString()
+		connectionErrorValues     = []string{connectionAcceptError, connectionCloseError}
+		cookieValues              = cookieTypesAsString()
+		requestStatusValues       = requestStatusesAsString()
+		requestTypeValues         = requestTypesAsString()
+		storedDataFetchTypeValues = storedDataFetchTypesAsString()
+		storedDataErrorValues     = storedDataErrorsAsString()
+		sourceValues              = []string{sourceRequest}
 	)
 
 	preloadLabelValuesForCounter(m.connectionsError, map[string][]string{
@@ -44,6 +46,46 @@ func preloadLabelValues(m *Metrics) {
 		requestTypeLabel: requestTypeValues,
 	})
 
+	preloadLabelValuesForHistogram(m.storedAccountFetchTimer, map[string][]string{
+		storedDataFetchTypeLabel: storedDataFetchTypeValues,
+	})
+
+	preloadLabelValuesForHistogram(m.storedAMPFetchTimer, map[string][]string{
+		storedDataFetchTypeLabel: storedDataFetchTypeValues,
+	})
+
+	preloadLabelValuesForHistogram(m.storedCategoryFetchTimer, map[string][]string{
+		storedDataFetchTypeLabel: storedDataFetchTypeValues,
+	})
+
+	preloadLabelValuesForHistogram(m.storedRequestFetchTimer, map[string][]string{
+		storedDataFetchTypeLabel: storedDataFetchTypeValues,
+	})
+
+	preloadLabelValuesForHistogram(m.storedVideoFetchTimer, map[string][]string{
+		storedDataFetchTypeLabel: storedDataFetchTypeValues,
+	})
+
+	preloadLabelValuesForCounter(m.storedAccountErrors, map[string][]string{
+		storedDataErrorLabel: storedDataErrorValues,
+	})
+
+	preloadLabelValuesForCounter(m.storedAMPErrors, map[string][]string{
+		storedDataErrorLabel: storedDataErrorValues,
+	})
+
+	preloadLabelValuesForCounter(m.storedCategoryErrors, map[string][]string{
+		storedDataErrorLabel: storedDataErrorValues,
+	})
+
+	preloadLabelValuesForCounter(m.storedRequestErrors, map[string][]string{
+		storedDataErrorLabel: storedDataErrorValues,
+	})
+
+	preloadLabelValuesForCounter(m.storedVideoErrors, map[string][]string{
+		storedDataErrorLabel: storedDataErrorValues,
+	})
+
 	preloadLabelValuesForCounter(m.requestsWithoutCookie, map[string][]string{
 		requestTypeLabel: requestTypeValues,
 	})
diff --git a/pbsmetrics/prometheus/prometheus.go b/pbsmetrics/prometheus/prometheus.go
index b42399b2..046d0769 100644
--- a/pbsmetrics/prometheus/prometheus.go
+++ b/pbsmetrics/prometheus/prometheus.go
@@ -28,6 +28,16 @@ type Metrics struct {
 	requestsWithoutCookie        *prometheus.CounterVec
 	storedImpressionsCacheResult *prometheus.CounterVec
 	storedRequestCacheResult     *prometheus.CounterVec
+	storedAccountFetchTimer      *prometheus.HistogramVec
+	storedAccountErrors          *prometheus.CounterVec
+	storedAMPFetchTimer          *prometheus.HistogramVec
+	storedAMPErrors              *prometheus.CounterVec
+	storedCategoryFetchTimer     *prometheus.HistogramVec
+	storedCategoryErrors         *prometheus.CounterVec
+	storedRequestFetchTimer      *prometheus.HistogramVec
+	storedRequestErrors          *prometheus.CounterVec
+	storedVideoFetchTimer        *prometheus.HistogramVec
+	storedVideoErrors            *prometheus.CounterVec
 	timeoutNotifications         *prometheus.CounterVec
 	dnsLookupTimer               prometheus.Histogram
 	privacyCCPA                  *prometheus.CounterVec
@@ -102,6 +112,11 @@ const (
 	sourceRequest = "request"
 )
 
+const (
+	storedDataFetchTypeLabel = "stored_data_fetch_type"
+	storedDataErrorLabel     = "stored_data_error"
+)
+
 // NewMetrics initializes a new Prometheus metrics instance with preloaded label values.
 func NewMetrics(cfg config.PrometheusMetrics, disabledMetrics config.DisabledMetrics) *Metrics {
 	standardTimeBuckets := []float64{0.05, 0.1, 0.15, 0.20, 0.25, 0.3, 0.4, 0.5, 0.75, 1}
@@ -171,6 +186,61 @@ func NewMetrics(cfg config.PrometheusMetrics, disabledMetrics config.DisabledMet
 		"Count of stored request cache requests attempts by hits or miss.",
 		[]string{cacheResultLabel})
 
+	metrics.storedAccountFetchTimer = newHistogramVec(cfg, metrics.Registry,
+		"stored_account_fetch_time_seconds",
+		"Seconds to fetch stored accounts labeled by fetch type",
+		[]string{storedDataFetchTypeLabel},
+		standardTimeBuckets)
+
+	metrics.storedAccountErrors = newCounter(cfg, metrics.Registry,
+		"stored_account_errors",
+		"Count of stored account errors by error type",
+		[]string{storedDataErrorLabel})
+
+	metrics.storedAMPFetchTimer = newHistogramVec(cfg, metrics.Registry,
+		"stored_amp_fetch_time_seconds",
+		"Seconds to fetch stored AMP requests labeled by fetch type",
+		[]string{storedDataFetchTypeLabel},
+		standardTimeBuckets)
+
+	metrics.storedAMPErrors = newCounter(cfg, metrics.Registry,
+		"stored_amp_errors",
+		"Count of stored AMP errors by error type",
+		[]string{storedDataErrorLabel})
+
+	metrics.storedCategoryFetchTimer = newHistogramVec(cfg, metrics.Registry,
+		"stored_category_fetch_time_seconds",
+		"Seconds to fetch stored categories labeled by fetch type",
+		[]string{storedDataFetchTypeLabel},
+		standardTimeBuckets)
+
+	metrics.storedCategoryErrors = newCounter(cfg, metrics.Registry,
+		"stored_category_errors",
+		"Count of stored category errors by error type",
+		[]string{storedDataErrorLabel})
+
+	metrics.storedRequestFetchTimer = newHistogramVec(cfg, metrics.Registry,
+		"stored_request_fetch_time_seconds",
+		"Seconds to fetch stored requests labeled by fetch type",
+		[]string{storedDataFetchTypeLabel},
+		standardTimeBuckets)
+
+	metrics.storedRequestErrors = newCounter(cfg, metrics.Registry,
+		"stored_request_errors",
+		"Count of stored request errors by error type",
+		[]string{storedDataErrorLabel})
+
+	metrics.storedVideoFetchTimer = newHistogramVec(cfg, metrics.Registry,
+		"stored_video_fetch_time_seconds",
+		"Seconds to fetch stored video labeled by fetch type",
+		[]string{storedDataFetchTypeLabel},
+		standardTimeBuckets)
+
+	metrics.storedVideoErrors = newCounter(cfg, metrics.Registry,
+		"stored_video_errors",
+		"Count of stored video errors by error type",
+		[]string{storedDataErrorLabel})
+
 	metrics.timeoutNotifications = newCounter(cfg, metrics.Registry,
 		"timeout_notification",
 		"Count of timeout notifications triggered, and if they were successfully sent.",
@@ -387,6 +457,56 @@ func (m *Metrics) RecordRequestTime(labels pbsmetrics.Labels, length time.Durati
 	}
 }
 
+func (m *Metrics) RecordStoredDataFetchTime(labels pbsmetrics.StoredDataLabels, length time.Duration) {
+	switch labels.DataType {
+	case pbsmetrics.AccountDataType:
+		m.storedAccountFetchTimer.With(prometheus.Labels{
+			storedDataFetchTypeLabel: string(labels.DataFetchType),
+		}).Observe(length.Seconds())
+	case pbsmetrics.AMPDataType:
+		m.storedAMPFetchTimer.With(prometheus.Labels{
+			storedDataFetchTypeLabel: string(labels.DataFetchType),
+		}).Observe(length.Seconds())
+	case pbsmetrics.CategoryDataType:
+		m.storedCategoryFetchTimer.With(prometheus.Labels{
+			storedDataFetchTypeLabel: string(labels.DataFetchType),
+		}).Observe(length.Seconds())
+	case pbsmetrics.RequestDataType:
+		m.storedRequestFetchTimer.With(prometheus.Labels{
+			storedDataFetchTypeLabel: string(labels.DataFetchType),
+		}).Observe(length.Seconds())
+	case pbsmetrics.VideoDataType:
+		m.storedVideoFetchTimer.With(prometheus.Labels{
+			storedDataFetchTypeLabel: string(labels.DataFetchType),
+		}).Observe(length.Seconds())
+	}
+}
+
+func (m *Metrics) RecordStoredDataError(labels pbsmetrics.StoredDataLabels) {
+	switch labels.DataType {
+	case pbsmetrics.AccountDataType:
+		m.storedAccountErrors.With(prometheus.Labels{
+			storedDataErrorLabel: string(labels.Error),
+		}).Inc()
+	case pbsmetrics.AMPDataType:
+		m.storedAMPErrors.With(prometheus.Labels{
+			storedDataErrorLabel: string(labels.Error),
+		}).Inc()
+	case pbsmetrics.CategoryDataType:
+		m.storedCategoryErrors.With(prometheus.Labels{
+			storedDataErrorLabel: string(labels.Error),
+		}).Inc()
+	case pbsmetrics.RequestDataType:
+		m.storedRequestErrors.With(prometheus.Labels{
+			storedDataErrorLabel: string(labels.Error),
+		}).Inc()
+	case pbsmetrics.VideoDataType:
+		m.storedVideoErrors.With(prometheus.Labels{
+			storedDataErrorLabel: string(labels.Error),
+		}).Inc()
+	}
+}
+
 func (m *Metrics) RecordAdapterRequest(labels pbsmetrics.AdapterLabels) {
 	m.adapterRequests.With(prometheus.Labels{
 		adapterLabel: string(labels.Adapter),
diff --git a/pbsmetrics/prometheus/type_conversion.go b/pbsmetrics/prometheus/type_conversion.go
index 55a7092e..75efb377 100644
--- a/pbsmetrics/prometheus/type_conversion.go
+++ b/pbsmetrics/prometheus/type_conversion.go
@@ -77,6 +77,33 @@ func requestTypesAsString() []string {
 	return valuesAsString
 }
 
+func storedDataTypesAsString() []string {
+	values := pbsmetrics.StoredDataTypes()
+	valuesAsString := make([]string, len(values))
+	for i, v := range values {
+		valuesAsString[i] = string(v)
+	}
+	return valuesAsString
+}
+
+func storedDataFetchTypesAsString() []string {
+	values := pbsmetrics.StoredDataFetchTypes()
+	valuesAsString := make([]string, len(values))
+	for i, v := range values {
+		valuesAsString[i] = string(v)
+	}
+	return valuesAsString
+}
+
+func storedDataErrorsAsString() []string {
+	values := pbsmetrics.StoredDataErrors()
+	valuesAsString := make([]string, len(values))
+	for i, v := range values {
+		valuesAsString[i] = string(v)
+	}
+	return valuesAsString
+}
+
 func tcfVersionsAsString() []string {
 	values := pbsmetrics.TCFVersions()
 	valuesAsString := make([]string, len(values))
diff --git a/static/bidder-info/emx_digital.yaml b/static/bidder-info/emx_digital.yaml
index 40f73fd0..49a06809 100644
--- a/static/bidder-info/emx_digital.yaml
+++ b/static/bidder-info/emx_digital.yaml
@@ -4,3 +4,8 @@ capabilities:
   site:
     mediaTypes:
       - banner
+      - video
+  app:
+    mediaTypes:
+      - banner
+      - video
\ No newline at end of file
diff --git a/stored_requests/caches/memory/cache.go b/stored_requests/caches/memory/cache.go
index aea087e6..5939c26d 100644
--- a/stored_requests/caches/memory/cache.go
+++ b/stored_requests/caches/memory/cache.go
@@ -18,7 +18,8 @@ import (
 // For no TTL, use ttlSeconds <= 0
 func NewCache(size int, ttl int, dataType string) stored_requests.CacheJSON {
 	if ttl > 0 && size <= 0 {
-		glog.Fatalf("No in-memory %s caches defined with a finite TTL but unbounded size. Config validation should have caught this. Failing fast because something is buggy.", dataType)
+		// a positive ttl indicates "LRU" cache type, while unlimited size indicates an "unbounded" cache type
+		glog.Fatalf("unbounded in-memory %s cache with TTL not allowed. Config validation should have caught this. Failing fast because something is buggy.", dataType)
 	}
 	if size > 0 {
 		glog.Infof("Using a Stored %s in-memory cache. Max size: %d bytes. TTL: %d seconds.", dataType, size, ttl)
diff --git a/stored_requests/config/config.go b/stored_requests/config/config.go
index c8231969..1036cf0a 100644
--- a/stored_requests/config/config.go
+++ b/stored_requests/config/config.go
@@ -65,7 +65,7 @@ func CreateStoredRequests(cfg *config.StoredRequests, metricsEngine pbsmetrics.M
 		}
 	}
 
-	eventProducers := newEventProducers(cfg, client, dbc.db, router)
+	eventProducers := newEventProducers(cfg, client, dbc.db, metricsEngine, router)
 	fetcher = newFetcher(cfg, client, dbc.db)
 
 	var shutdown1 func()
@@ -177,18 +177,20 @@ func newFetcher(cfg *config.StoredRequests, client *http.Client, db *sql.DB) (fe
 }
 
 func newCache(cfg *config.StoredRequests) stored_requests.Cache {
-	if cfg.InMemoryCache.Type == "none" {
-		glog.Infof("No Stored %s cache configured. The %s Fetcher backend will be used for all data requests", cfg.DataType(), cfg.DataType())
-		return stored_requests.Cache{&nil_cache.NilCache{}, &nil_cache.NilCache{}}
-	}
-
-	return stored_requests.Cache{
-		Requests: memory.NewCache(cfg.InMemoryCache.RequestCacheSize, cfg.InMemoryCache.TTL, "Requests"),
-		Imps:     memory.NewCache(cfg.InMemoryCache.ImpCacheSize, cfg.InMemoryCache.TTL, "Imps"),
+	cache := stored_requests.Cache{&nil_cache.NilCache{}, &nil_cache.NilCache{}, &nil_cache.NilCache{}}
+	switch {
+	case cfg.InMemoryCache.Type == "none":
+		glog.Warningf("No %s cache configured. The %s Fetcher backend will be used for all data requests", cfg.DataType(), cfg.DataType())
+	case cfg.DataType() == config.AccountDataType:
+		cache.Accounts = memory.NewCache(cfg.InMemoryCache.Size, cfg.InMemoryCache.TTL, "Accounts")
+	default:
+		cache.Requests = memory.NewCache(cfg.InMemoryCache.RequestCacheSize, cfg.InMemoryCache.TTL, "Requests")
+		cache.Imps = memory.NewCache(cfg.InMemoryCache.ImpCacheSize, cfg.InMemoryCache.TTL, "Imps")
 	}
+	return cache
 }
 
-func newEventProducers(cfg *config.StoredRequests, client *http.Client, db *sql.DB, router *httprouter.Router) (eventProducers []events.EventProducer) {
+func newEventProducers(cfg *config.StoredRequests, client *http.Client, db *sql.DB, metricsEngine pbsmetrics.MetricsEngine, router *httprouter.Router) (eventProducers []events.EventProducer) {
 	if cfg.CacheEvents.Enabled {
 		eventProducers = append(eventProducers, newEventsAPI(router, cfg.CacheEvents.Endpoint))
 	}
@@ -203,6 +205,7 @@ func newEventProducers(cfg *config.StoredRequests, client *http.Client, db *sql.
 			CacheInitTimeout:   time.Duration(cfg.Postgres.CacheInitialization.Timeout) * time.Millisecond,
 			CacheUpdateQuery:   cfg.Postgres.PollUpdates.Query,
 			CacheUpdateTimeout: time.Duration(cfg.Postgres.PollUpdates.Timeout) * time.Millisecond,
+			MetricsEngine:      metricsEngine,
 		}
 		pgEventProducer := postgresEvents.NewPostgresEventProducer(pgEventCfg)
 		fetchInterval := time.Duration(cfg.Postgres.PollUpdates.RefreshRate) * time.Second
diff --git a/stored_requests/events/postgres/database.go b/stored_requests/events/postgres/database.go
index cec9b504..8e1269c6 100644
--- a/stored_requests/events/postgres/database.go
+++ b/stored_requests/events/postgres/database.go
@@ -5,10 +5,12 @@ import (
 	"context"
 	"database/sql"
 	"encoding/json"
+	"net"
 	"time"
 
 	"github.com/golang/glog"
 	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/pbsmetrics"
 	"github.com/prebid/prebid-server/stored_requests/events"
 	"github.com/prebid/prebid-server/util/timeutil"
 )
@@ -17,6 +19,14 @@ func bytesNull() []byte {
 	return []byte{'n', 'u', 'l', 'l'}
 }
 
+var storedDataTypeMetricMap = map[config.DataType]pbsmetrics.StoredDataType{
+	config.RequestDataType:    pbsmetrics.RequestDataType,
+	config.CategoryDataType:   pbsmetrics.CategoryDataType,
+	config.VideoDataType:      pbsmetrics.VideoDataType,
+	config.AMPRequestDataType: pbsmetrics.AMPDataType,
+	config.AccountDataType:    pbsmetrics.AccountDataType,
+}
+
 type PostgresEventProducerConfig struct {
 	DB                 *sql.DB
 	RequestType        config.DataType
@@ -24,6 +34,7 @@ type PostgresEventProducerConfig struct {
 	CacheInitTimeout   time.Duration
 	CacheUpdateQuery   string
 	CacheUpdateTimeout time.Duration
+	MetricsEngine      pbsmetrics.MetricsEngine
 }
 
 type PostgresEventProducer struct {
@@ -51,9 +62,9 @@ func NewPostgresEventProducer(cfg PostgresEventProducerConfig) (eventProducer *P
 func (e *PostgresEventProducer) Run() error {
 	if e.lastUpdate.IsZero() {
 		return e.fetchAll()
-	} else {
-		return e.fetchDelta()
 	}
+
+	return e.fetchDelta()
 }
 
 func (e *PostgresEventProducer) Saves() <-chan events.Save {
@@ -64,60 +75,96 @@ func (e *PostgresEventProducer) Invalidations() <-chan events.Invalidation {
 	return e.invalidations
 }
 
-func (e *PostgresEventProducer) fetchAll() error {
-	thisTimeInUTC := e.time.Now().UTC()
-
+func (e *PostgresEventProducer) fetchAll() (fetchErr error) {
 	timeout := e.cfg.CacheInitTimeout * time.Millisecond
 	ctx, cancel := context.WithTimeout(context.Background(), timeout)
 	defer cancel()
 
+	startTime := e.time.Now().UTC()
 	rows, err := e.cfg.DB.QueryContext(ctx, e.cfg.CacheInitQuery)
+	elapsedTime := time.Since(startTime)
+	e.recordFetchTime(elapsedTime, pbsmetrics.FetchAll)
 
 	if err != nil {
 		glog.Warningf("Failed to fetch all Stored %s data from the DB: %v", e.cfg.RequestType, err)
+		if _, ok := err.(net.Error); ok {
+			e.recordError(pbsmetrics.StoredDataErrorNetwork)
+		} else {
+			e.recordError(pbsmetrics.StoredDataErrorUndefined)
+		}
 		return err
 	}
+
 	defer func() {
 		if err := rows.Close(); err != nil {
 			glog.Warningf("Failed to close the Stored %s DB connection: %v", e.cfg.RequestType, err)
+			e.recordError(pbsmetrics.StoredDataErrorUndefined)
+			fetchErr = err
 		}
 	}()
 	if err := e.sendEvents(rows); err != nil {
 		glog.Warningf("Failed to load all Stored %s data from the DB: %v", e.cfg.RequestType, err)
+		e.recordError(pbsmetrics.StoredDataErrorUndefined)
 		return err
-	} else {
-		e.lastUpdate = thisTimeInUTC
 	}
+
+	e.lastUpdate = startTime
 	return nil
 }
 
-func (e *PostgresEventProducer) fetchDelta() error {
-	thisTimeInUTC := e.time.Now().UTC()
-
+func (e *PostgresEventProducer) fetchDelta() (fetchErr error) {
 	timeout := e.cfg.CacheUpdateTimeout * time.Millisecond
 	ctx, cancel := context.WithTimeout(context.Background(), timeout)
 	defer cancel()
 
+	startTime := e.time.Now().UTC()
 	rows, err := e.cfg.DB.QueryContext(ctx, e.cfg.CacheUpdateQuery, e.lastUpdate)
+	elapsedTime := time.Since(startTime)
+	e.recordFetchTime(elapsedTime, pbsmetrics.FetchDelta)
 
 	if err != nil {
 		glog.Warningf("Failed to fetch updated Stored %s data from the DB: %v", e.cfg.RequestType, err)
+		if _, ok := err.(net.Error); ok {
+			e.recordError(pbsmetrics.StoredDataErrorNetwork)
+		} else {
+			e.recordError(pbsmetrics.StoredDataErrorUndefined)
+		}
 		return err
 	}
+
 	defer func() {
 		if err := rows.Close(); err != nil {
 			glog.Warningf("Failed to close the Stored %s DB connection: %v", e.cfg.RequestType, err)
+			e.recordError(pbsmetrics.StoredDataErrorUndefined)
+			fetchErr = err
 		}
 	}()
 	if err := e.sendEvents(rows); err != nil {
 		glog.Warningf("Failed to load updated Stored %s data from the DB: %v", e.cfg.RequestType, err)
+		e.recordError(pbsmetrics.StoredDataErrorUndefined)
 		return err
-	} else {
-		e.lastUpdate = thisTimeInUTC
 	}
+
+	e.lastUpdate = startTime
 	return nil
 }
 
+func (e *PostgresEventProducer) recordFetchTime(elapsedTime time.Duration, fetchType pbsmetrics.StoredDataFetchType) {
+	e.cfg.MetricsEngine.RecordStoredDataFetchTime(
+		pbsmetrics.StoredDataLabels{
+			DataType:      storedDataTypeMetricMap[e.cfg.RequestType],
+			DataFetchType: fetchType,
+		}, elapsedTime)
+}
+
+func (e *PostgresEventProducer) recordError(errorType pbsmetrics.StoredDataError) {
+	e.cfg.MetricsEngine.RecordStoredDataError(
+		pbsmetrics.StoredDataLabels{
+			DataType: storedDataTypeMetricMap[e.cfg.RequestType],
+			Error:    errorType,
+		})
+}
+
 // sendEvents reads the rows and sends notifications into the channel for any updates.
 // If it returns an error, then callers can be certain that no events were sent to the channels.
 func (e *PostgresEventProducer) sendEvents(rows *sql.Rows) (err error) {
diff --git a/stored_requests/fetcher.go b/stored_requests/fetcher.go
index e9716e08..1773c966 100644
--- a/stored_requests/fetcher.go
+++ b/stored_requests/fetcher.go
@@ -65,6 +65,7 @@ func (e NotFoundError) Error() string {
 type Cache struct {
 	Requests CacheJSON
 	Imps     CacheJSON
+	Accounts CacheJSON
 }
 type CacheJSON interface {
 	// Get works much like Fetcher.FetchRequests, with a few exceptions:
@@ -190,8 +191,17 @@ func (f *fetcherWithCache) FetchRequests(ctx context.Context, requestIDs []strin
 	return
 }
 
-func (f *fetcherWithCache) FetchAccount(ctx context.Context, accountID string) (json.RawMessage, []error) {
-	return f.fetcher.FetchAccount(ctx, accountID)
+func (f *fetcherWithCache) FetchAccount(ctx context.Context, accountID string) (account json.RawMessage, errs []error) {
+	accountData := f.cache.Accounts.Get(ctx, []string{accountID})
+	// TODO: add metrics
+	if account, ok := accountData[accountID]; ok {
+		return account, errs
+	}
+	account, errs = f.fetcher.FetchAccount(ctx, accountID)
+	if len(errs) == 0 {
+		f.cache.Accounts.Save(ctx, map[string]json.RawMessage{accountID: account})
+	}
+	return account, errs
 }
 
 func (f *fetcherWithCache) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) {


Test patch:
diff --git a/adapters/emx_digital/emx_digitaltest/exemplary/banner-and-video-app.json b/adapters/emx_digital/emx_digitaltest/exemplary/banner-and-video-app.json
new file mode 100644
index 00000000..4b44afcc
--- /dev/null
+++ b/adapters/emx_digital/emx_digitaltest/exemplary/banner-and-video-app.json
@@ -0,0 +1,198 @@
+{
+    "mockBidRequest": {
+      "id": "some_test_auction",
+      "imp": [{
+        "id": "some_test_ad_id_1",
+        "banner": {
+          "format": [{
+            "w": 300,
+            "h": 250
+          }],
+          "w": 300,
+          "h": 250
+        },
+        "ext": {
+          "bidder": {
+            "tagid": "25251"
+          }
+        }
+      },
+      {
+        "id": "some_test_ad_id_2",
+        "video":{
+            "mimes": [
+              "video/mp4",
+              "application/javascript"
+            ],
+            "protocols":[
+               2,
+               3,
+               5,
+               6
+            ],
+            "w":640,
+            "h":480
+        },
+        "ext": {
+          "bidder": {
+            "tagid": "25251"
+          }
+        }
+      }],
+      "device": {
+        "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36",
+        "ip": "123.123.123.123",
+        "dnt": 1
+      },
+      "app": {
+        "domain": "www.publisher.com",
+        "storeurl": "http://www.publisher.com/awesome/site?with=some&parameters=here"
+      }
+    },
+  
+    "httpCalls": [{
+      "expectedRequest": {
+        "uri": "https://hb.emxdgt.com?t=1000&ts=2060541160",
+        "headers": {
+          "Accept": [
+            "application/json"
+          ],
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "X-Forwarded-For": [
+            "123.123.123.123"
+          ],
+          "Dnt": [
+            "1"
+          ],
+          "User-Agent": [
+            "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36"
+          ]
+        },
+        "body": {
+          "id": "some_test_auction",
+          "imp": [{
+            "id": "some_test_ad_id_1",
+            "banner": {
+              "format": [{
+                "w": 300,
+                "h": 250
+              }],
+              "w": 300,
+              "h": 250
+            },
+          "ext": {
+            "bidder": {
+              "tagid": "25251"
+            }
+          },
+            "tagid": "25251",
+            "secure": 0
+          },
+          {
+            "id": "some_test_ad_id_2",
+            "video":{
+                "mimes": [
+                  "video/mp4",
+                  "application/javascript"
+                ],
+                "protocols":[
+                   2,
+                   3,
+                   5,
+                   6
+                ],
+                "w":640,
+                "h":480
+            },
+          "ext": {
+            "bidder": {
+              "tagid": "25251"
+            }
+          },
+            "tagid": "25251",
+            "secure": 0
+          }],
+          "app": {
+            "domain": "www.publisher.com",
+            "storeurl": "http://www.publisher.com/awesome/site?with=some&parameters=here"
+          },
+          "device": {
+            "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36",
+            "ip": "123.123.123.123",
+            "dnt": 1
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "some_test_auction",
+          "seatbid": [
+            {
+              "seat": "12356",
+              "bid": [{
+                "adm": "<div id=\"emximp\"><script>var adm='\\<script\\ src\\=\\'https\\:\\/\\/presentation\\-atl1\\.turn\\.com\\/server\\/ads\\.js\\?code\\=180858515\\&aid\\=49508660\\&tuid\\=3697246472959142409\\&duid\\=3592248512493872937\\&dt\\=a\\&uimpd\\=1OB0KmixN8sHuJc8RPFjxbSZrYW2YZcBuHKg\\-pD5EUJnlrGZTGPpxpl3uYcBYQ7GO937FCfn8dB72mF1jiGRToh8X_UEdA5MP\\-U6ORPQsQ8hmQV0VpZDR7yCaVRZu\\-I8GC4aswLJKm1ehJk2JLqoR8fNGOTt5N02R4_bafcFcCDQehYt_oMETehbgQUsGUuJOUrv\\-rRl46jWaNxtTjLFx01U8qDhPdLKvi2iEiNPYp2pLFfly7qg\\-pjrs0yN8zASsoG5g0qWiuyTjJKZ62irD5SLwF4jQYBxaXIFyF\\-pGADUX2GD6JPzOK9mLaWdX\\-N_fB9gUj8TnVS0WHJ5XJWnCMijjO4qguAQo7LiUftTOQZ66OhXUiNcdI55yMwK0uS_fo2FyHMj2JXiOVpZaJHnWE\\-9QOR3F1E4D9qv9EpOMFAwH1kJqoY\\-G3CBJWC_zUjizw2diGrctqk_tJ9b0jujUs_LmoarsIJcKdWBQoF4xfCjfO7DyaM4l0SBbwLA07GYz0tdDtMd7hbstccVk0PPPVNTOv\\-Pan6yUSHX\\-r\\-U7yDLcFLWxwEaapCh_mKwrATp9oJuOXuK0oIioxIIX9n4raGZPHkl27AX1stb2eIpawf5FkEnwY5YrYIiycI38zuqVAE2ZoI\\-Rn0l8Uckhf1zurAgg6_4AjqZp8oi26jsIHbIc5IsI\\-9UoAoJR_nv_gTojvepaSsA97RYysgopxAW_YzeixMr0LJTvo53DFh4WzrcgaMMPXPf79kdetKmcjPBTPC3bhu9BgI4zAdSobLNCH5BYV1aUcv80K3a1WD7cDZtUXckjWnHt\\-JdzFutuwFnNn66YqhPRxbuWKrW\\-grBHm4T2xv0NxEQIEJxnExroXYCEsPmcki2QTLHbo1fSPgqMF5U4fnfq3\\-3MwNKhtpT4\\-iZN1mzvqeTZPD7TdBWee7k1JXOO_PRyRtwQXaDPQy3HPPvGlRnin_bp26aHCP13HKhn63gh4unBlH7R1xgw0KeaYeBt6C_J9EnIgJQrbpHpwZK0LpM1GSqZ20CbfV891w\\&acp\\=\\$\\{EMX_MACRO\\}\\'\\>\\<\\/script\\><script src=\"https://imp.emxdgt.com/imp/?cp=0.36666&ts=1603222211&adom=www.avis.com&seat=1085&w=320&h=50&pb=0.293328&sid=13835&tid=114507&pid=1764&uid=91441603222210784060e1&wid=15&dom=358899126&tp=${EMX_MACRO}&mt=1&dt=1&st=3&os=&ip=108.17.145.117&sz=&country=US&region=VA&city=Chesapeake&zip=&dma=&agency_id=0&cluster=staging&rf=${RF_MACRO}&cat=1-4&crid=1607428720\"><\\/script>'; window.vc= 'https://imp.emxdgt.com/view/?cp=0.36666&ts=1603222211&adom=www.avis.com&seat=1085&w=320&h=50&pb=0.293328&sid=13835&tid=114507&pid=1764&uid=91441603222210784060e1&wid=15&dom=358899126&tp=${EMX_MACRO}&mt=1&dt=1&st=3&os=&ip=108.17.145.117&sz=&country=US&region=VA&city=Chesapeake&zip=&dma=&agency_id=0&cluster=staging&rf=${RF_MACRO}&cat=1-4&crid=1607428720';  var ap=\"${AUCTION_PRICE}\";var rf=document.domain; window.cp=\"0.36666\";  if(ap == \"test=1\" || ap == \"AUDIT\" || rf == \"s3.amazonaws.com\"){var  finaladm=adm.replace(/\\${EMX_MACRO}/g,\"AUDIT\"); window.audit =  true;}else{var  finaladm=finaladm=adm.replace(/\\${EMX_MACRO}/g,window.cp);}finaladm=finaladm.replace(/\\${RF_MACRO}/g,rf);try{document.write(decodeURI(finaladm));}catch(e){document.write(unescape(finaladm));};</script><script   src=\"https://js.brealtime.com/openvv.js\"></script></div>",
+                "id": "some_test_ad_id_1",
+                "impid": "some_test_ad_id_1",
+                "ttl": 300,
+                "crid": "94395500",
+                "w": 300,
+                "price": 2.942808,
+                "adid": "94395500",
+                "h": 250
+              }]
+          },
+          {
+            "seat": "45678",
+            "bid": [{
+              "adm": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><VAST version=\"2.0\"><Ad id=\"VPAID\"><InLine><Creatives><Creative sequence=\"1\"><Linear><Duration>00:00:15</Duration><AdParameters><![CDATA[{\"adType\":\"VIDEO\",\"ad\":{\"viewTrk\":\"https%3A%2F%2Fimp.emxdgt.com%2Fview%2F%3Fcp%3D30.0000%26ts%3D1583442857%26adom%3Dhttps%3A%2F%2Femxdigital.com%26seat%3D1356%26w%3D5%26h%3D5%26pb%3D24%26sid%3D7732%26tid%3D71423%26pid%3D1158%26uid%3D29451583442857274462e1%26wid%3D1%26dom%3Dwww.brit.co%26tp%3D%24%7BEMX_MACRO%7D%26rf%3D%24%7BRF_MACRO%7D%26cat%3D3-1%26crid%3D209610044\",\"tag\":\"%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%20standalone%3D%22yes%22%3F%3E%3CVAST%20version%3D%223.0%22%3E%3CAd%20id%3D%22209610044%22%3E%3CWrapper%3E%3CAdSystem%20version%3D%223.0%22%3Eadnxs%3C%2FAdSystem%3E%3CVASTAdTagURI%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fnym1-ib.adnxs.com%2Fab%3Fan_audit%3D0%26referrer%3Dhttps%253A%252F%252Fwww.brit.co%252Fthese-actors-cant-escape-their-famous-lines%252F%26e%3DwqT_3QLkCvBDZAUAAAMA1gAFAQir14XzBRCQ2fHY6OTvjT4YhqCpw5iblL0LKjYJR6zFpwAAPkARR6zFpwAAPkAZAAAA4FE4NkAhR6wJGwApESTIMQAAAAAp3CNAMPqI3Qc4zApAzApIAlC8yvljWL_Bc2AAaIrFCni14gSAAQGKAQNVU0SSBQbwPpgBAaABAagBAbABALgBA8ABBMgBAtABCdgBAOABAPABAIoCO3VmKCdhJywgMjg5MTAxLCAxNTgzNDQyODU5KQUcAHIBHCAwOTYxMDA0NCwyHwD0FwGSAt0CIXB6OWpKZ2l0b3NvU0VMekstV01ZQUNDX3dYTXdBRGdBUUFSSXpBcFEtb2pkQjFnQVlMNENhQUJ3QUhnQWdBRUFpQUVBa0FFQW1BRUFvQUVCcUFFRHNBRUF1UUh0QktEMkFBQS1RTUVCN1FTZzlnQUFQa0RKQVZ3MUt3WjloQUJBMlFFQUFBQUFBQUR3UC1BQkFQVUJBQUFBQUpnQ0FLQUNBTFVDQUFBQUFMMENBQUFBQU9BQ0FPZ0NBUGdDQUlBREFaZ0RBYWdEcmFMS0Vyb0RDVTVaVFRJNk5ERXlNLUFEZ1J5SUJBQ1FCQUNZQkFHeUJBb0kwN2JfQ1JDNTRjb013UVFBQUFBQUFBQUFBTWtFQUFBBWcQQUFEUkINCiB3Q0ZBMkFRQTguLAAgSWdGbXlDcEJRDSUUUEFfc1FVDQ0BASwuLpoCiQEhYkJBdko6YQEkdjhGeklBUW9BRBH9XEEtUURvSlRsbE5Nam8wTVRJelFJRWNTUQFJBEFBAV0AVREMDEFBQVcdDABZHQwAYR0MAGMNDAG98LBlQUEu2AKsA-ACiZ1S6gJAaHR0cHM6Ly93d3cuYnJpdC5jby90aGVzZS1hY3RvcnMtY2FudC1lc2NhcGUtdGhlaXItZmFtb3VzLWxpbmVzL_ICEAoGQURWX0lEEgYyODkxMDHyAhIKBkNQR19JRBIIMTExMDg3NznyAhEKBUNQX0lEEggzODk2NTU0OfICDQoIQURWX0ZSRVESATDyAg0KCFJFTV9VU0VSEgEw8gIMCggBIDRDT0RFEgDyAgwKCENQRxUPEAsKB0NQFQ4cCQoFSU9fSUQBKRALCgdJTxUaOBMKD0NVU1RPTV9NT0RFTA0kCBoKFjIWABxMRUFGX05BTQVqCB4KGjYdAAhBU1QBPgxJRklFBWIcDQoIU1BMSVQBTfCLATCAAwCIAwGQAwCYAxSgAwGqAwDAA-CoAcgDANgD3q434AMA6AMA-AMDgAQAkgQJL29wZW5ydGIymAQAogQOMTA0LjE2Mi41MC4yNDWoBAGyBBIIARAQGNwGIOQDKAEoAjAAOAO4BADABADIBIGhggHSBA4xMzU2I05ZTTI6NDEyM9oEAggB4AQA8ASBSgz6BBIJgWgQiFxEQBGBjDDpdVLAiAUBmAUAoAX_EQEgAaoFFjI5NDUxkSc0NzI3NDQ2MmUxwAUAyQUBQRgAAPA_0gUJBU0BAXDYBQHgBQHwBYCvM_oFBAgAEACQBgGYBgC4BgDBBgEhATEg0AbCBNoGFgoQCREZAVAQABgA4AYE8gYCCACABwGIBwCgB0A.%26s%3Dc26c363d17931ce09fc5383e6f8aa1ff7e9324ca%26pp%3D%24%7BEMX_MACRO%7D%5D%5D%3E%3C%2FVASTAdTagURI%3E%3CImpression%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fib.adnxs.com%2Fnop%5D%5D%3E%3C%2FImpression%3E%3CCreatives%3E%3CCreative%20adID%3D%22209610044%22%3E%3CLinear%3E%3C%2FLinear%3E%3C%2FCreative%3E%3C%2FCreatives%3E%3CImpression%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fimp.emxdgt.com%2Fimp%2F%3Fcp%3D30.0000%26ts%3D1583442857%26adom%3Dhttps%3A%2F%2Femxdigital.com%26seat%3D1356%26w%3D5%26h%3D5%26pb%3D24%26sid%3D7732%26tid%3D71423%26pid%3D1158%26uid%3D29451583442857274462e1%26wid%3D1%26dom%3Dwww.brit.co%26tp%3D%24%7BEMX_MACRO%7D%26rf%3D%24%7BRF_MACRO%7D%26cat%3D3-1%26crid%3D209610044%5D%5D%3E%3C%2FImpression%3E%3C%2FWrapper%3E%3C%2FAd%3E%3C%2FVAST%3E\",\"avg\": \"30.0000\", \"auction\" : \"${AUCTION_PRICE}\"}}]]></AdParameters><MediaFiles><MediaFile delivery=\"progressive\" type=\"application/javascript\" apiFramework=\"VPAID\"><![CDATA[https://js.brealtime.com/ovvbundle-vcr_1.1.js]]></MediaFile></MediaFiles></Linear></Creative></Creatives></InLine></Ad></VAST>",
+              "id": "some_test_ad_id_2",
+              "impid": "some_test_ad_id_2",
+              "ttl": 300,
+              "crid": "9999999",
+              "w": 1020,
+              "price": 1,
+              "adid": "9999999",
+              "h": 1000
+            }
+          ]
+        }],
+          "cur": "USD"
+      }
+      }
+    }],
+  
+    "expectedBids": [
+      {
+      "bid": {
+        "adm": "<div id=\"emximp\"><script>var adm='\\<script\\ src\\=\\'https\\:\\/\\/presentation\\-atl1\\.turn\\.com\\/server\\/ads\\.js\\?code\\=180858515\\&aid\\=49508660\\&tuid\\=3697246472959142409\\&duid\\=3592248512493872937\\&dt\\=a\\&uimpd\\=1OB0KmixN8sHuJc8RPFjxbSZrYW2YZcBuHKg\\-pD5EUJnlrGZTGPpxpl3uYcBYQ7GO937FCfn8dB72mF1jiGRToh8X_UEdA5MP\\-U6ORPQsQ8hmQV0VpZDR7yCaVRZu\\-I8GC4aswLJKm1ehJk2JLqoR8fNGOTt5N02R4_bafcFcCDQehYt_oMETehbgQUsGUuJOUrv\\-rRl46jWaNxtTjLFx01U8qDhPdLKvi2iEiNPYp2pLFfly7qg\\-pjrs0yN8zASsoG5g0qWiuyTjJKZ62irD5SLwF4jQYBxaXIFyF\\-pGADUX2GD6JPzOK9mLaWdX\\-N_fB9gUj8TnVS0WHJ5XJWnCMijjO4qguAQo7LiUftTOQZ66OhXUiNcdI55yMwK0uS_fo2FyHMj2JXiOVpZaJHnWE\\-9QOR3F1E4D9qv9EpOMFAwH1kJqoY\\-G3CBJWC_zUjizw2diGrctqk_tJ9b0jujUs_LmoarsIJcKdWBQoF4xfCjfO7DyaM4l0SBbwLA07GYz0tdDtMd7hbstccVk0PPPVNTOv\\-Pan6yUSHX\\-r\\-U7yDLcFLWxwEaapCh_mKwrATp9oJuOXuK0oIioxIIX9n4raGZPHkl27AX1stb2eIpawf5FkEnwY5YrYIiycI38zuqVAE2ZoI\\-Rn0l8Uckhf1zurAgg6_4AjqZp8oi26jsIHbIc5IsI\\-9UoAoJR_nv_gTojvepaSsA97RYysgopxAW_YzeixMr0LJTvo53DFh4WzrcgaMMPXPf79kdetKmcjPBTPC3bhu9BgI4zAdSobLNCH5BYV1aUcv80K3a1WD7cDZtUXckjWnHt\\-JdzFutuwFnNn66YqhPRxbuWKrW\\-grBHm4T2xv0NxEQIEJxnExroXYCEsPmcki2QTLHbo1fSPgqMF5U4fnfq3\\-3MwNKhtpT4\\-iZN1mzvqeTZPD7TdBWee7k1JXOO_PRyRtwQXaDPQy3HPPvGlRnin_bp26aHCP13HKhn63gh4unBlH7R1xgw0KeaYeBt6C_J9EnIgJQrbpHpwZK0LpM1GSqZ20CbfV891w\\&acp\\=\\$\\{EMX_MACRO\\}\\'\\>\\<\\/script\\><script src=\"https://imp.emxdgt.com/imp/?cp=0.36666&ts=1603222211&adom=www.avis.com&seat=1085&w=320&h=50&pb=0.293328&sid=13835&tid=114507&pid=1764&uid=91441603222210784060e1&wid=15&dom=358899126&tp=${EMX_MACRO}&mt=1&dt=1&st=3&os=&ip=108.17.145.117&sz=&country=US&region=VA&city=Chesapeake&zip=&dma=&agency_id=0&cluster=staging&rf=${RF_MACRO}&cat=1-4&crid=1607428720\"><\\/script>'; window.vc= 'https://imp.emxdgt.com/view/?cp=0.36666&ts=1603222211&adom=www.avis.com&seat=1085&w=320&h=50&pb=0.293328&sid=13835&tid=114507&pid=1764&uid=91441603222210784060e1&wid=15&dom=358899126&tp=${EMX_MACRO}&mt=1&dt=1&st=3&os=&ip=108.17.145.117&sz=&country=US&region=VA&city=Chesapeake&zip=&dma=&agency_id=0&cluster=staging&rf=${RF_MACRO}&cat=1-4&crid=1607428720';  var ap=\"${AUCTION_PRICE}\";var rf=document.domain; window.cp=\"0.36666\";  if(ap == \"test=1\" || ap == \"AUDIT\" || rf == \"s3.amazonaws.com\"){var  finaladm=adm.replace(/\\${EMX_MACRO}/g,\"AUDIT\"); window.audit =  true;}else{var  finaladm=finaladm=adm.replace(/\\${EMX_MACRO}/g,window.cp);}finaladm=finaladm.replace(/\\${RF_MACRO}/g,rf);try{document.write(decodeURI(finaladm));}catch(e){document.write(unescape(finaladm));};</script><script   src=\"https://js.brealtime.com/openvv.js\"></script></div>",
+        "id": "some_test_ad_id_1",
+        "impid": "some_test_ad_id_1",
+        "ttl": 300,
+        "crid": "94395500",
+        "w": 300,
+        "price": 2.942808,
+        "adid": "94395500",
+        "h": 250
+      },
+      "type": "banner"
+    },
+    {
+      "bid": {
+        "adm": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><VAST version=\"2.0\"><Ad id=\"VPAID\"><InLine><Creatives><Creative sequence=\"1\"><Linear><Duration>00:00:15</Duration><AdParameters><![CDATA[{\"adType\":\"VIDEO\",\"ad\":{\"viewTrk\":\"https%3A%2F%2Fimp.emxdgt.com%2Fview%2F%3Fcp%3D30.0000%26ts%3D1583442857%26adom%3Dhttps%3A%2F%2Femxdigital.com%26seat%3D1356%26w%3D5%26h%3D5%26pb%3D24%26sid%3D7732%26tid%3D71423%26pid%3D1158%26uid%3D29451583442857274462e1%26wid%3D1%26dom%3Dwww.brit.co%26tp%3D%24%7BEMX_MACRO%7D%26rf%3D%24%7BRF_MACRO%7D%26cat%3D3-1%26crid%3D209610044\",\"tag\":\"%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%20standalone%3D%22yes%22%3F%3E%3CVAST%20version%3D%223.0%22%3E%3CAd%20id%3D%22209610044%22%3E%3CWrapper%3E%3CAdSystem%20version%3D%223.0%22%3Eadnxs%3C%2FAdSystem%3E%3CVASTAdTagURI%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fnym1-ib.adnxs.com%2Fab%3Fan_audit%3D0%26referrer%3Dhttps%253A%252F%252Fwww.brit.co%252Fthese-actors-cant-escape-their-famous-lines%252F%26e%3DwqT_3QLkCvBDZAUAAAMA1gAFAQir14XzBRCQ2fHY6OTvjT4YhqCpw5iblL0LKjYJR6zFpwAAPkARR6zFpwAAPkAZAAAA4FE4NkAhR6wJGwApESTIMQAAAAAp3CNAMPqI3Qc4zApAzApIAlC8yvljWL_Bc2AAaIrFCni14gSAAQGKAQNVU0SSBQbwPpgBAaABAagBAbABALgBA8ABBMgBAtABCdgBAOABAPABAIoCO3VmKCdhJywgMjg5MTAxLCAxNTgzNDQyODU5KQUcAHIBHCAwOTYxMDA0NCwyHwD0FwGSAt0CIXB6OWpKZ2l0b3NvU0VMekstV01ZQUNDX3dYTXdBRGdBUUFSSXpBcFEtb2pkQjFnQVlMNENhQUJ3QUhnQWdBRUFpQUVBa0FFQW1BRUFvQUVCcUFFRHNBRUF1UUh0QktEMkFBQS1RTUVCN1FTZzlnQUFQa0RKQVZ3MUt3WjloQUJBMlFFQUFBQUFBQUR3UC1BQkFQVUJBQUFBQUpnQ0FLQUNBTFVDQUFBQUFMMENBQUFBQU9BQ0FPZ0NBUGdDQUlBREFaZ0RBYWdEcmFMS0Vyb0RDVTVaVFRJNk5ERXlNLUFEZ1J5SUJBQ1FCQUNZQkFHeUJBb0kwN2JfQ1JDNTRjb013UVFBQUFBQUFBQUFBTWtFQUFBBWcQQUFEUkINCiB3Q0ZBMkFRQTguLAAgSWdGbXlDcEJRDSUUUEFfc1FVDQ0BASwuLpoCiQEhYkJBdko6YQEkdjhGeklBUW9BRBH9XEEtUURvSlRsbE5Nam8wTVRJelFJRWNTUQFJBEFBAV0AVREMDEFBQVcdDABZHQwAYR0MAGMNDAG98LBlQUEu2AKsA-ACiZ1S6gJAaHR0cHM6Ly93d3cuYnJpdC5jby90aGVzZS1hY3RvcnMtY2FudC1lc2NhcGUtdGhlaXItZmFtb3VzLWxpbmVzL_ICEAoGQURWX0lEEgYyODkxMDHyAhIKBkNQR19JRBIIMTExMDg3NznyAhEKBUNQX0lEEggzODk2NTU0OfICDQoIQURWX0ZSRVESATDyAg0KCFJFTV9VU0VSEgEw8gIMCggBIDRDT0RFEgDyAgwKCENQRxUPEAsKB0NQFQ4cCQoFSU9fSUQBKRALCgdJTxUaOBMKD0NVU1RPTV9NT0RFTA0kCBoKFjIWABxMRUFGX05BTQVqCB4KGjYdAAhBU1QBPgxJRklFBWIcDQoIU1BMSVQBTfCLATCAAwCIAwGQAwCYAxSgAwGqAwDAA-CoAcgDANgD3q434AMA6AMA-AMDgAQAkgQJL29wZW5ydGIymAQAogQOMTA0LjE2Mi41MC4yNDWoBAGyBBIIARAQGNwGIOQDKAEoAjAAOAO4BADABADIBIGhggHSBA4xMzU2I05ZTTI6NDEyM9oEAggB4AQA8ASBSgz6BBIJgWgQiFxEQBGBjDDpdVLAiAUBmAUAoAX_EQEgAaoFFjI5NDUxkSc0NzI3NDQ2MmUxwAUAyQUBQRgAAPA_0gUJBU0BAXDYBQHgBQHwBYCvM_oFBAgAEACQBgGYBgC4BgDBBgEhATEg0AbCBNoGFgoQCREZAVAQABgA4AYE8gYCCACABwGIBwCgB0A.%26s%3Dc26c363d17931ce09fc5383e6f8aa1ff7e9324ca%26pp%3D%24%7BEMX_MACRO%7D%5D%5D%3E%3C%2FVASTAdTagURI%3E%3CImpression%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fib.adnxs.com%2Fnop%5D%5D%3E%3C%2FImpression%3E%3CCreatives%3E%3CCreative%20adID%3D%22209610044%22%3E%3CLinear%3E%3C%2FLinear%3E%3C%2FCreative%3E%3C%2FCreatives%3E%3CImpression%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fimp.emxdgt.com%2Fimp%2F%3Fcp%3D30.0000%26ts%3D1583442857%26adom%3Dhttps%3A%2F%2Femxdigital.com%26seat%3D1356%26w%3D5%26h%3D5%26pb%3D24%26sid%3D7732%26tid%3D71423%26pid%3D1158%26uid%3D29451583442857274462e1%26wid%3D1%26dom%3Dwww.brit.co%26tp%3D%24%7BEMX_MACRO%7D%26rf%3D%24%7BRF_MACRO%7D%26cat%3D3-1%26crid%3D209610044%5D%5D%3E%3C%2FImpression%3E%3C%2FWrapper%3E%3C%2FAd%3E%3C%2FVAST%3E\",\"avg\": \"30.0000\", \"auction\" : \"${AUCTION_PRICE}\"}}]]></AdParameters><MediaFiles><MediaFile delivery=\"progressive\" type=\"application/javascript\" apiFramework=\"VPAID\"><![CDATA[https://js.brealtime.com/ovvbundle-vcr_1.1.js]]></MediaFile></MediaFiles></Linear></Creative></Creatives></InLine></Ad></VAST>",
+        "id": "some_test_ad_id_2",
+        "impid": "some_test_ad_id_2",
+        "ttl": 300,
+        "crid": "9999999",
+        "w": 1020,
+        "price": 1,
+        "adid": "9999999",
+        "h": 1000
+      },
+      "type": "video"
+    }
+  ]
+}
+  
\ No newline at end of file
diff --git a/adapters/emx_digital/emx_digitaltest/exemplary/banner-and-video-site.json b/adapters/emx_digital/emx_digitaltest/exemplary/banner-and-video-site.json
new file mode 100644
index 00000000..c2b20cf1
--- /dev/null
+++ b/adapters/emx_digital/emx_digitaltest/exemplary/banner-and-video-site.json
@@ -0,0 +1,200 @@
+{
+    "mockBidRequest": {
+      "id": "some_test_auction",
+      "imp": [{
+        "id": "some_test_ad_id_1",
+        "banner": {
+          "format": [{
+            "w": 300,
+            "h": 250
+          }],
+          "w": 300,
+          "h": 250
+        },
+        "ext": {
+          "bidder": {
+            "tagid": "25251"
+          }
+        }
+      },
+      {
+        "id": "some_test_ad_id_2",
+        "video":{
+            "mimes": [
+              "video/mp4",
+              "application/javascript"
+            ],
+            "protocols":[
+               2,
+               3,
+               5,
+               6
+            ],
+            "w":640,
+            "h":480
+        },
+        "ext": {
+          "bidder": {
+            "tagid": "25251"
+          }
+        }
+      }
+    ],
+      "device": {
+        "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36",
+        "ip": "123.123.123.123",
+        "dnt": 1
+      },
+      "site": {
+        "domain": "www.publisher.com",
+        "page": "http://www.publisher.com/awesome/site?with=some&parameters=here"
+      }
+    },
+  
+    "httpCalls": [{
+      "expectedRequest": {
+        "uri": "https://hb.emxdgt.com?t=1000&ts=2060541160",
+        "headers": {
+          "Accept": [
+            "application/json"
+          ],
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "X-Forwarded-For": [
+            "123.123.123.123"
+          ],
+          "Referer": [
+            "http://www.publisher.com/awesome/site?with=some&parameters=here"
+          ],
+          "Dnt": [
+            "1"
+          ],
+          "User-Agent": [
+            "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36"
+          ]
+        },
+        "body": {
+          "id": "some_test_auction",
+          "imp": [{
+            "id": "some_test_ad_id_1",
+            "banner": {
+              "format": [{
+                "w": 300,
+                "h": 250
+              }],
+              "w": 300,
+              "h": 250
+            },
+            "ext": {
+              "bidder": {
+                "tagid": "25251"
+              }
+            },
+            "tagid": "25251",
+            "secure": 0
+          },
+          {
+            "id": "some_test_ad_id_2",
+            "video":{
+                "mimes": [
+                  "video/mp4",
+                  "application/javascript"
+                ],
+                "protocols":[
+                   2,
+                   3,
+                   5,
+                   6
+                ],
+                "w":640,
+                "h":480
+            },
+            "ext": {
+              "bidder": {
+                "tagid": "25251"
+              }
+            },
+            "tagid": "25251",
+            "secure": 0
+          }],
+          "site": {
+            "domain": "www.publisher.com",
+            "page": "http://www.publisher.com/awesome/site?with=some&parameters=here"
+          },
+          "device": {
+            "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36",
+            "ip": "123.123.123.123",
+            "dnt": 1
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "some_test_auction",
+          "seatbid": [{
+              "seat": "12356",
+              "bid": [{
+                "adm": "<div id=\"emximp\"><script>var adm='\\<script\\ src\\=\\'https\\:\\/\\/presentation\\-atl1\\.turn\\.com\\/server\\/ads\\.js\\?code\\=180858515\\&aid\\=49508660\\&tuid\\=3697246472959142409\\&duid\\=3592248512493872937\\&dt\\=a\\&uimpd\\=1OB0KmixN8sHuJc8RPFjxbSZrYW2YZcBuHKg\\-pD5EUJnlrGZTGPpxpl3uYcBYQ7GO937FCfn8dB72mF1jiGRToh8X_UEdA5MP\\-U6ORPQsQ8hmQV0VpZDR7yCaVRZu\\-I8GC4aswLJKm1ehJk2JLqoR8fNGOTt5N02R4_bafcFcCDQehYt_oMETehbgQUsGUuJOUrv\\-rRl46jWaNxtTjLFx01U8qDhPdLKvi2iEiNPYp2pLFfly7qg\\-pjrs0yN8zASsoG5g0qWiuyTjJKZ62irD5SLwF4jQYBxaXIFyF\\-pGADUX2GD6JPzOK9mLaWdX\\-N_fB9gUj8TnVS0WHJ5XJWnCMijjO4qguAQo7LiUftTOQZ66OhXUiNcdI55yMwK0uS_fo2FyHMj2JXiOVpZaJHnWE\\-9QOR3F1E4D9qv9EpOMFAwH1kJqoY\\-G3CBJWC_zUjizw2diGrctqk_tJ9b0jujUs_LmoarsIJcKdWBQoF4xfCjfO7DyaM4l0SBbwLA07GYz0tdDtMd7hbstccVk0PPPVNTOv\\-Pan6yUSHX\\-r\\-U7yDLcFLWxwEaapCh_mKwrATp9oJuOXuK0oIioxIIX9n4raGZPHkl27AX1stb2eIpawf5FkEnwY5YrYIiycI38zuqVAE2ZoI\\-Rn0l8Uckhf1zurAgg6_4AjqZp8oi26jsIHbIc5IsI\\-9UoAoJR_nv_gTojvepaSsA97RYysgopxAW_YzeixMr0LJTvo53DFh4WzrcgaMMPXPf79kdetKmcjPBTPC3bhu9BgI4zAdSobLNCH5BYV1aUcv80K3a1WD7cDZtUXckjWnHt\\-JdzFutuwFnNn66YqhPRxbuWKrW\\-grBHm4T2xv0NxEQIEJxnExroXYCEsPmcki2QTLHbo1fSPgqMF5U4fnfq3\\-3MwNKhtpT4\\-iZN1mzvqeTZPD7TdBWee7k1JXOO_PRyRtwQXaDPQy3HPPvGlRnin_bp26aHCP13HKhn63gh4unBlH7R1xgw0KeaYeBt6C_J9EnIgJQrbpHpwZK0LpM1GSqZ20CbfV891w\\&acp\\=\\$\\{EMX_MACRO\\}\\'\\>\\<\\/script\\><script src=\"https://imp.emxdgt.com/imp/?cp=0.36666&ts=1603222211&adom=www.avis.com&seat=1085&w=320&h=50&pb=0.293328&sid=13835&tid=114507&pid=1764&uid=91441603222210784060e1&wid=15&dom=358899126&tp=${EMX_MACRO}&mt=1&dt=1&st=3&os=&ip=108.17.145.117&sz=&country=US&region=VA&city=Chesapeake&zip=&dma=&agency_id=0&cluster=staging&rf=${RF_MACRO}&cat=1-4&crid=1607428720\"><\\/script>'; window.vc= 'https://imp.emxdgt.com/view/?cp=0.36666&ts=1603222211&adom=www.avis.com&seat=1085&w=320&h=50&pb=0.293328&sid=13835&tid=114507&pid=1764&uid=91441603222210784060e1&wid=15&dom=358899126&tp=${EMX_MACRO}&mt=1&dt=1&st=3&os=&ip=108.17.145.117&sz=&country=US&region=VA&city=Chesapeake&zip=&dma=&agency_id=0&cluster=staging&rf=${RF_MACRO}&cat=1-4&crid=1607428720';  var ap=\"${AUCTION_PRICE}\";var rf=document.domain; window.cp=\"0.36666\";  if(ap == \"test=1\" || ap == \"AUDIT\" || rf == \"s3.amazonaws.com\"){var  finaladm=adm.replace(/\\${EMX_MACRO}/g,\"AUDIT\"); window.audit =  true;}else{var  finaladm=finaladm=adm.replace(/\\${EMX_MACRO}/g,window.cp);}finaladm=finaladm.replace(/\\${RF_MACRO}/g,rf);try{document.write(decodeURI(finaladm));}catch(e){document.write(unescape(finaladm));};</script><script   src=\"https://js.brealtime.com/openvv.js\"></script></div>",
+                "id": "some_test_ad_id_1",
+                "impid": "some_test_ad_id_1",
+                "ttl": 300,
+                "crid": "94395500",
+                "w": 300,
+                "price": 2.942808,
+                "adid": "94395500",
+                "h": 250
+              }]
+          },
+          {
+            "seat": "45678",
+            "bid": [{
+              "adm": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><VAST version=\"2.0\"><Ad id=\"VPAID\"><InLine><Creatives><Creative sequence=\"1\"><Linear><Duration>00:00:15</Duration><AdParameters><![CDATA[{\"adType\":\"VIDEO\",\"ad\":{\"viewTrk\":\"https%3A%2F%2Fimp.emxdgt.com%2Fview%2F%3Fcp%3D30.0000%26ts%3D1583442857%26adom%3Dhttps%3A%2F%2Femxdigital.com%26seat%3D1356%26w%3D5%26h%3D5%26pb%3D24%26sid%3D7732%26tid%3D71423%26pid%3D1158%26uid%3D29451583442857274462e1%26wid%3D1%26dom%3Dwww.brit.co%26tp%3D%24%7BEMX_MACRO%7D%26rf%3D%24%7BRF_MACRO%7D%26cat%3D3-1%26crid%3D209610044\",\"tag\":\"%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%20standalone%3D%22yes%22%3F%3E%3CVAST%20version%3D%223.0%22%3E%3CAd%20id%3D%22209610044%22%3E%3CWrapper%3E%3CAdSystem%20version%3D%223.0%22%3Eadnxs%3C%2FAdSystem%3E%3CVASTAdTagURI%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fnym1-ib.adnxs.com%2Fab%3Fan_audit%3D0%26referrer%3Dhttps%253A%252F%252Fwww.brit.co%252Fthese-actors-cant-escape-their-famous-lines%252F%26e%3DwqT_3QLkCvBDZAUAAAMA1gAFAQir14XzBRCQ2fHY6OTvjT4YhqCpw5iblL0LKjYJR6zFpwAAPkARR6zFpwAAPkAZAAAA4FE4NkAhR6wJGwApESTIMQAAAAAp3CNAMPqI3Qc4zApAzApIAlC8yvljWL_Bc2AAaIrFCni14gSAAQGKAQNVU0SSBQbwPpgBAaABAagBAbABALgBA8ABBMgBAtABCdgBAOABAPABAIoCO3VmKCdhJywgMjg5MTAxLCAxNTgzNDQyODU5KQUcAHIBHCAwOTYxMDA0NCwyHwD0FwGSAt0CIXB6OWpKZ2l0b3NvU0VMekstV01ZQUNDX3dYTXdBRGdBUUFSSXpBcFEtb2pkQjFnQVlMNENhQUJ3QUhnQWdBRUFpQUVBa0FFQW1BRUFvQUVCcUFFRHNBRUF1UUh0QktEMkFBQS1RTUVCN1FTZzlnQUFQa0RKQVZ3MUt3WjloQUJBMlFFQUFBQUFBQUR3UC1BQkFQVUJBQUFBQUpnQ0FLQUNBTFVDQUFBQUFMMENBQUFBQU9BQ0FPZ0NBUGdDQUlBREFaZ0RBYWdEcmFMS0Vyb0RDVTVaVFRJNk5ERXlNLUFEZ1J5SUJBQ1FCQUNZQkFHeUJBb0kwN2JfQ1JDNTRjb013UVFBQUFBQUFBQUFBTWtFQUFBBWcQQUFEUkINCiB3Q0ZBMkFRQTguLAAgSWdGbXlDcEJRDSUUUEFfc1FVDQ0BASwuLpoCiQEhYkJBdko6YQEkdjhGeklBUW9BRBH9XEEtUURvSlRsbE5Nam8wTVRJelFJRWNTUQFJBEFBAV0AVREMDEFBQVcdDABZHQwAYR0MAGMNDAG98LBlQUEu2AKsA-ACiZ1S6gJAaHR0cHM6Ly93d3cuYnJpdC5jby90aGVzZS1hY3RvcnMtY2FudC1lc2NhcGUtdGhlaXItZmFtb3VzLWxpbmVzL_ICEAoGQURWX0lEEgYyODkxMDHyAhIKBkNQR19JRBIIMTExMDg3NznyAhEKBUNQX0lEEggzODk2NTU0OfICDQoIQURWX0ZSRVESATDyAg0KCFJFTV9VU0VSEgEw8gIMCggBIDRDT0RFEgDyAgwKCENQRxUPEAsKB0NQFQ4cCQoFSU9fSUQBKRALCgdJTxUaOBMKD0NVU1RPTV9NT0RFTA0kCBoKFjIWABxMRUFGX05BTQVqCB4KGjYdAAhBU1QBPgxJRklFBWIcDQoIU1BMSVQBTfCLATCAAwCIAwGQAwCYAxSgAwGqAwDAA-CoAcgDANgD3q434AMA6AMA-AMDgAQAkgQJL29wZW5ydGIymAQAogQOMTA0LjE2Mi41MC4yNDWoBAGyBBIIARAQGNwGIOQDKAEoAjAAOAO4BADABADIBIGhggHSBA4xMzU2I05ZTTI6NDEyM9oEAggB4AQA8ASBSgz6BBIJgWgQiFxEQBGBjDDpdVLAiAUBmAUAoAX_EQEgAaoFFjI5NDUxkSc0NzI3NDQ2MmUxwAUAyQUBQRgAAPA_0gUJBU0BAXDYBQHgBQHwBYCvM_oFBAgAEACQBgGYBgC4BgDBBgEhATEg0AbCBNoGFgoQCREZAVAQABgA4AYE8gYCCACABwGIBwCgB0A.%26s%3Dc26c363d17931ce09fc5383e6f8aa1ff7e9324ca%26pp%3D%24%7BEMX_MACRO%7D%5D%5D%3E%3C%2FVASTAdTagURI%3E%3CImpression%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fib.adnxs.com%2Fnop%5D%5D%3E%3C%2FImpression%3E%3CCreatives%3E%3CCreative%20adID%3D%22209610044%22%3E%3CLinear%3E%3C%2FLinear%3E%3C%2FCreative%3E%3C%2FCreatives%3E%3CImpression%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fimp.emxdgt.com%2Fimp%2F%3Fcp%3D30.0000%26ts%3D1583442857%26adom%3Dhttps%3A%2F%2Femxdigital.com%26seat%3D1356%26w%3D5%26h%3D5%26pb%3D24%26sid%3D7732%26tid%3D71423%26pid%3D1158%26uid%3D29451583442857274462e1%26wid%3D1%26dom%3Dwww.brit.co%26tp%3D%24%7BEMX_MACRO%7D%26rf%3D%24%7BRF_MACRO%7D%26cat%3D3-1%26crid%3D209610044%5D%5D%3E%3C%2FImpression%3E%3C%2FWrapper%3E%3C%2FAd%3E%3C%2FVAST%3E\",\"avg\": \"30.0000\", \"auction\" : \"${AUCTION_PRICE}\"}}]]></AdParameters><MediaFiles><MediaFile delivery=\"progressive\" type=\"application/javascript\" apiFramework=\"VPAID\"><![CDATA[https://js.brealtime.com/ovvbundle-vcr_1.1.js]]></MediaFile></MediaFiles></Linear></Creative></Creatives></InLine></Ad></VAST>",
+              "id": "some_test_ad_id_2",
+              "impid": "some_test_ad_id_2",
+              "ttl": 300,
+              "crid": "9999999",
+              "w": 1020,
+              "price": 1,
+              "adid": "9999999",
+              "h": 1000
+            }
+          ]
+        }],
+          "cur": "USD"
+      }
+      }
+    }],
+  
+    "expectedBids": [{
+      "bid": {
+        "adm": "<div id=\"emximp\"><script>var adm='\\<script\\ src\\=\\'https\\:\\/\\/presentation\\-atl1\\.turn\\.com\\/server\\/ads\\.js\\?code\\=180858515\\&aid\\=49508660\\&tuid\\=3697246472959142409\\&duid\\=3592248512493872937\\&dt\\=a\\&uimpd\\=1OB0KmixN8sHuJc8RPFjxbSZrYW2YZcBuHKg\\-pD5EUJnlrGZTGPpxpl3uYcBYQ7GO937FCfn8dB72mF1jiGRToh8X_UEdA5MP\\-U6ORPQsQ8hmQV0VpZDR7yCaVRZu\\-I8GC4aswLJKm1ehJk2JLqoR8fNGOTt5N02R4_bafcFcCDQehYt_oMETehbgQUsGUuJOUrv\\-rRl46jWaNxtTjLFx01U8qDhPdLKvi2iEiNPYp2pLFfly7qg\\-pjrs0yN8zASsoG5g0qWiuyTjJKZ62irD5SLwF4jQYBxaXIFyF\\-pGADUX2GD6JPzOK9mLaWdX\\-N_fB9gUj8TnVS0WHJ5XJWnCMijjO4qguAQo7LiUftTOQZ66OhXUiNcdI55yMwK0uS_fo2FyHMj2JXiOVpZaJHnWE\\-9QOR3F1E4D9qv9EpOMFAwH1kJqoY\\-G3CBJWC_zUjizw2diGrctqk_tJ9b0jujUs_LmoarsIJcKdWBQoF4xfCjfO7DyaM4l0SBbwLA07GYz0tdDtMd7hbstccVk0PPPVNTOv\\-Pan6yUSHX\\-r\\-U7yDLcFLWxwEaapCh_mKwrATp9oJuOXuK0oIioxIIX9n4raGZPHkl27AX1stb2eIpawf5FkEnwY5YrYIiycI38zuqVAE2ZoI\\-Rn0l8Uckhf1zurAgg6_4AjqZp8oi26jsIHbIc5IsI\\-9UoAoJR_nv_gTojvepaSsA97RYysgopxAW_YzeixMr0LJTvo53DFh4WzrcgaMMPXPf79kdetKmcjPBTPC3bhu9BgI4zAdSobLNCH5BYV1aUcv80K3a1WD7cDZtUXckjWnHt\\-JdzFutuwFnNn66YqhPRxbuWKrW\\-grBHm4T2xv0NxEQIEJxnExroXYCEsPmcki2QTLHbo1fSPgqMF5U4fnfq3\\-3MwNKhtpT4\\-iZN1mzvqeTZPD7TdBWee7k1JXOO_PRyRtwQXaDPQy3HPPvGlRnin_bp26aHCP13HKhn63gh4unBlH7R1xgw0KeaYeBt6C_J9EnIgJQrbpHpwZK0LpM1GSqZ20CbfV891w\\&acp\\=\\$\\{EMX_MACRO\\}\\'\\>\\<\\/script\\><script src=\"https://imp.emxdgt.com/imp/?cp=0.36666&ts=1603222211&adom=www.avis.com&seat=1085&w=320&h=50&pb=0.293328&sid=13835&tid=114507&pid=1764&uid=91441603222210784060e1&wid=15&dom=358899126&tp=${EMX_MACRO}&mt=1&dt=1&st=3&os=&ip=108.17.145.117&sz=&country=US&region=VA&city=Chesapeake&zip=&dma=&agency_id=0&cluster=staging&rf=${RF_MACRO}&cat=1-4&crid=1607428720\"><\\/script>'; window.vc= 'https://imp.emxdgt.com/view/?cp=0.36666&ts=1603222211&adom=www.avis.com&seat=1085&w=320&h=50&pb=0.293328&sid=13835&tid=114507&pid=1764&uid=91441603222210784060e1&wid=15&dom=358899126&tp=${EMX_MACRO}&mt=1&dt=1&st=3&os=&ip=108.17.145.117&sz=&country=US&region=VA&city=Chesapeake&zip=&dma=&agency_id=0&cluster=staging&rf=${RF_MACRO}&cat=1-4&crid=1607428720';  var ap=\"${AUCTION_PRICE}\";var rf=document.domain; window.cp=\"0.36666\";  if(ap == \"test=1\" || ap == \"AUDIT\" || rf == \"s3.amazonaws.com\"){var  finaladm=adm.replace(/\\${EMX_MACRO}/g,\"AUDIT\"); window.audit =  true;}else{var  finaladm=finaladm=adm.replace(/\\${EMX_MACRO}/g,window.cp);}finaladm=finaladm.replace(/\\${RF_MACRO}/g,rf);try{document.write(decodeURI(finaladm));}catch(e){document.write(unescape(finaladm));};</script><script   src=\"https://js.brealtime.com/openvv.js\"></script></div>",
+        "id": "some_test_ad_id_1",
+        "impid": "some_test_ad_id_1",
+        "ttl": 300,
+        "crid": "94395500",
+        "w": 300,
+        "price": 2.942808,
+        "adid": "94395500",
+        "h": 250
+      },
+      "type": "banner"
+    },
+    {
+      "bid": {
+        "adm": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><VAST version=\"2.0\"><Ad id=\"VPAID\"><InLine><Creatives><Creative sequence=\"1\"><Linear><Duration>00:00:15</Duration><AdParameters><![CDATA[{\"adType\":\"VIDEO\",\"ad\":{\"viewTrk\":\"https%3A%2F%2Fimp.emxdgt.com%2Fview%2F%3Fcp%3D30.0000%26ts%3D1583442857%26adom%3Dhttps%3A%2F%2Femxdigital.com%26seat%3D1356%26w%3D5%26h%3D5%26pb%3D24%26sid%3D7732%26tid%3D71423%26pid%3D1158%26uid%3D29451583442857274462e1%26wid%3D1%26dom%3Dwww.brit.co%26tp%3D%24%7BEMX_MACRO%7D%26rf%3D%24%7BRF_MACRO%7D%26cat%3D3-1%26crid%3D209610044\",\"tag\":\"%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%20standalone%3D%22yes%22%3F%3E%3CVAST%20version%3D%223.0%22%3E%3CAd%20id%3D%22209610044%22%3E%3CWrapper%3E%3CAdSystem%20version%3D%223.0%22%3Eadnxs%3C%2FAdSystem%3E%3CVASTAdTagURI%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fnym1-ib.adnxs.com%2Fab%3Fan_audit%3D0%26referrer%3Dhttps%253A%252F%252Fwww.brit.co%252Fthese-actors-cant-escape-their-famous-lines%252F%26e%3DwqT_3QLkCvBDZAUAAAMA1gAFAQir14XzBRCQ2fHY6OTvjT4YhqCpw5iblL0LKjYJR6zFpwAAPkARR6zFpwAAPkAZAAAA4FE4NkAhR6wJGwApESTIMQAAAAAp3CNAMPqI3Qc4zApAzApIAlC8yvljWL_Bc2AAaIrFCni14gSAAQGKAQNVU0SSBQbwPpgBAaABAagBAbABALgBA8ABBMgBAtABCdgBAOABAPABAIoCO3VmKCdhJywgMjg5MTAxLCAxNTgzNDQyODU5KQUcAHIBHCAwOTYxMDA0NCwyHwD0FwGSAt0CIXB6OWpKZ2l0b3NvU0VMekstV01ZQUNDX3dYTXdBRGdBUUFSSXpBcFEtb2pkQjFnQVlMNENhQUJ3QUhnQWdBRUFpQUVBa0FFQW1BRUFvQUVCcUFFRHNBRUF1UUh0QktEMkFBQS1RTUVCN1FTZzlnQUFQa0RKQVZ3MUt3WjloQUJBMlFFQUFBQUFBQUR3UC1BQkFQVUJBQUFBQUpnQ0FLQUNBTFVDQUFBQUFMMENBQUFBQU9BQ0FPZ0NBUGdDQUlBREFaZ0RBYWdEcmFMS0Vyb0RDVTVaVFRJNk5ERXlNLUFEZ1J5SUJBQ1FCQUNZQkFHeUJBb0kwN2JfQ1JDNTRjb013UVFBQUFBQUFBQUFBTWtFQUFBBWcQQUFEUkINCiB3Q0ZBMkFRQTguLAAgSWdGbXlDcEJRDSUUUEFfc1FVDQ0BASwuLpoCiQEhYkJBdko6YQEkdjhGeklBUW9BRBH9XEEtUURvSlRsbE5Nam8wTVRJelFJRWNTUQFJBEFBAV0AVREMDEFBQVcdDABZHQwAYR0MAGMNDAG98LBlQUEu2AKsA-ACiZ1S6gJAaHR0cHM6Ly93d3cuYnJpdC5jby90aGVzZS1hY3RvcnMtY2FudC1lc2NhcGUtdGhlaXItZmFtb3VzLWxpbmVzL_ICEAoGQURWX0lEEgYyODkxMDHyAhIKBkNQR19JRBIIMTExMDg3NznyAhEKBUNQX0lEEggzODk2NTU0OfICDQoIQURWX0ZSRVESATDyAg0KCFJFTV9VU0VSEgEw8gIMCggBIDRDT0RFEgDyAgwKCENQRxUPEAsKB0NQFQ4cCQoFSU9fSUQBKRALCgdJTxUaOBMKD0NVU1RPTV9NT0RFTA0kCBoKFjIWABxMRUFGX05BTQVqCB4KGjYdAAhBU1QBPgxJRklFBWIcDQoIU1BMSVQBTfCLATCAAwCIAwGQAwCYAxSgAwGqAwDAA-CoAcgDANgD3q434AMA6AMA-AMDgAQAkgQJL29wZW5ydGIymAQAogQOMTA0LjE2Mi41MC4yNDWoBAGyBBIIARAQGNwGIOQDKAEoAjAAOAO4BADABADIBIGhggHSBA4xMzU2I05ZTTI6NDEyM9oEAggB4AQA8ASBSgz6BBIJgWgQiFxEQBGBjDDpdVLAiAUBmAUAoAX_EQEgAaoFFjI5NDUxkSc0NzI3NDQ2MmUxwAUAyQUBQRgAAPA_0gUJBU0BAXDYBQHgBQHwBYCvM_oFBAgAEACQBgGYBgC4BgDBBgEhATEg0AbCBNoGFgoQCREZAVAQABgA4AYE8gYCCACABwGIBwCgB0A.%26s%3Dc26c363d17931ce09fc5383e6f8aa1ff7e9324ca%26pp%3D%24%7BEMX_MACRO%7D%5D%5D%3E%3C%2FVASTAdTagURI%3E%3CImpression%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fib.adnxs.com%2Fnop%5D%5D%3E%3C%2FImpression%3E%3CCreatives%3E%3CCreative%20adID%3D%22209610044%22%3E%3CLinear%3E%3C%2FLinear%3E%3C%2FCreative%3E%3C%2FCreatives%3E%3CImpression%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fimp.emxdgt.com%2Fimp%2F%3Fcp%3D30.0000%26ts%3D1583442857%26adom%3Dhttps%3A%2F%2Femxdigital.com%26seat%3D1356%26w%3D5%26h%3D5%26pb%3D24%26sid%3D7732%26tid%3D71423%26pid%3D1158%26uid%3D29451583442857274462e1%26wid%3D1%26dom%3Dwww.brit.co%26tp%3D%24%7BEMX_MACRO%7D%26rf%3D%24%7BRF_MACRO%7D%26cat%3D3-1%26crid%3D209610044%5D%5D%3E%3C%2FImpression%3E%3C%2FWrapper%3E%3C%2FAd%3E%3C%2FVAST%3E\",\"avg\": \"30.0000\", \"auction\" : \"${AUCTION_PRICE}\"}}]]></AdParameters><MediaFiles><MediaFile delivery=\"progressive\" type=\"application/javascript\" apiFramework=\"VPAID\"><![CDATA[https://js.brealtime.com/ovvbundle-vcr_1.1.js]]></MediaFile></MediaFiles></Linear></Creative></Creatives></InLine></Ad></VAST>",
+        "id": "some_test_ad_id_2",
+        "impid": "some_test_ad_id_2",
+        "ttl": 300,
+        "crid": "9999999",
+        "w": 1020,
+        "price": 1,
+        "adid": "9999999",
+        "h": 1000
+      },
+      "type": "video"
+    }
+  ]
+}
+  
\ No newline at end of file
diff --git a/adapters/emx_digital/emx_digitaltest/exemplary/banner-app.json b/adapters/emx_digital/emx_digitaltest/exemplary/banner-app.json
new file mode 100644
index 00000000..8de90f52
--- /dev/null
+++ b/adapters/emx_digital/emx_digitaltest/exemplary/banner-app.json
@@ -0,0 +1,119 @@
+{
+  "mockBidRequest": {
+    "id": "some_test_auction",
+    "imp": [{
+      "id": "some_test_ad_id",
+      "banner": {
+        "format": [{
+          "w": 300,
+          "h": 250
+        }],
+        "w": 300,
+        "h": 250
+      },
+      "ext": {
+        "bidder": {
+          "tagid": "25251"
+        }
+      }
+    }],
+    "device": {
+      "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36",
+      "ip": "123.123.123.123",
+      "dnt": 1
+    },
+    "app": {
+      "domain": "www.publisher.com",
+      "storeurl": "http://www.publisher.com/awesome/site?with=some&parameters=here"
+    }
+  },
+
+  "httpCalls": [{
+    "expectedRequest": {
+      "uri": "https://hb.emxdgt.com?t=1000&ts=2060541160",
+      "headers": {
+        "Accept": [
+          "application/json"
+        ],
+        "Content-Type": [
+          "application/json;charset=utf-8"
+        ],
+        "X-Forwarded-For": [
+          "123.123.123.123"
+        ],
+        "Dnt": [
+          "1"
+        ],
+        "User-Agent": [
+          "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36"
+        ]
+      },
+      "body": {
+        "id": "some_test_auction",
+        "imp": [{
+          "id": "some_test_ad_id",
+          "banner": {
+            "format": [{
+              "w": 300,
+              "h": 250
+            }],
+            "w": 300,
+            "h": 250
+          },
+          "ext": {
+            "bidder": {
+              "tagid": "25251"
+            }
+          },
+          "tagid": "25251",
+          "secure": 0
+        }],
+        "app": {
+          "domain": "www.publisher.com",
+          "storeurl": "http://www.publisher.com/awesome/site?with=some&parameters=here"
+        },
+        "device": {
+          "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36",
+          "ip": "123.123.123.123",
+          "dnt": 1
+        }
+      }
+    },
+    "mockResponse": {
+      "status": 200,
+      "body": {
+        "id": "some_test_auction",
+        "seatbid": [{
+            "seat": "12356",
+            "bid": [{
+              "adm": "<div id=\"123456789_ad\"><script>!function(){console.log\"Hello, world.\";}();<\/script><\/div>",
+              "id": "some_test_ad_id",
+              "impid": "some_test_ad_id",
+              "ttl": 300,
+              "crid": "94395500",
+              "w": 300,
+              "price": 2.942808,
+              "adid": "94395500",
+              "h": 250
+            }]
+        }],
+        "cur": "USD"
+    }
+    }
+  }],
+
+  "expectedBids": [{
+    "bid": {
+      "adm": "<div id=\"123456789_ad\"><script>!function(){console.log\"Hello, world.\";}();<\/script><\/div>",
+      "id": "some_test_ad_id",
+      "impid": "some_test_ad_id",
+      "ttl": 300,
+      "crid": "94395500",
+      "w": 300,
+      "price": 2.942808,
+      "adid": "94395500",
+      "h": 250
+    },
+    "type": "banner"
+  }]
+}
diff --git a/adapters/emx_digital/emx_digitaltest/exemplary/minimal-banner.json b/adapters/emx_digital/emx_digitaltest/exemplary/minimal-banner.json
index 33a96cb4..855bdf2e 100644
--- a/adapters/emx_digital/emx_digitaltest/exemplary/minimal-banner.json
+++ b/adapters/emx_digital/emx_digitaltest/exemplary/minimal-banner.json
@@ -63,6 +63,11 @@
             "w": 300,
             "h": 250
           },
+          "ext": {
+            "bidder": {
+              "tagid": "25251"
+            }
+          },
           "tagid": "25251",
           "secure": 0
         }],
@@ -85,7 +90,7 @@
             "seat": "12356",
             "bid": [{
               "adm": "<div id=\"123456789_ad\"><script>!function(){console.log\"Hello, world.\";}();<\/script><\/div>",
-              "id": "uuid",
+              "id": "some_test_ad_id",
               "impid": "some_test_ad_id",
               "ttl": 300,
               "crid": "94395500",
@@ -103,7 +108,7 @@
   "expectedBids": [{
     "bid": {
       "adm": "<div id=\"123456789_ad\"><script>!function(){console.log\"Hello, world.\";}();<\/script><\/div>",
-      "id": "uuid",
+      "id": "some_test_ad_id",
       "impid": "some_test_ad_id",
       "ttl": 300,
       "crid": "94395500",
@@ -114,4 +119,4 @@
     },
     "type": "banner"
   }]
-}
\ No newline at end of file
+}
diff --git a/adapters/emx_digital/emx_digitaltest/exemplary/video-app.json b/adapters/emx_digital/emx_digitaltest/exemplary/video-app.json
new file mode 100644
index 00000000..ebc313a2
--- /dev/null
+++ b/adapters/emx_digital/emx_digitaltest/exemplary/video-app.json
@@ -0,0 +1,132 @@
+{
+    "mockBidRequest": {
+      "id": "some_test_auction",
+      "imp": [{
+        "id": "some_test_ad_id",
+        "video":{
+            "mimes": [
+              "video/mp4",
+              "application/javascript"
+            ],
+            "protocols":[
+               2,
+               3,
+               5,
+               6
+            ],
+            "w":640,
+            "h":480
+        },
+        "ext": {
+          "bidder": {
+            "tagid": "25251"
+          }
+        }
+      }],
+      "device": {
+        "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36",
+        "ip": "123.123.123.123",
+        "dnt": 1
+      },
+      "app": {
+        "domain": "www.publisher.com",
+        "storeurl": "http://www.publisher.com/awesome/site?with=some&parameters=here"
+      }
+    },
+  
+    "httpCalls": [{
+      "expectedRequest": {
+        "uri": "https://hb.emxdgt.com?t=1000&ts=2060541160",
+        "headers": {
+          "Accept": [
+            "application/json"
+          ],
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "X-Forwarded-For": [
+            "123.123.123.123"
+          ],
+          "Dnt": [
+            "1"
+          ],
+          "User-Agent": [
+            "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36"
+          ]
+        },
+        "body": {
+          "id": "some_test_auction",
+          "imp": [{
+            "id": "some_test_ad_id",
+            "video":{
+                "mimes": [
+                  "video/mp4",
+                  "application/javascript"
+                ],
+                "protocols":[
+                   2,
+                   3,
+                   5,
+                   6
+                ],
+                "w":640,
+                "h":480
+            },
+            "ext": {
+              "bidder": {
+                "tagid": "25251"
+              }
+            },
+            "tagid": "25251",
+            "secure": 0
+          }],
+          "app": {
+            "domain": "www.publisher.com",
+            "storeurl": "http://www.publisher.com/awesome/site?with=some&parameters=here"
+          },
+          "device": {
+            "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36",
+            "ip": "123.123.123.123",
+            "dnt": 1
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "some_test_auction",
+          "seatbid": [{
+              "seat": "12356",
+              "bid": [{
+                "adm": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><VAST version=\"2.0\"><Ad id=\"VPAID\"><InLine><Creatives><Creative sequence=\"1\"><Linear><Duration>00:00:15</Duration><AdParameters><![CDATA[{\"adType\":\"VIDEO\",\"ad\":{\"viewTrk\":\"https%3A%2F%2Fimp.emxdgt.com%2Fview%2F%3Fcp%3D30.0000%26ts%3D1583442857%26adom%3Dhttps%3A%2F%2Femxdigital.com%26seat%3D1356%26w%3D5%26h%3D5%26pb%3D24%26sid%3D7732%26tid%3D71423%26pid%3D1158%26uid%3D29451583442857274462e1%26wid%3D1%26dom%3Dwww.brit.co%26tp%3D%24%7BEMX_MACRO%7D%26rf%3D%24%7BRF_MACRO%7D%26cat%3D3-1%26crid%3D209610044\",\"tag\":\"%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%20standalone%3D%22yes%22%3F%3E%3CVAST%20version%3D%223.0%22%3E%3CAd%20id%3D%22209610044%22%3E%3CWrapper%3E%3CAdSystem%20version%3D%223.0%22%3Eadnxs%3C%2FAdSystem%3E%3CVASTAdTagURI%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fnym1-ib.adnxs.com%2Fab%3Fan_audit%3D0%26referrer%3Dhttps%253A%252F%252Fwww.brit.co%252Fthese-actors-cant-escape-their-famous-lines%252F%26e%3DwqT_3QLkCvBDZAUAAAMA1gAFAQir14XzBRCQ2fHY6OTvjT4YhqCpw5iblL0LKjYJR6zFpwAAPkARR6zFpwAAPkAZAAAA4FE4NkAhR6wJGwApESTIMQAAAAAp3CNAMPqI3Qc4zApAzApIAlC8yvljWL_Bc2AAaIrFCni14gSAAQGKAQNVU0SSBQbwPpgBAaABAagBAbABALgBA8ABBMgBAtABCdgBAOABAPABAIoCO3VmKCdhJywgMjg5MTAxLCAxNTgzNDQyODU5KQUcAHIBHCAwOTYxMDA0NCwyHwD0FwGSAt0CIXB6OWpKZ2l0b3NvU0VMekstV01ZQUNDX3dYTXdBRGdBUUFSSXpBcFEtb2pkQjFnQVlMNENhQUJ3QUhnQWdBRUFpQUVBa0FFQW1BRUFvQUVCcUFFRHNBRUF1UUh0QktEMkFBQS1RTUVCN1FTZzlnQUFQa0RKQVZ3MUt3WjloQUJBMlFFQUFBQUFBQUR3UC1BQkFQVUJBQUFBQUpnQ0FLQUNBTFVDQUFBQUFMMENBQUFBQU9BQ0FPZ0NBUGdDQUlBREFaZ0RBYWdEcmFMS0Vyb0RDVTVaVFRJNk5ERXlNLUFEZ1J5SUJBQ1FCQUNZQkFHeUJBb0kwN2JfQ1JDNTRjb013UVFBQUFBQUFBQUFBTWtFQUFBBWcQQUFEUkINCiB3Q0ZBMkFRQTguLAAgSWdGbXlDcEJRDSUUUEFfc1FVDQ0BASwuLpoCiQEhYkJBdko6YQEkdjhGeklBUW9BRBH9XEEtUURvSlRsbE5Nam8wTVRJelFJRWNTUQFJBEFBAV0AVREMDEFBQVcdDABZHQwAYR0MAGMNDAG98LBlQUEu2AKsA-ACiZ1S6gJAaHR0cHM6Ly93d3cuYnJpdC5jby90aGVzZS1hY3RvcnMtY2FudC1lc2NhcGUtdGhlaXItZmFtb3VzLWxpbmVzL_ICEAoGQURWX0lEEgYyODkxMDHyAhIKBkNQR19JRBIIMTExMDg3NznyAhEKBUNQX0lEEggzODk2NTU0OfICDQoIQURWX0ZSRVESATDyAg0KCFJFTV9VU0VSEgEw8gIMCggBIDRDT0RFEgDyAgwKCENQRxUPEAsKB0NQFQ4cCQoFSU9fSUQBKRALCgdJTxUaOBMKD0NVU1RPTV9NT0RFTA0kCBoKFjIWABxMRUFGX05BTQVqCB4KGjYdAAhBU1QBPgxJRklFBWIcDQoIU1BMSVQBTfCLATCAAwCIAwGQAwCYAxSgAwGqAwDAA-CoAcgDANgD3q434AMA6AMA-AMDgAQAkgQJL29wZW5ydGIymAQAogQOMTA0LjE2Mi41MC4yNDWoBAGyBBIIARAQGNwGIOQDKAEoAjAAOAO4BADABADIBIGhggHSBA4xMzU2I05ZTTI6NDEyM9oEAggB4AQA8ASBSgz6BBIJgWgQiFxEQBGBjDDpdVLAiAUBmAUAoAX_EQEgAaoFFjI5NDUxkSc0NzI3NDQ2MmUxwAUAyQUBQRgAAPA_0gUJBU0BAXDYBQHgBQHwBYCvM_oFBAgAEACQBgGYBgC4BgDBBgEhATEg0AbCBNoGFgoQCREZAVAQABgA4AYE8gYCCACABwGIBwCgB0A.%26s%3Dc26c363d17931ce09fc5383e6f8aa1ff7e9324ca%26pp%3D%24%7BEMX_MACRO%7D%5D%5D%3E%3C%2FVASTAdTagURI%3E%3CImpression%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fib.adnxs.com%2Fnop%5D%5D%3E%3C%2FImpression%3E%3CCreatives%3E%3CCreative%20adID%3D%22209610044%22%3E%3CLinear%3E%3C%2FLinear%3E%3C%2FCreative%3E%3C%2FCreatives%3E%3CImpression%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fimp.emxdgt.com%2Fimp%2F%3Fcp%3D30.0000%26ts%3D1583442857%26adom%3Dhttps%3A%2F%2Femxdigital.com%26seat%3D1356%26w%3D5%26h%3D5%26pb%3D24%26sid%3D7732%26tid%3D71423%26pid%3D1158%26uid%3D29451583442857274462e1%26wid%3D1%26dom%3Dwww.brit.co%26tp%3D%24%7BEMX_MACRO%7D%26rf%3D%24%7BRF_MACRO%7D%26cat%3D3-1%26crid%3D209610044%5D%5D%3E%3C%2FImpression%3E%3C%2FWrapper%3E%3C%2FAd%3E%3C%2FVAST%3E\",\"avg\": \"30.0000\", \"auction\" : \"${AUCTION_PRICE}\"}}]]></AdParameters><MediaFiles><MediaFile delivery=\"progressive\" type=\"application/javascript\" apiFramework=\"VPAID\"><![CDATA[https://js.brealtime.com/ovvbundle-vcr_1.1.js]]></MediaFile></MediaFiles></Linear></Creative></Creatives></InLine></Ad></VAST>",
+                "id": "some_test_ad_id",
+                "impid": "some_test_ad_id",
+                "ttl": 300,
+                "crid": "148479785",
+                "w": 640,
+                "price": 0.05,
+                "adid": "148479785",
+                "h": 480
+              }]
+          }],
+          "cur": "USD"
+      }
+      }
+    }],
+  
+    "expectedBids": [{
+      "bid": {
+        "adm": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><VAST version=\"2.0\"><Ad id=\"VPAID\"><InLine><Creatives><Creative sequence=\"1\"><Linear><Duration>00:00:15</Duration><AdParameters><![CDATA[{\"adType\":\"VIDEO\",\"ad\":{\"viewTrk\":\"https%3A%2F%2Fimp.emxdgt.com%2Fview%2F%3Fcp%3D30.0000%26ts%3D1583442857%26adom%3Dhttps%3A%2F%2Femxdigital.com%26seat%3D1356%26w%3D5%26h%3D5%26pb%3D24%26sid%3D7732%26tid%3D71423%26pid%3D1158%26uid%3D29451583442857274462e1%26wid%3D1%26dom%3Dwww.brit.co%26tp%3D%24%7BEMX_MACRO%7D%26rf%3D%24%7BRF_MACRO%7D%26cat%3D3-1%26crid%3D209610044\",\"tag\":\"%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%20standalone%3D%22yes%22%3F%3E%3CVAST%20version%3D%223.0%22%3E%3CAd%20id%3D%22209610044%22%3E%3CWrapper%3E%3CAdSystem%20version%3D%223.0%22%3Eadnxs%3C%2FAdSystem%3E%3CVASTAdTagURI%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fnym1-ib.adnxs.com%2Fab%3Fan_audit%3D0%26referrer%3Dhttps%253A%252F%252Fwww.brit.co%252Fthese-actors-cant-escape-their-famous-lines%252F%26e%3DwqT_3QLkCvBDZAUAAAMA1gAFAQir14XzBRCQ2fHY6OTvjT4YhqCpw5iblL0LKjYJR6zFpwAAPkARR6zFpwAAPkAZAAAA4FE4NkAhR6wJGwApESTIMQAAAAAp3CNAMPqI3Qc4zApAzApIAlC8yvljWL_Bc2AAaIrFCni14gSAAQGKAQNVU0SSBQbwPpgBAaABAagBAbABALgBA8ABBMgBAtABCdgBAOABAPABAIoCO3VmKCdhJywgMjg5MTAxLCAxNTgzNDQyODU5KQUcAHIBHCAwOTYxMDA0NCwyHwD0FwGSAt0CIXB6OWpKZ2l0b3NvU0VMekstV01ZQUNDX3dYTXdBRGdBUUFSSXpBcFEtb2pkQjFnQVlMNENhQUJ3QUhnQWdBRUFpQUVBa0FFQW1BRUFvQUVCcUFFRHNBRUF1UUh0QktEMkFBQS1RTUVCN1FTZzlnQUFQa0RKQVZ3MUt3WjloQUJBMlFFQUFBQUFBQUR3UC1BQkFQVUJBQUFBQUpnQ0FLQUNBTFVDQUFBQUFMMENBQUFBQU9BQ0FPZ0NBUGdDQUlBREFaZ0RBYWdEcmFMS0Vyb0RDVTVaVFRJNk5ERXlNLUFEZ1J5SUJBQ1FCQUNZQkFHeUJBb0kwN2JfQ1JDNTRjb013UVFBQUFBQUFBQUFBTWtFQUFBBWcQQUFEUkINCiB3Q0ZBMkFRQTguLAAgSWdGbXlDcEJRDSUUUEFfc1FVDQ0BASwuLpoCiQEhYkJBdko6YQEkdjhGeklBUW9BRBH9XEEtUURvSlRsbE5Nam8wTVRJelFJRWNTUQFJBEFBAV0AVREMDEFBQVcdDABZHQwAYR0MAGMNDAG98LBlQUEu2AKsA-ACiZ1S6gJAaHR0cHM6Ly93d3cuYnJpdC5jby90aGVzZS1hY3RvcnMtY2FudC1lc2NhcGUtdGhlaXItZmFtb3VzLWxpbmVzL_ICEAoGQURWX0lEEgYyODkxMDHyAhIKBkNQR19JRBIIMTExMDg3NznyAhEKBUNQX0lEEggzODk2NTU0OfICDQoIQURWX0ZSRVESATDyAg0KCFJFTV9VU0VSEgEw8gIMCggBIDRDT0RFEgDyAgwKCENQRxUPEAsKB0NQFQ4cCQoFSU9fSUQBKRALCgdJTxUaOBMKD0NVU1RPTV9NT0RFTA0kCBoKFjIWABxMRUFGX05BTQVqCB4KGjYdAAhBU1QBPgxJRklFBWIcDQoIU1BMSVQBTfCLATCAAwCIAwGQAwCYAxSgAwGqAwDAA-CoAcgDANgD3q434AMA6AMA-AMDgAQAkgQJL29wZW5ydGIymAQAogQOMTA0LjE2Mi41MC4yNDWoBAGyBBIIARAQGNwGIOQDKAEoAjAAOAO4BADABADIBIGhggHSBA4xMzU2I05ZTTI6NDEyM9oEAggB4AQA8ASBSgz6BBIJgWgQiFxEQBGBjDDpdVLAiAUBmAUAoAX_EQEgAaoFFjI5NDUxkSc0NzI3NDQ2MmUxwAUAyQUBQRgAAPA_0gUJBU0BAXDYBQHgBQHwBYCvM_oFBAgAEACQBgGYBgC4BgDBBgEhATEg0AbCBNoGFgoQCREZAVAQABgA4AYE8gYCCACABwGIBwCgB0A.%26s%3Dc26c363d17931ce09fc5383e6f8aa1ff7e9324ca%26pp%3D%24%7BEMX_MACRO%7D%5D%5D%3E%3C%2FVASTAdTagURI%3E%3CImpression%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fib.adnxs.com%2Fnop%5D%5D%3E%3C%2FImpression%3E%3CCreatives%3E%3CCreative%20adID%3D%22209610044%22%3E%3CLinear%3E%3C%2FLinear%3E%3C%2FCreative%3E%3C%2FCreatives%3E%3CImpression%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fimp.emxdgt.com%2Fimp%2F%3Fcp%3D30.0000%26ts%3D1583442857%26adom%3Dhttps%3A%2F%2Femxdigital.com%26seat%3D1356%26w%3D5%26h%3D5%26pb%3D24%26sid%3D7732%26tid%3D71423%26pid%3D1158%26uid%3D29451583442857274462e1%26wid%3D1%26dom%3Dwww.brit.co%26tp%3D%24%7BEMX_MACRO%7D%26rf%3D%24%7BRF_MACRO%7D%26cat%3D3-1%26crid%3D209610044%5D%5D%3E%3C%2FImpression%3E%3C%2FWrapper%3E%3C%2FAd%3E%3C%2FVAST%3E\",\"avg\": \"30.0000\", \"auction\" : \"${AUCTION_PRICE}\"}}]]></AdParameters><MediaFiles><MediaFile delivery=\"progressive\" type=\"application/javascript\" apiFramework=\"VPAID\"><![CDATA[https://js.brealtime.com/ovvbundle-vcr_1.1.js]]></MediaFile></MediaFiles></Linear></Creative></Creatives></InLine></Ad></VAST>",
+        "id": "some_test_ad_id",
+        "impid": "some_test_ad_id",
+        "ttl": 300,
+        "crid": "148479785",
+        "w": 640,
+        "price": 0.05,
+        "adid": "148479785",
+        "h": 480
+      },
+      "type": "video"
+    }]
+  }
+  
\ No newline at end of file
diff --git a/adapters/emx_digital/emx_digitaltest/exemplary/video-ctv.json b/adapters/emx_digital/emx_digitaltest/exemplary/video-ctv.json
new file mode 100644
index 00000000..66a6413d
--- /dev/null
+++ b/adapters/emx_digital/emx_digitaltest/exemplary/video-ctv.json
@@ -0,0 +1,150 @@
+{
+    "mockBidRequest": {
+      "id": "some_test_auction",
+      "imp": [{
+        "id": "some_test_ad_id",
+        "video":{
+            "mimes": [
+              "video/mp4",
+              "application/javascript"
+            ],
+            "protocols":[
+               2,
+               3,
+               5,
+               6,
+               7
+            ],
+            "w": 1920,
+            "h": 1080
+        },
+        "ext": {
+          "bidder": {
+            "tagid": "25251"
+          }
+        }
+      }],
+      "device": {
+        "ua": "Roku\/DVP-9.0 (919.00E04142A)",
+        "ip": "123.123.123.123",
+        "dnt": 1,
+        "devicetype": 3,
+        "make": "Roku",
+        "model": "Digital Video player",
+        "language": "en",
+        "ifa": "7a2016d7-17b0-572f-a7a6-13f89a81d905"
+      },
+      "app": {
+        "domain": "www.publisher.com",
+        "storeurl": "http://www.publisher.com/awesome/site?with=some&parameters=here",
+        "name": "Example TV",
+        "bundle": "com.example.tv"
+      }
+    },
+  
+    "httpCalls": [{
+      "expectedRequest": {
+        "uri": "https://hb.emxdgt.com?t=1000&ts=2060541160",
+        "headers": {
+          "Accept": [
+            "application/json"
+          ],
+          "Accept-Language": [
+            "en"
+          ],
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "X-Forwarded-For": [
+            "123.123.123.123"
+          ],
+          "Dnt": [
+            "1"
+          ],
+          "User-Agent": [
+            "Roku\/DVP-9.0 (919.00E04142A)"
+          ]
+        },
+        "body": {
+          "id": "some_test_auction",
+          "imp": [{
+            "id": "some_test_ad_id",
+            "video":{
+                "mimes": [
+                  "video/mp4",
+                  "application/javascript"
+                ],
+                "protocols":[
+                   2,
+                   3,
+                   5,
+                   6
+                ],
+                "w":1920,
+                "h":1080
+            },
+            "ext": {
+              "bidder": {
+                "tagid": "25251"
+              }
+            },
+            "tagid": "25251",
+            "secure": 0
+          }],
+          "app": {
+            "domain": "www.publisher.com",
+            "storeurl": "http://www.publisher.com/awesome/site?with=some&parameters=here",
+            "name": "Example TV",
+            "bundle": "com.example.tv"
+          },
+          "device": {
+            "ua": "Roku\/DVP-9.0 (919.00E04142A)",
+            "ip": "123.123.123.123",
+            "dnt": 1,
+            "devicetype": 3,
+            "make": "Roku",
+            "model": "Digital Video player",
+            "language": "en",
+            "ifa": "7a2016d7-17b0-572f-a7a6-13f89a81d905"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "some_test_auction",
+          "seatbid": [{
+              "seat": "12356",
+              "bid": [{
+                "adm": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><VAST version=\"2.0\"><Ad id=\"VPAID\"><InLine><Creatives><Creative sequence=\"1\"><Linear><Duration>00:00:15</Duration><AdParameters><![CDATA[{\"adType\":\"VIDEO\",\"ad\":{\"viewTrk\":\"https%3A%2F%2Fimp.emxdgt.com%2Fview%2F%3Fcp%3D30.0000%26ts%3D1583442857%26adom%3Dhttps%3A%2F%2Femxdigital.com%26seat%3D1356%26w%3D5%26h%3D5%26pb%3D24%26sid%3D7732%26tid%3D71423%26pid%3D1158%26uid%3D29451583442857274462e1%26wid%3D1%26dom%3Dwww.brit.co%26tp%3D%24%7BEMX_MACRO%7D%26rf%3D%24%7BRF_MACRO%7D%26cat%3D3-1%26crid%3D209610044\",\"tag\":\"%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%20standalone%3D%22yes%22%3F%3E%3CVAST%20version%3D%223.0%22%3E%3CAd%20id%3D%22209610044%22%3E%3CWrapper%3E%3CAdSystem%20version%3D%223.0%22%3Eadnxs%3C%2FAdSystem%3E%3CVASTAdTagURI%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fnym1-ib.adnxs.com%2Fab%3Fan_audit%3D0%26referrer%3Dhttps%253A%252F%252Fwww.brit.co%252Fthese-actors-cant-escape-their-famous-lines%252F%26e%3DwqT_3QLkCvBDZAUAAAMA1gAFAQir14XzBRCQ2fHY6OTvjT4YhqCpw5iblL0LKjYJR6zFpwAAPkARR6zFpwAAPkAZAAAA4FE4NkAhR6wJGwApESTIMQAAAAAp3CNAMPqI3Qc4zApAzApIAlC8yvljWL_Bc2AAaIrFCni14gSAAQGKAQNVU0SSBQbwPpgBAaABAagBAbABALgBA8ABBMgBAtABCdgBAOABAPABAIoCO3VmKCdhJywgMjg5MTAxLCAxNTgzNDQyODU5KQUcAHIBHCAwOTYxMDA0NCwyHwD0FwGSAt0CIXB6OWpKZ2l0b3NvU0VMekstV01ZQUNDX3dYTXdBRGdBUUFSSXpBcFEtb2pkQjFnQVlMNENhQUJ3QUhnQWdBRUFpQUVBa0FFQW1BRUFvQUVCcUFFRHNBRUF1UUh0QktEMkFBQS1RTUVCN1FTZzlnQUFQa0RKQVZ3MUt3WjloQUJBMlFFQUFBQUFBQUR3UC1BQkFQVUJBQUFBQUpnQ0FLQUNBTFVDQUFBQUFMMENBQUFBQU9BQ0FPZ0NBUGdDQUlBREFaZ0RBYWdEcmFMS0Vyb0RDVTVaVFRJNk5ERXlNLUFEZ1J5SUJBQ1FCQUNZQkFHeUJBb0kwN2JfQ1JDNTRjb013UVFBQUFBQUFBQUFBTWtFQUFBBWcQQUFEUkINCiB3Q0ZBMkFRQTguLAAgSWdGbXlDcEJRDSUUUEFfc1FVDQ0BASwuLpoCiQEhYkJBdko6YQEkdjhGeklBUW9BRBH9XEEtUURvSlRsbE5Nam8wTVRJelFJRWNTUQFJBEFBAV0AVREMDEFBQVcdDABZHQwAYR0MAGMNDAG98LBlQUEu2AKsA-ACiZ1S6gJAaHR0cHM6Ly93d3cuYnJpdC5jby90aGVzZS1hY3RvcnMtY2FudC1lc2NhcGUtdGhlaXItZmFtb3VzLWxpbmVzL_ICEAoGQURWX0lEEgYyODkxMDHyAhIKBkNQR19JRBIIMTExMDg3NznyAhEKBUNQX0lEEggzODk2NTU0OfICDQoIQURWX0ZSRVESATDyAg0KCFJFTV9VU0VSEgEw8gIMCggBIDRDT0RFEgDyAgwKCENQRxUPEAsKB0NQFQ4cCQoFSU9fSUQBKRALCgdJTxUaOBMKD0NVU1RPTV9NT0RFTA0kCBoKFjIWABxMRUFGX05BTQVqCB4KGjYdAAhBU1QBPgxJRklFBWIcDQoIU1BMSVQBTfCLATCAAwCIAwGQAwCYAxSgAwGqAwDAA-CoAcgDANgD3q434AMA6AMA-AMDgAQAkgQJL29wZW5ydGIymAQAogQOMTA0LjE2Mi41MC4yNDWoBAGyBBIIARAQGNwGIOQDKAEoAjAAOAO4BADABADIBIGhggHSBA4xMzU2I05ZTTI6NDEyM9oEAggB4AQA8ASBSgz6BBIJgWgQiFxEQBGBjDDpdVLAiAUBmAUAoAX_EQEgAaoFFjI5NDUxkSc0NzI3NDQ2MmUxwAUAyQUBQRgAAPA_0gUJBU0BAXDYBQHgBQHwBYCvM_oFBAgAEACQBgGYBgC4BgDBBgEhATEg0AbCBNoGFgoQCREZAVAQABgA4AYE8gYCCACABwGIBwCgB0A.%26s%3Dc26c363d17931ce09fc5383e6f8aa1ff7e9324ca%26pp%3D%24%7BEMX_MACRO%7D%5D%5D%3E%3C%2FVASTAdTagURI%3E%3CImpression%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fib.adnxs.com%2Fnop%5D%5D%3E%3C%2FImpression%3E%3CCreatives%3E%3CCreative%20adID%3D%22209610044%22%3E%3CLinear%3E%3C%2FLinear%3E%3C%2FCreative%3E%3C%2FCreatives%3E%3CImpression%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fimp.emxdgt.com%2Fimp%2F%3Fcp%3D30.0000%26ts%3D1583442857%26adom%3Dhttps%3A%2F%2Femxdigital.com%26seat%3D1356%26w%3D5%26h%3D5%26pb%3D24%26sid%3D7732%26tid%3D71423%26pid%3D1158%26uid%3D29451583442857274462e1%26wid%3D1%26dom%3Dwww.brit.co%26tp%3D%24%7BEMX_MACRO%7D%26rf%3D%24%7BRF_MACRO%7D%26cat%3D3-1%26crid%3D209610044%5D%5D%3E%3C%2FImpression%3E%3C%2FWrapper%3E%3C%2FAd%3E%3C%2FVAST%3E\",\"avg\": \"30.0000\", \"auction\" : \"${AUCTION_PRICE}\"}}]]></AdParameters><MediaFiles><MediaFile delivery=\"progressive\" type=\"application/javascript\" apiFramework=\"VPAID\"><![CDATA[https://js.brealtime.com/ovvbundle-vcr_1.1.js]]></MediaFile></MediaFiles></Linear></Creative></Creatives></InLine></Ad></VAST>",
+                "id": "some_test_ad_id",
+                "impid": "some_test_ad_id",
+                "ttl": 300,
+                "crid": "148479785",
+                "w": 1920,
+                "price": 1.00,
+                "adid": "148479785",
+                "h": 1080
+              }]
+          }],
+          "cur": "USD"
+      }
+      }
+    }],
+  
+    "expectedBids": [{
+      "bid": {
+        "adm": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><VAST version=\"2.0\"><Ad id=\"VPAID\"><InLine><Creatives><Creative sequence=\"1\"><Linear><Duration>00:00:15</Duration><AdParameters><![CDATA[{\"adType\":\"VIDEO\",\"ad\":{\"viewTrk\":\"https%3A%2F%2Fimp.emxdgt.com%2Fview%2F%3Fcp%3D30.0000%26ts%3D1583442857%26adom%3Dhttps%3A%2F%2Femxdigital.com%26seat%3D1356%26w%3D5%26h%3D5%26pb%3D24%26sid%3D7732%26tid%3D71423%26pid%3D1158%26uid%3D29451583442857274462e1%26wid%3D1%26dom%3Dwww.brit.co%26tp%3D%24%7BEMX_MACRO%7D%26rf%3D%24%7BRF_MACRO%7D%26cat%3D3-1%26crid%3D209610044\",\"tag\":\"%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%20standalone%3D%22yes%22%3F%3E%3CVAST%20version%3D%223.0%22%3E%3CAd%20id%3D%22209610044%22%3E%3CWrapper%3E%3CAdSystem%20version%3D%223.0%22%3Eadnxs%3C%2FAdSystem%3E%3CVASTAdTagURI%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fnym1-ib.adnxs.com%2Fab%3Fan_audit%3D0%26referrer%3Dhttps%253A%252F%252Fwww.brit.co%252Fthese-actors-cant-escape-their-famous-lines%252F%26e%3DwqT_3QLkCvBDZAUAAAMA1gAFAQir14XzBRCQ2fHY6OTvjT4YhqCpw5iblL0LKjYJR6zFpwAAPkARR6zFpwAAPkAZAAAA4FE4NkAhR6wJGwApESTIMQAAAAAp3CNAMPqI3Qc4zApAzApIAlC8yvljWL_Bc2AAaIrFCni14gSAAQGKAQNVU0SSBQbwPpgBAaABAagBAbABALgBA8ABBMgBAtABCdgBAOABAPABAIoCO3VmKCdhJywgMjg5MTAxLCAxNTgzNDQyODU5KQUcAHIBHCAwOTYxMDA0NCwyHwD0FwGSAt0CIXB6OWpKZ2l0b3NvU0VMekstV01ZQUNDX3dYTXdBRGdBUUFSSXpBcFEtb2pkQjFnQVlMNENhQUJ3QUhnQWdBRUFpQUVBa0FFQW1BRUFvQUVCcUFFRHNBRUF1UUh0QktEMkFBQS1RTUVCN1FTZzlnQUFQa0RKQVZ3MUt3WjloQUJBMlFFQUFBQUFBQUR3UC1BQkFQVUJBQUFBQUpnQ0FLQUNBTFVDQUFBQUFMMENBQUFBQU9BQ0FPZ0NBUGdDQUlBREFaZ0RBYWdEcmFMS0Vyb0RDVTVaVFRJNk5ERXlNLUFEZ1J5SUJBQ1FCQUNZQkFHeUJBb0kwN2JfQ1JDNTRjb013UVFBQUFBQUFBQUFBTWtFQUFBBWcQQUFEUkINCiB3Q0ZBMkFRQTguLAAgSWdGbXlDcEJRDSUUUEFfc1FVDQ0BASwuLpoCiQEhYkJBdko6YQEkdjhGeklBUW9BRBH9XEEtUURvSlRsbE5Nam8wTVRJelFJRWNTUQFJBEFBAV0AVREMDEFBQVcdDABZHQwAYR0MAGMNDAG98LBlQUEu2AKsA-ACiZ1S6gJAaHR0cHM6Ly93d3cuYnJpdC5jby90aGVzZS1hY3RvcnMtY2FudC1lc2NhcGUtdGhlaXItZmFtb3VzLWxpbmVzL_ICEAoGQURWX0lEEgYyODkxMDHyAhIKBkNQR19JRBIIMTExMDg3NznyAhEKBUNQX0lEEggzODk2NTU0OfICDQoIQURWX0ZSRVESATDyAg0KCFJFTV9VU0VSEgEw8gIMCggBIDRDT0RFEgDyAgwKCENQRxUPEAsKB0NQFQ4cCQoFSU9fSUQBKRALCgdJTxUaOBMKD0NVU1RPTV9NT0RFTA0kCBoKFjIWABxMRUFGX05BTQVqCB4KGjYdAAhBU1QBPgxJRklFBWIcDQoIU1BMSVQBTfCLATCAAwCIAwGQAwCYAxSgAwGqAwDAA-CoAcgDANgD3q434AMA6AMA-AMDgAQAkgQJL29wZW5ydGIymAQAogQOMTA0LjE2Mi41MC4yNDWoBAGyBBIIARAQGNwGIOQDKAEoAjAAOAO4BADABADIBIGhggHSBA4xMzU2I05ZTTI6NDEyM9oEAggB4AQA8ASBSgz6BBIJgWgQiFxEQBGBjDDpdVLAiAUBmAUAoAX_EQEgAaoFFjI5NDUxkSc0NzI3NDQ2MmUxwAUAyQUBQRgAAPA_0gUJBU0BAXDYBQHgBQHwBYCvM_oFBAgAEACQBgGYBgC4BgDBBgEhATEg0AbCBNoGFgoQCREZAVAQABgA4AYE8gYCCACABwGIBwCgB0A.%26s%3Dc26c363d17931ce09fc5383e6f8aa1ff7e9324ca%26pp%3D%24%7BEMX_MACRO%7D%5D%5D%3E%3C%2FVASTAdTagURI%3E%3CImpression%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fib.adnxs.com%2Fnop%5D%5D%3E%3C%2FImpression%3E%3CCreatives%3E%3CCreative%20adID%3D%22209610044%22%3E%3CLinear%3E%3C%2FLinear%3E%3C%2FCreative%3E%3C%2FCreatives%3E%3CImpression%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fimp.emxdgt.com%2Fimp%2F%3Fcp%3D30.0000%26ts%3D1583442857%26adom%3Dhttps%3A%2F%2Femxdigital.com%26seat%3D1356%26w%3D5%26h%3D5%26pb%3D24%26sid%3D7732%26tid%3D71423%26pid%3D1158%26uid%3D29451583442857274462e1%26wid%3D1%26dom%3Dwww.brit.co%26tp%3D%24%7BEMX_MACRO%7D%26rf%3D%24%7BRF_MACRO%7D%26cat%3D3-1%26crid%3D209610044%5D%5D%3E%3C%2FImpression%3E%3C%2FWrapper%3E%3C%2FAd%3E%3C%2FVAST%3E\",\"avg\": \"30.0000\", \"auction\" : \"${AUCTION_PRICE}\"}}]]></AdParameters><MediaFiles><MediaFile delivery=\"progressive\" type=\"application/javascript\" apiFramework=\"VPAID\"><![CDATA[https://js.brealtime.com/ovvbundle-vcr_1.1.js]]></MediaFile></MediaFiles></Linear></Creative></Creatives></InLine></Ad></VAST>",
+        "id": "some_test_ad_id",
+        "impid": "some_test_ad_id",
+        "ttl": 300,
+        "crid": "148479785",
+        "w": 1920,
+        "price": 1.00,
+        "adid": "148479785",
+        "h": 1080
+      },
+      "type": "video"
+    }]
+  }
+  
\ No newline at end of file
diff --git a/adapters/emx_digital/emx_digitaltest/exemplary/video-site.json b/adapters/emx_digital/emx_digitaltest/exemplary/video-site.json
new file mode 100644
index 00000000..30a1167b
--- /dev/null
+++ b/adapters/emx_digital/emx_digitaltest/exemplary/video-site.json
@@ -0,0 +1,135 @@
+{
+    "mockBidRequest": {
+      "id": "some_test_auction",
+      "imp": [{
+        "id": "some_test_ad_id",
+        "video":{
+            "mimes": [
+              "video/mp4",
+              "application/javascript"
+            ],
+            "protocols":[
+               2,
+               3,
+               5,
+               6
+            ],
+            "w":640,
+            "h":480
+        },
+        "ext": {
+          "bidder": {
+            "tagid": "25251"
+          }
+        }
+      }],
+      "device": {
+        "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36",
+        "ip": "123.123.123.123",
+        "dnt": 1
+      },
+      "site": {
+        "domain": "www.publisher.com",
+        "page": "http://www.publisher.com/awesome/site?with=some&parameters=here"
+      }
+    },
+  
+    "httpCalls": [{
+      "expectedRequest": {
+        "uri": "https://hb.emxdgt.com?t=1000&ts=2060541160",
+        "headers": {
+          "Accept": [
+            "application/json"
+          ],
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "X-Forwarded-For": [
+            "123.123.123.123"
+          ],
+          "Referer": [
+            "http://www.publisher.com/awesome/site?with=some&parameters=here"
+          ],
+          "Dnt": [
+            "1"
+          ],
+          "User-Agent": [
+            "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36"
+          ]
+        },
+        "body": {
+          "id": "some_test_auction",
+          "imp": [{
+            "id": "some_test_ad_id",
+            "video":{
+                "mimes": [
+                  "video/mp4",
+                  "application/javascript"
+                ],
+                "protocols":[
+                   2,
+                   3,
+                   5,
+                   6
+                ],
+                "w":640,
+                "h":480
+            },
+            "ext": {
+              "bidder": {
+                "tagid": "25251"
+              }
+            },
+            "tagid": "25251",
+            "secure": 0
+          }],
+          "site": {
+            "domain": "www.publisher.com",
+            "page": "http://www.publisher.com/awesome/site?with=some&parameters=here"
+          },
+          "device": {
+            "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.91 Safari/537.36",
+            "ip": "123.123.123.123",
+            "dnt": 1
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "some_test_auction",
+          "seatbid": [{
+              "seat": "12356",
+              "bid": [{
+                "adm": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><VAST version=\"2.0\"><Ad id=\"VPAID\"><InLine><Creatives><Creative sequence=\"1\"><Linear><Duration>00:00:15</Duration><AdParameters><![CDATA[{\"adType\":\"VIDEO\",\"ad\":{\"viewTrk\":\"https%3A%2F%2Fimp.emxdgt.com%2Fview%2F%3Fcp%3D30.0000%26ts%3D1583442857%26adom%3Dhttps%3A%2F%2Femxdigital.com%26seat%3D1356%26w%3D5%26h%3D5%26pb%3D24%26sid%3D7732%26tid%3D71423%26pid%3D1158%26uid%3D29451583442857274462e1%26wid%3D1%26dom%3Dwww.brit.co%26tp%3D%24%7BEMX_MACRO%7D%26rf%3D%24%7BRF_MACRO%7D%26cat%3D3-1%26crid%3D209610044\",\"tag\":\"%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%20standalone%3D%22yes%22%3F%3E%3CVAST%20version%3D%223.0%22%3E%3CAd%20id%3D%22209610044%22%3E%3CWrapper%3E%3CAdSystem%20version%3D%223.0%22%3Eadnxs%3C%2FAdSystem%3E%3CVASTAdTagURI%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fnym1-ib.adnxs.com%2Fab%3Fan_audit%3D0%26referrer%3Dhttps%253A%252F%252Fwww.brit.co%252Fthese-actors-cant-escape-their-famous-lines%252F%26e%3DwqT_3QLkCvBDZAUAAAMA1gAFAQir14XzBRCQ2fHY6OTvjT4YhqCpw5iblL0LKjYJR6zFpwAAPkARR6zFpwAAPkAZAAAA4FE4NkAhR6wJGwApESTIMQAAAAAp3CNAMPqI3Qc4zApAzApIAlC8yvljWL_Bc2AAaIrFCni14gSAAQGKAQNVU0SSBQbwPpgBAaABAagBAbABALgBA8ABBMgBAtABCdgBAOABAPABAIoCO3VmKCdhJywgMjg5MTAxLCAxNTgzNDQyODU5KQUcAHIBHCAwOTYxMDA0NCwyHwD0FwGSAt0CIXB6OWpKZ2l0b3NvU0VMekstV01ZQUNDX3dYTXdBRGdBUUFSSXpBcFEtb2pkQjFnQVlMNENhQUJ3QUhnQWdBRUFpQUVBa0FFQW1BRUFvQUVCcUFFRHNBRUF1UUh0QktEMkFBQS1RTUVCN1FTZzlnQUFQa0RKQVZ3MUt3WjloQUJBMlFFQUFBQUFBQUR3UC1BQkFQVUJBQUFBQUpnQ0FLQUNBTFVDQUFBQUFMMENBQUFBQU9BQ0FPZ0NBUGdDQUlBREFaZ0RBYWdEcmFMS0Vyb0RDVTVaVFRJNk5ERXlNLUFEZ1J5SUJBQ1FCQUNZQkFHeUJBb0kwN2JfQ1JDNTRjb013UVFBQUFBQUFBQUFBTWtFQUFBBWcQQUFEUkINCiB3Q0ZBMkFRQTguLAAgSWdGbXlDcEJRDSUUUEFfc1FVDQ0BASwuLpoCiQEhYkJBdko6YQEkdjhGeklBUW9BRBH9XEEtUURvSlRsbE5Nam8wTVRJelFJRWNTUQFJBEFBAV0AVREMDEFBQVcdDABZHQwAYR0MAGMNDAG98LBlQUEu2AKsA-ACiZ1S6gJAaHR0cHM6Ly93d3cuYnJpdC5jby90aGVzZS1hY3RvcnMtY2FudC1lc2NhcGUtdGhlaXItZmFtb3VzLWxpbmVzL_ICEAoGQURWX0lEEgYyODkxMDHyAhIKBkNQR19JRBIIMTExMDg3NznyAhEKBUNQX0lEEggzODk2NTU0OfICDQoIQURWX0ZSRVESATDyAg0KCFJFTV9VU0VSEgEw8gIMCggBIDRDT0RFEgDyAgwKCENQRxUPEAsKB0NQFQ4cCQoFSU9fSUQBKRALCgdJTxUaOBMKD0NVU1RPTV9NT0RFTA0kCBoKFjIWABxMRUFGX05BTQVqCB4KGjYdAAhBU1QBPgxJRklFBWIcDQoIU1BMSVQBTfCLATCAAwCIAwGQAwCYAxSgAwGqAwDAA-CoAcgDANgD3q434AMA6AMA-AMDgAQAkgQJL29wZW5ydGIymAQAogQOMTA0LjE2Mi41MC4yNDWoBAGyBBIIARAQGNwGIOQDKAEoAjAAOAO4BADABADIBIGhggHSBA4xMzU2I05ZTTI6NDEyM9oEAggB4AQA8ASBSgz6BBIJgWgQiFxEQBGBjDDpdVLAiAUBmAUAoAX_EQEgAaoFFjI5NDUxkSc0NzI3NDQ2MmUxwAUAyQUBQRgAAPA_0gUJBU0BAXDYBQHgBQHwBYCvM_oFBAgAEACQBgGYBgC4BgDBBgEhATEg0AbCBNoGFgoQCREZAVAQABgA4AYE8gYCCACABwGIBwCgB0A.%26s%3Dc26c363d17931ce09fc5383e6f8aa1ff7e9324ca%26pp%3D%24%7BEMX_MACRO%7D%5D%5D%3E%3C%2FVASTAdTagURI%3E%3CImpression%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fib.adnxs.com%2Fnop%5D%5D%3E%3C%2FImpression%3E%3CCreatives%3E%3CCreative%20adID%3D%22209610044%22%3E%3CLinear%3E%3C%2FLinear%3E%3C%2FCreative%3E%3C%2FCreatives%3E%3CImpression%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fimp.emxdgt.com%2Fimp%2F%3Fcp%3D30.0000%26ts%3D1583442857%26adom%3Dhttps%3A%2F%2Femxdigital.com%26seat%3D1356%26w%3D5%26h%3D5%26pb%3D24%26sid%3D7732%26tid%3D71423%26pid%3D1158%26uid%3D29451583442857274462e1%26wid%3D1%26dom%3Dwww.brit.co%26tp%3D%24%7BEMX_MACRO%7D%26rf%3D%24%7BRF_MACRO%7D%26cat%3D3-1%26crid%3D209610044%5D%5D%3E%3C%2FImpression%3E%3C%2FWrapper%3E%3C%2FAd%3E%3C%2FVAST%3E\",\"avg\": \"30.0000\", \"auction\" : \"${AUCTION_PRICE}\"}}]]></AdParameters><MediaFiles><MediaFile delivery=\"progressive\" type=\"application/javascript\" apiFramework=\"VPAID\"><![CDATA[https://js.brealtime.com/ovvbundle-vcr_1.1.js]]></MediaFile></MediaFiles></Linear></Creative></Creatives></InLine></Ad></VAST>",
+                "id": "some_test_ad_id",
+                "impid": "some_test_ad_id",
+                "ttl": 300,
+                "crid": "148479785",
+                "w": 640,
+                "price": 0.05,
+                "adid": "148479785",
+                "h": 480
+              }]
+          }],
+          "cur": "USD"
+      }
+      }
+    }],
+  
+    "expectedBids": [{
+      "bid": {
+        "adm": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><VAST version=\"2.0\"><Ad id=\"VPAID\"><InLine><Creatives><Creative sequence=\"1\"><Linear><Duration>00:00:15</Duration><AdParameters><![CDATA[{\"adType\":\"VIDEO\",\"ad\":{\"viewTrk\":\"https%3A%2F%2Fimp.emxdgt.com%2Fview%2F%3Fcp%3D30.0000%26ts%3D1583442857%26adom%3Dhttps%3A%2F%2Femxdigital.com%26seat%3D1356%26w%3D5%26h%3D5%26pb%3D24%26sid%3D7732%26tid%3D71423%26pid%3D1158%26uid%3D29451583442857274462e1%26wid%3D1%26dom%3Dwww.brit.co%26tp%3D%24%7BEMX_MACRO%7D%26rf%3D%24%7BRF_MACRO%7D%26cat%3D3-1%26crid%3D209610044\",\"tag\":\"%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%20standalone%3D%22yes%22%3F%3E%3CVAST%20version%3D%223.0%22%3E%3CAd%20id%3D%22209610044%22%3E%3CWrapper%3E%3CAdSystem%20version%3D%223.0%22%3Eadnxs%3C%2FAdSystem%3E%3CVASTAdTagURI%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fnym1-ib.adnxs.com%2Fab%3Fan_audit%3D0%26referrer%3Dhttps%253A%252F%252Fwww.brit.co%252Fthese-actors-cant-escape-their-famous-lines%252F%26e%3DwqT_3QLkCvBDZAUAAAMA1gAFAQir14XzBRCQ2fHY6OTvjT4YhqCpw5iblL0LKjYJR6zFpwAAPkARR6zFpwAAPkAZAAAA4FE4NkAhR6wJGwApESTIMQAAAAAp3CNAMPqI3Qc4zApAzApIAlC8yvljWL_Bc2AAaIrFCni14gSAAQGKAQNVU0SSBQbwPpgBAaABAagBAbABALgBA8ABBMgBAtABCdgBAOABAPABAIoCO3VmKCdhJywgMjg5MTAxLCAxNTgzNDQyODU5KQUcAHIBHCAwOTYxMDA0NCwyHwD0FwGSAt0CIXB6OWpKZ2l0b3NvU0VMekstV01ZQUNDX3dYTXdBRGdBUUFSSXpBcFEtb2pkQjFnQVlMNENhQUJ3QUhnQWdBRUFpQUVBa0FFQW1BRUFvQUVCcUFFRHNBRUF1UUh0QktEMkFBQS1RTUVCN1FTZzlnQUFQa0RKQVZ3MUt3WjloQUJBMlFFQUFBQUFBQUR3UC1BQkFQVUJBQUFBQUpnQ0FLQUNBTFVDQUFBQUFMMENBQUFBQU9BQ0FPZ0NBUGdDQUlBREFaZ0RBYWdEcmFMS0Vyb0RDVTVaVFRJNk5ERXlNLUFEZ1J5SUJBQ1FCQUNZQkFHeUJBb0kwN2JfQ1JDNTRjb013UVFBQUFBQUFBQUFBTWtFQUFBBWcQQUFEUkINCiB3Q0ZBMkFRQTguLAAgSWdGbXlDcEJRDSUUUEFfc1FVDQ0BASwuLpoCiQEhYkJBdko6YQEkdjhGeklBUW9BRBH9XEEtUURvSlRsbE5Nam8wTVRJelFJRWNTUQFJBEFBAV0AVREMDEFBQVcdDABZHQwAYR0MAGMNDAG98LBlQUEu2AKsA-ACiZ1S6gJAaHR0cHM6Ly93d3cuYnJpdC5jby90aGVzZS1hY3RvcnMtY2FudC1lc2NhcGUtdGhlaXItZmFtb3VzLWxpbmVzL_ICEAoGQURWX0lEEgYyODkxMDHyAhIKBkNQR19JRBIIMTExMDg3NznyAhEKBUNQX0lEEggzODk2NTU0OfICDQoIQURWX0ZSRVESATDyAg0KCFJFTV9VU0VSEgEw8gIMCggBIDRDT0RFEgDyAgwKCENQRxUPEAsKB0NQFQ4cCQoFSU9fSUQBKRALCgdJTxUaOBMKD0NVU1RPTV9NT0RFTA0kCBoKFjIWABxMRUFGX05BTQVqCB4KGjYdAAhBU1QBPgxJRklFBWIcDQoIU1BMSVQBTfCLATCAAwCIAwGQAwCYAxSgAwGqAwDAA-CoAcgDANgD3q434AMA6AMA-AMDgAQAkgQJL29wZW5ydGIymAQAogQOMTA0LjE2Mi41MC4yNDWoBAGyBBIIARAQGNwGIOQDKAEoAjAAOAO4BADABADIBIGhggHSBA4xMzU2I05ZTTI6NDEyM9oEAggB4AQA8ASBSgz6BBIJgWgQiFxEQBGBjDDpdVLAiAUBmAUAoAX_EQEgAaoFFjI5NDUxkSc0NzI3NDQ2MmUxwAUAyQUBQRgAAPA_0gUJBU0BAXDYBQHgBQHwBYCvM_oFBAgAEACQBgGYBgC4BgDBBgEhATEg0AbCBNoGFgoQCREZAVAQABgA4AYE8gYCCACABwGIBwCgB0A.%26s%3Dc26c363d17931ce09fc5383e6f8aa1ff7e9324ca%26pp%3D%24%7BEMX_MACRO%7D%5D%5D%3E%3C%2FVASTAdTagURI%3E%3CImpression%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fib.adnxs.com%2Fnop%5D%5D%3E%3C%2FImpression%3E%3CCreatives%3E%3CCreative%20adID%3D%22209610044%22%3E%3CLinear%3E%3C%2FLinear%3E%3C%2FCreative%3E%3C%2FCreatives%3E%3CImpression%3E%3C%21%5BCDATA%5Bhttps%3A%2F%2Fimp.emxdgt.com%2Fimp%2F%3Fcp%3D30.0000%26ts%3D1583442857%26adom%3Dhttps%3A%2F%2Femxdigital.com%26seat%3D1356%26w%3D5%26h%3D5%26pb%3D24%26sid%3D7732%26tid%3D71423%26pid%3D1158%26uid%3D29451583442857274462e1%26wid%3D1%26dom%3Dwww.brit.co%26tp%3D%24%7BEMX_MACRO%7D%26rf%3D%24%7BRF_MACRO%7D%26cat%3D3-1%26crid%3D209610044%5D%5D%3E%3C%2FImpression%3E%3C%2FWrapper%3E%3C%2FAd%3E%3C%2FVAST%3E\",\"avg\": \"30.0000\", \"auction\" : \"${AUCTION_PRICE}\"}}]]></AdParameters><MediaFiles><MediaFile delivery=\"progressive\" type=\"application/javascript\" apiFramework=\"VPAID\"><![CDATA[https://js.brealtime.com/ovvbundle-vcr_1.1.js]]></MediaFile></MediaFiles></Linear></Creative></Creatives></InLine></Ad></VAST>",
+        "id": "some_test_ad_id",
+        "impid": "some_test_ad_id",
+        "ttl": 300,
+        "crid": "148479785",
+        "w": 640,
+        "price": 0.05,
+        "adid": "148479785",
+        "h": 480
+      },
+      "type": "video"
+    }]
+  }
+  
\ No newline at end of file
diff --git a/adapters/emx_digital/emx_digitaltest/params/race/video.json b/adapters/emx_digital/emx_digitaltest/params/race/video.json
new file mode 100644
index 00000000..d3e484af
--- /dev/null
+++ b/adapters/emx_digital/emx_digitaltest/params/race/video.json
@@ -0,0 +1,4 @@
+{
+    "tagid": "25251",
+    "bidfloor": "0.01"
+}
diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/add-bidfloor.json b/adapters/emx_digital/emx_digitaltest/supplemental/add-bidfloor.json
index 98e04887..a4041a1d 100644
--- a/adapters/emx_digital/emx_digitaltest/supplemental/add-bidfloor.json
+++ b/adapters/emx_digital/emx_digitaltest/supplemental/add-bidfloor.json
@@ -25,6 +25,12 @@
                         "h": 728,
                         "w": 90
                     },
+                    "ext": {
+                        "bidder": {
+                            "tagid": "1234568",
+                            "bidfloor": "1.25"
+                        }
+                    },
                     "id": "some_test_ad",
                     "secure": 0,
                     "tagid": "1234568",
@@ -40,4 +46,4 @@
           }
         }
     }]
-}
\ No newline at end of file
+}
diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/app-domain-and-url-correctly-parsed.json b/adapters/emx_digital/emx_digitaltest/supplemental/app-domain-and-url-correctly-parsed.json
new file mode 100644
index 00000000..5fd563dd
--- /dev/null
+++ b/adapters/emx_digital/emx_digitaltest/supplemental/app-domain-and-url-correctly-parsed.json
@@ -0,0 +1,66 @@
+{
+    "mockBidRequest": {
+      "id": "some_test_auction",
+      "imp": [
+        {
+          "id": "test-imp-id",
+          "banner": {
+            "format": [{
+              "w": 300,
+              "h": 600
+          }],
+            "w": 300,
+            "h": 600
+          },
+          "ext": {
+            "bidder": {
+              "tagid": "123456"
+            }
+          }
+        }
+      ],
+      "app": {
+        "domain": "www.publisher.com",
+        "storeurl": "http://www.publisher.com/awesome/site?with=some&parameters=here"
+      }
+    },
+
+    "httpCalls": [
+      {
+        "expectedRequest": {
+          "uri": "https://hb.emxdgt.com?t=1000&ts=2060541160",
+          "body": {
+            "id": "some_test_auction",
+            "imp": [{
+                "id": "test-imp-id",
+                "banner": {
+                    "format": [{
+                        "w": 300,
+                        "h": 600
+                    }],
+                    "w": 300,
+                    "h": 600
+                },
+                "ext": {
+                  "bidder": {
+                    "tagid": "123456"
+                  }
+                },
+                "tagid": "123456",
+                "secure": 0
+            }],
+            "app": {
+                "domain": "www.publisher.com",
+                "storeurl": "http://www.publisher.com/awesome/site?with=some&parameters=here"
+            }
+          }
+        },
+        "mockResponse": {
+          "status": 200,
+          "body": {
+
+          }
+        }
+      }
+    ]
+  }
diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/app-storeUrl-correctly-parsed.json b/adapters/emx_digital/emx_digitaltest/supplemental/app-storeUrl-correctly-parsed.json
new file mode 100644
index 00000000..8c8fe35b
--- /dev/null
+++ b/adapters/emx_digital/emx_digitaltest/supplemental/app-storeUrl-correctly-parsed.json
@@ -0,0 +1,64 @@
+{
+    "mockBidRequest": {
+      "id": "some_test_auction",
+      "imp": [
+        {
+          "id": "test-imp-id",
+          "banner": {
+            "format": [{
+              "w": 300,
+              "h": 600
+          }],
+            "w": 300,
+            "h": 600
+          },
+          "ext": {
+            "bidder": {
+              "tagid": "123456"
+            }
+          }
+        }
+      ],
+      "app": {
+        "storeurl": "http://www.publisher.com/awesome/site?with=some&parameters=here"
+      }
+    },
+
+    "httpCalls": [
+      {
+        "expectedRequest": {
+          "uri": "https://hb.emxdgt.com?t=1000&ts=2060541160",
+          "body": {
+            "id": "some_test_auction",
+            "imp": [{
+                "id": "test-imp-id",
+                "banner": {
+                    "format": [{
+                        "w": 300,
+                        "h": 600
+                    }],
+                    "w": 300,
+                    "h": 600
+                },
+                "ext": {
+                  "bidder": {
+                    "tagid": "123456"
+                  }
+                },
+                "tagid": "123456",
+                "secure": 0
+            }],
+            "app": {
+                "storeurl": "http://www.publisher.com/awesome/site?with=some&parameters=here"
+            }
+          }
+        },
+        "mockResponse": {
+          "status": 200,
+          "body": {
+
+          }
+        }
+      }
+    ]
+  }
diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/bad-imp-banner-missing-sizes.json b/adapters/emx_digital/emx_digitaltest/supplemental/bad-imp-banner-missing-sizes.json
index 975d5335..3265abd1 100644
--- a/adapters/emx_digital/emx_digitaltest/supplemental/bad-imp-banner-missing-sizes.json
+++ b/adapters/emx_digital/emx_digitaltest/supplemental/bad-imp-banner-missing-sizes.json
@@ -28,4 +28,4 @@
       "comparison": "literal"
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/bad-imp-ext-tagid-value.json b/adapters/emx_digital/emx_digitaltest/supplemental/bad-imp-ext-tagid-value.json
index 4116c09d..279b007e 100644
--- a/adapters/emx_digital/emx_digitaltest/supplemental/bad-imp-ext-tagid-value.json
+++ b/adapters/emx_digital/emx_digitaltest/supplemental/bad-imp-ext-tagid-value.json
@@ -30,4 +30,4 @@
       "comparison": "literal"
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/bad-imp-video-missing-mimes.json b/adapters/emx_digital/emx_digitaltest/supplemental/bad-imp-video-missing-mimes.json
new file mode 100644
index 00000000..cf027cbf
--- /dev/null
+++ b/adapters/emx_digital/emx_digitaltest/supplemental/bad-imp-video-missing-mimes.json
@@ -0,0 +1,34 @@
+{
+    "mockBidRequest": {
+      "id": "some_test_auction",
+      "imp": [
+        {
+          "id": "test-imp-id",
+          "video": {
+              "w":350,
+              "h":250
+          },
+          "ext": {
+            "bidder": {
+              "tagid": "12356"
+            }
+          }
+        }
+      ],
+      "site": {
+        "domain": "www.publisher.com",
+        "page": "http://www.publisher.com/awesome/site?with=some&parameters=here"
+      }
+    },
+    "expectedMakeRequestsErrors": [
+      {
+        "value": "Video: missing required field mimes",
+        "comparison": "literal"
+      },
+      {
+        "value": "Error in preprocess of Imp, err: [Video: missing required field mimes]",
+        "comparison": "literal"
+      }
+    ]
+  }
+  
\ No newline at end of file
diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/bad-imp-video-missing-sizes.json b/adapters/emx_digital/emx_digitaltest/supplemental/bad-imp-video-missing-sizes.json
new file mode 100644
index 00000000..3316a553
--- /dev/null
+++ b/adapters/emx_digital/emx_digitaltest/supplemental/bad-imp-video-missing-sizes.json
@@ -0,0 +1,36 @@
+{
+    "mockBidRequest": {
+      "id": "some_test_auction",
+      "imp": [
+        {
+          "id": "test-imp-id",
+          "video": {
+            "mimes": [
+              "video/mp4",
+              "application/javascript"
+            ]
+          },
+          "ext": {
+            "bidder": {
+              "tagid": "12356"
+            }
+          }
+        }
+      ],
+      "site": {
+        "domain": "www.publisher.com",
+        "page": "http://www.publisher.com/awesome/site?with=some&parameters=here"
+      }
+    },
+    "expectedMakeRequestsErrors": [
+      {
+        "value": "Video: Need at least one size to build request",
+        "comparison": "literal"
+      },
+      {
+        "value": "Error in preprocess of Imp, err: [Video: Need at least one size to build request]",
+        "comparison": "literal"
+      }
+    ]
+  }
+  
\ No newline at end of file
diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/build-banner-object.json b/adapters/emx_digital/emx_digitaltest/supplemental/build-banner-object.json
index de1465e9..87418621 100644
--- a/adapters/emx_digital/emx_digitaltest/supplemental/build-banner-object.json
+++ b/adapters/emx_digital/emx_digitaltest/supplemental/build-banner-object.json
@@ -48,7 +48,13 @@
                     "secure": 0,
                     "tagid": "1234568",
                     "bidfloor": 1.25,
-                    "bidfloorcur": "USD"
+                    "bidfloorcur": "USD",
+                    "ext": {
+                        "bidder": {
+                            "tagid": "1234568",
+                            "bidfloor": "1.25"
+                        }
+                    }
                 }]
             }
         },
@@ -59,4 +65,4 @@
           }
         }
     }]
-}
\ No newline at end of file
+}
diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/build-video-object.json b/adapters/emx_digital/emx_digitaltest/supplemental/build-video-object.json
new file mode 100644
index 00000000..a8cbc270
--- /dev/null
+++ b/adapters/emx_digital/emx_digitaltest/supplemental/build-video-object.json
@@ -0,0 +1,69 @@
+{
+    "mockBidRequest": {
+        "id": "some_test_auction",
+        "imp": [{
+            "id": "some_test_ad",
+            "video":{
+                "mimes": [
+                  "video/mp4",
+                  "application/javascript"
+                ],
+                "protocols":[
+                   2,
+                   3,
+                   5,
+                   6
+                ],
+                "w":640,
+                "h":480
+             },
+            "ext": {
+                "bidder": {
+                    "tagid": "1234568",
+                    "bidfloor": "1.25"
+                }
+            }
+        }]
+    },
+    "httpCalls": [{
+        "expectedRequest": {
+            "uri": "https://hb.emxdgt.com?t=1000&ts=2060541160",
+            "body": {
+                "id": "some_test_auction",
+                "imp": [{
+                    "video":{
+                        "mimes": [
+                          "video/mp4",
+                          "application/javascript"
+                        ],
+                        "protocols":[
+                           2,
+                           3,
+                           5,
+                           6
+                        ],
+                        "w":640,
+                        "h":480
+                     },
+                    "id": "some_test_ad",
+                    "secure": 0,
+                    "tagid": "1234568",
+                    "bidfloor": 1.25,
+                    "bidfloorcur": "USD",
+                    "ext": {
+                        "bidder": {
+                            "tagid": "1234568",
+                            "bidfloor": "1.25"
+                        }
+                    }
+                }]
+            }
+        },
+        "mockResponse": {
+          "status": 200,
+          "body": {
+
+          }
+        }
+    }]
+}
diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/invalid-response-no-bids.json b/adapters/emx_digital/emx_digitaltest/supplemental/invalid-response-no-bids.json
index b27f9866..d6842935 100644
--- a/adapters/emx_digital/emx_digitaltest/supplemental/invalid-response-no-bids.json
+++ b/adapters/emx_digital/emx_digitaltest/supplemental/invalid-response-no-bids.json
@@ -31,6 +31,11 @@
                   "h": 300
                 },
                 "secure": 0,
+                "ext": {
+                  "bidder": {
+                    "tagid": "98765"
+                  }
+                },
                 "tagid": "98765"
               }
             ]
diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/invalid-response-unmarshall-error.json b/adapters/emx_digital/emx_digitaltest/supplemental/invalid-response-unmarshall-error.json
index 21492e74..1281db66 100644
--- a/adapters/emx_digital/emx_digitaltest/supplemental/invalid-response-unmarshall-error.json
+++ b/adapters/emx_digital/emx_digitaltest/supplemental/invalid-response-unmarshall-error.json
@@ -27,6 +27,11 @@
                     },
                     "id": "some_test_ad",
                     "secure": 0,
+                    "ext": {
+                        "bidder": {
+                            "tagid": "654321"
+                        }
+                    },
                     "tagid": "654321"
                 }]
             }
@@ -60,4 +65,4 @@
             "comparison": "regex"
         }
     ]
-}
\ No newline at end of file
+}
diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/no-imps-in-request.json b/adapters/emx_digital/emx_digitaltest/supplemental/no-imps-in-request.json
index 274a3422..9527e78e 100644
--- a/adapters/emx_digital/emx_digitaltest/supplemental/no-imps-in-request.json
+++ b/adapters/emx_digital/emx_digitaltest/supplemental/no-imps-in-request.json
@@ -15,4 +15,4 @@
           "comparison": "literal"
         }
       ]
-}
\ No newline at end of file
+}
diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/server-error-code.json b/adapters/emx_digital/emx_digitaltest/supplemental/server-error-code.json
index a18ef316..e2652b36 100644
--- a/adapters/emx_digital/emx_digitaltest/supplemental/server-error-code.json
+++ b/adapters/emx_digital/emx_digitaltest/supplemental/server-error-code.json
@@ -31,6 +31,11 @@
                   "h": 300
                 },
                 "secure": 0,
+                "ext": {
+                  "bidder": {
+                    "tagid": "1234567"
+                  }
+                },
                 "tagid": "1234567"
               }
             ]
diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/server-no-content.json b/adapters/emx_digital/emx_digitaltest/supplemental/server-no-content.json
index 67fdbc39..d75bee4f 100644
--- a/adapters/emx_digital/emx_digitaltest/supplemental/server-no-content.json
+++ b/adapters/emx_digital/emx_digitaltest/supplemental/server-no-content.json
@@ -1,44 +1,49 @@
 {
-    "mockBidRequest": {
-    "id": "some_test_auction",
-    "imp": [
-      {
-        "id": "test-imp-id",
-        "banner": {
-            "w": 300,
-            "h": 250
-        },
-        "ext": {
-          "bidder": {
-            "tagId": "1234567"
-          }
+  "mockBidRequest": {
+  "id": "some_test_auction",
+  "imp": [
+    {
+      "id": "test-imp-id",
+      "banner": {
+          "w": 300,
+          "h": 250
+      },
+      "ext": {
+        "bidder": {
+          "tagId": "1234567"
         }
       }
-    ]
-  },
+    }
+  ]
+},
 
-    "httpCalls": [
-      {
-        "expectedRequest": {
-          "uri": "https://hb.emxdgt.com?t=1000&ts=2060541160",
-          "body": {
-            "id": "some_test_auction",
-            "imp": [
-              {
-                "id": "test-imp-id",
-                "banner": {
-                    "w": 300,
-                    "h": 250
-                },
-                "secure": 0,
-                "tagid": "1234567"
-              }
-            ]
-          }
-        },
-        "mockResponse": {
-          "status": 204
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://hb.emxdgt.com?t=1000&ts=2060541160",
+        "body": {
+          "id": "some_test_auction",
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "banner": {
+                  "w": 300,
+                  "h": 250
+              },
+              "secure": 0,
+              "ext": {
+                "bidder": {
+                  "tagId": "1234567"
+                }
+              },
+              "tagid": "1234567"
+            }
+          ]
         }
+      },
+      "mockResponse": {
+        "status": 204
       }
-    ]
     }
+  ]
+}
diff --git a/adapters/emx_digital/emx_digitaltest/supplemental/site-domain-and-url-correctly-parsed.json b/adapters/emx_digital/emx_digitaltest/supplemental/site-domain-and-url-correctly-parsed.json
index be9bdb94..e92ccea6 100644
--- a/adapters/emx_digital/emx_digitaltest/supplemental/site-domain-and-url-correctly-parsed.json
+++ b/adapters/emx_digital/emx_digitaltest/supplemental/site-domain-and-url-correctly-parsed.json
@@ -41,6 +41,11 @@
                     "w": 300,
                     "h": 600
                 },
+                "ext": {
+                  "bidder": {
+                    "tagid": "123456"
+                  }
+                },
                 "tagid": "123456",
                 "secure": 0
             }],
diff --git a/config/stored_requests_test.go b/config/stored_requests_test.go
index 36a5e379..a3bd5a96 100644
--- a/config/stored_requests_test.go
+++ b/config/stored_requests_test.go
@@ -75,43 +75,83 @@ func TestPostgressConnString(t *testing.T) {
 	assertHasValue(t, params, "sslmode", "disable")
 }
 
-func TestInMemoryCacheValidation(t *testing.T) {
+func TestInMemoryCacheValidationStoredRequests(t *testing.T) {
 	assertNoErrs(t, (&InMemoryCache{
 		Type: "unbounded",
-	}).validate("Test", nil))
+	}).validate(RequestDataType, nil))
 	assertNoErrs(t, (&InMemoryCache{
 		Type: "none",
-	}).validate("Test", nil))
+	}).validate(RequestDataType, nil))
 	assertNoErrs(t, (&InMemoryCache{
 		Type:             "lru",
 		RequestCacheSize: 1000,
 		ImpCacheSize:     1000,
-	}).validate("Test", nil))
+	}).validate(RequestDataType, nil))
 	assertErrsExist(t, (&InMemoryCache{
 		Type: "unrecognized",
-	}).validate("Test", nil))
+	}).validate(RequestDataType, nil))
 	assertErrsExist(t, (&InMemoryCache{
 		Type:         "unbounded",
 		ImpCacheSize: 1000,
-	}).validate("Test", nil))
+	}).validate(RequestDataType, nil))
 	assertErrsExist(t, (&InMemoryCache{
 		Type:             "unbounded",
 		RequestCacheSize: 1000,
-	}).validate("Test", nil))
+	}).validate(RequestDataType, nil))
 	assertErrsExist(t, (&InMemoryCache{
 		Type: "unbounded",
 		TTL:  500,
-	}).validate("Test", nil))
+	}).validate(RequestDataType, nil))
 	assertErrsExist(t, (&InMemoryCache{
 		Type:             "lru",
 		RequestCacheSize: 0,
 		ImpCacheSize:     1000,
-	}).validate("Test", nil))
+	}).validate(RequestDataType, nil))
 	assertErrsExist(t, (&InMemoryCache{
 		Type:             "lru",
 		RequestCacheSize: 1000,
 		ImpCacheSize:     0,
-	}).validate("Test", nil))
+	}).validate(RequestDataType, nil))
+	assertErrsExist(t, (&InMemoryCache{
+		Type: "lru",
+		Size: 1000,
+	}).validate(RequestDataType, nil))
+}
+
+func TestInMemoryCacheValidationSingleCache(t *testing.T) {
+	assertNoErrs(t, (&InMemoryCache{
+		Type: "unbounded",
+	}).validate(AccountDataType, nil))
+	assertNoErrs(t, (&InMemoryCache{
+		Type: "none",
+	}).validate(AccountDataType, nil))
+	assertNoErrs(t, (&InMemoryCache{
+		Type: "lru",
+		Size: 1000,
+	}).validate(AccountDataType, nil))
+	assertErrsExist(t, (&InMemoryCache{
+		Type: "unrecognized",
+	}).validate(AccountDataType, nil))
+	assertErrsExist(t, (&InMemoryCache{
+		Type: "unbounded",
+		Size: 1000,
+	}).validate(AccountDataType, nil))
+	assertErrsExist(t, (&InMemoryCache{
+		Type: "unbounded",
+		TTL:  500,
+	}).validate(AccountDataType, nil))
+	assertErrsExist(t, (&InMemoryCache{
+		Type: "lru",
+		Size: 0,
+	}).validate(AccountDataType, nil))
+	assertErrsExist(t, (&InMemoryCache{
+		Type:             "lru",
+		RequestCacheSize: 1000,
+	}).validate(AccountDataType, nil))
+	assertErrsExist(t, (&InMemoryCache{
+		Type:         "lru",
+		ImpCacheSize: 1000,
+	}).validate(AccountDataType, nil))
 }
 
 func assertErrsExist(t *testing.T, err configErrors) {
diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go
index be57271e..b8dd75b2 100644
--- a/endpoints/openrtb2/amp_auction_test.go
+++ b/endpoints/openrtb2/amp_auction_test.go
@@ -1232,7 +1232,7 @@ func TestBuildAmpObject(t *testing.T) {
 					},
 					AT:   1,
 					TMax: 500,
-					Ext:  json.RawMessage(`{"prebid":{"cache":{"bids":{"returnCreative":null},"vastxml":null},"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":20,"increment":0.1}]},"includewinners":true,"includebidderkeys":true,"includebrandcategory":null,"includeformat":false,"durationrangesec":null}}}`),
+					Ext:  json.RawMessage(`{"prebid":{"cache":{"bids":{"returnCreative":null},"vastxml":null},"targeting":{"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":20,"increment":0.1}]},"includewinners":true,"includebidderkeys":true,"includebrandcategory":null,"includeformat":false,"durationrangesec":null,"preferdeals":false}}}`),
 				},
 				AuctionResponse: &openrtb.BidResponse{
 					SeatBid: []openrtb.SeatBid{{
diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go
index 27741b22..33411c87 100644
--- a/endpoints/openrtb2/video_auction_test.go
+++ b/endpoints/openrtb2/video_auction_test.go
@@ -1149,6 +1149,48 @@ func TestCCPA(t *testing.T) {
 	}
 }
 
+func TestVideoEndpointAppendBidderNames(t *testing.T) {
+	ex := &mockExchangeAppendBidderNames{}
+	reqData, err := ioutil.ReadFile("sample-requests/video/video_valid_sample_appendbiddernames.json")
+	if err != nil {
+		t.Fatalf("Failed to fetch a valid request: %v", err)
+	}
+	reqBody := string(getRequestPayload(t, reqData))
+	req := httptest.NewRequest("POST", "/openrtb2/video", strings.NewReader(reqBody))
+	recorder := httptest.NewRecorder()
+
+	deps := mockDepsAppendBidderNames(t, ex)
+	deps.VideoAuctionEndpoint(recorder, req, nil)
+
+	if ex.lastRequest == nil {
+		t.Fatalf("The request never made it into the Exchange.")
+	}
+
+	var extData openrtb_ext.ExtRequest
+	json.Unmarshal(ex.lastRequest.Ext, &extData)
+	assert.True(t, extData.Prebid.Targeting.AppendBidderNames, "Request ext incorrect: AppendBidderNames should be true ")
+
+	respBytes := recorder.Body.Bytes()
+	resp := &openrtb_ext.BidResponseVideo{}
+	if err := json.Unmarshal(respBytes, resp); err != nil {
+		t.Fatalf("Unable to unmarshal response.")
+	}
+
+	assert.Len(t, ex.lastRequest.Imp, 11, "Incorrect number of impressions in request")
+	assert.Equal(t, string(ex.lastRequest.Site.Page), "prebid.com", "Incorrect site page in request")
+	assert.Equal(t, ex.lastRequest.Site.Content.Series, "TvName", "Incorrect site content series in request")
+
+	assert.Len(t, resp.AdPods, 5, "Incorrect number of Ad Pods in response")
+	assert.Len(t, resp.AdPods[0].Targeting, 4, "Incorrect Targeting data in response")
+	assert.Len(t, resp.AdPods[1].Targeting, 3, "Incorrect Targeting data in response")
+	assert.Len(t, resp.AdPods[2].Targeting, 5, "Incorrect Targeting data in response")
+	assert.Len(t, resp.AdPods[3].Targeting, 1, "Incorrect Targeting data in response")
+	assert.Len(t, resp.AdPods[4].Targeting, 3, "Incorrect Targeting data in response")
+
+	assert.Equal(t, resp.AdPods[4].Targeting[0].HbPbCatDur, "20.00_395_30s_appnexus", "Incorrect number of Ad Pods in response")
+
+}
+
 func TestFormatTargetingKey(t *testing.T) {
 	res := formatTargetingKey(openrtb_ext.HbCategoryDurationKey, "appnexus")
 	assert.Equal(t, "hb_pb_cat_dur_appnex", res, "Tergeting key constructed incorrectly")
@@ -1229,6 +1271,30 @@ func mockDeps(t *testing.T, ex *mockExchangeVideo) *endpointDeps {
 	return deps
 }
 
+func mockDepsAppendBidderNames(t *testing.T, ex *mockExchangeAppendBidderNames) *endpointDeps {
+	theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{})
+	deps := &endpointDeps{
+		ex,
+		newParamsValidator(t),
+		&mockVideoStoredReqFetcher{},
+		&mockVideoStoredReqFetcher{},
+		empty_fetcher.EmptyFetcher{},
+		empty_fetcher.EmptyFetcher{},
+		&config.Configuration{MaxRequestSize: maxSize},
+		theMetrics,
+		analyticsConf.NewPBSAnalytics(&config.Analytics{}),
+		map[string]string{},
+		false,
+		[]byte{},
+		openrtb_ext.BidderMap,
+		ex.cache,
+		regexp.MustCompile(`[<>]`),
+		hardcodedResponseIPValidator{response: true},
+	}
+
+	return deps
+}
+
 func mockDepsNoBids(t *testing.T, ex *mockExchangeVideoNoBids) *endpointDeps {
 	theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{})
 	edep := &endpointDeps{
@@ -1311,6 +1377,42 @@ func (m *mockExchangeVideo) HoldAuction(ctx context.Context, bidRequest *openrtb
 	}, nil
 }
 
+type mockExchangeAppendBidderNames struct {
+	lastRequest *openrtb.BidRequest
+	cache       *mockCacheClient
+}
+
+func (m *mockExchangeAppendBidderNames) HoldAuction(ctx context.Context, bidRequest *openrtb.BidRequest, ids exchange.IdFetcher, labels pbsmetrics.Labels, account *config.Account, categoriesFetcher *stored_requests.CategoryFetcher, debugLog *exchange.DebugLog) (*openrtb.BidResponse, error) {
+	m.lastRequest = bidRequest
+	if debugLog != nil && debugLog.Enabled {
+		m.cache.called = true
+	}
+	ext := []byte(`{"prebid":{"targeting":{"hb_bidder_appnexus":"appnexus","hb_pb_appnexus":"20.00","hb_pb_cat_dur_appnex":"20.00_395_30s_appnexus","hb_size":"1x1", "hb_uuid_appnexus":"837ea3b7-5598-4958-8c45-8e9ef2bf7cc1"},"type":"video"},"bidder":{"appnexus":{"brand_id":1,"auction_id":7840037870526938650,"bidder_id":2,"bid_ad_type":1,"creative_info":{"video":{"duration":30,"mimes":["video\/mp4"]}}}}}`)
+	return &openrtb.BidResponse{
+		SeatBid: []openrtb.SeatBid{{
+			Seat: "appnexus",
+			Bid: []openrtb.Bid{
+				{ID: "01", ImpID: "1_0", Ext: ext},
+				{ID: "02", ImpID: "1_1", Ext: ext},
+				{ID: "03", ImpID: "1_2", Ext: ext},
+				{ID: "04", ImpID: "1_3", Ext: ext},
+				{ID: "05", ImpID: "2_0", Ext: ext},
+				{ID: "06", ImpID: "2_1", Ext: ext},
+				{ID: "07", ImpID: "2_2", Ext: ext},
+				{ID: "08", ImpID: "3_0", Ext: ext},
+				{ID: "09", ImpID: "3_1", Ext: ext},
+				{ID: "10", ImpID: "3_2", Ext: ext},
+				{ID: "11", ImpID: "3_3", Ext: ext},
+				{ID: "12", ImpID: "3_5", Ext: ext},
+				{ID: "13", ImpID: "4_0", Ext: ext},
+				{ID: "14", ImpID: "5_0", Ext: ext},
+				{ID: "15", ImpID: "5_1", Ext: ext},
+				{ID: "16", ImpID: "5_2", Ext: ext},
+			},
+		}},
+	}, nil
+}
+
 type mockExchangeVideoNoBids struct {
 	lastRequest *openrtb.BidRequest
 	cache       *mockCacheClient
diff --git a/exchange/auction_test.go b/exchange/auction_test.go
index 9f24682b..cdd6a6ca 100644
--- a/exchange/auction_test.go
+++ b/exchange/auction_test.go
@@ -280,6 +280,225 @@ func runCacheSpec(t *testing.T, fileDisplayName string, specData *cacheSpec) {
 	}
 }
 
+func TestNewAuction(t *testing.T) {
+	bid1p077 := pbsOrtbBid{
+		bid: &openrtb.Bid{
+			ImpID: "imp1",
+			Price: 0.77,
+		},
+	}
+	bid1p123 := pbsOrtbBid{
+		bid: &openrtb.Bid{
+			ImpID: "imp1",
+			Price: 1.23,
+		},
+	}
+	bid1p230 := pbsOrtbBid{
+		bid: &openrtb.Bid{
+			ImpID: "imp1",
+			Price: 2.30,
+		},
+	}
+	bid1p088d := pbsOrtbBid{
+		bid: &openrtb.Bid{
+			ImpID:  "imp1",
+			Price:  0.88,
+			DealID: "SpecialDeal",
+		},
+	}
+	bid1p166d := pbsOrtbBid{
+		bid: &openrtb.Bid{
+			ImpID:  "imp1",
+			Price:  1.66,
+			DealID: "BigDeal",
+		},
+	}
+	bid2p123 := pbsOrtbBid{
+		bid: &openrtb.Bid{
+			ImpID: "imp2",
+			Price: 1.23,
+		},
+	}
+	bid2p144 := pbsOrtbBid{
+		bid: &openrtb.Bid{
+			ImpID: "imp2",
+			Price: 1.44,
+		},
+	}
+	tests := []struct {
+		description     string
+		seatBids        map[openrtb_ext.BidderName]*pbsOrtbSeatBid
+		numImps         int
+		preferDeals     bool
+		expectedAuction auction
+	}{
+		{
+			description: "Basic auction test",
+			seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{
+				"appnexus": {
+					bids: []*pbsOrtbBid{&bid1p123},
+				},
+				"rubicon": {
+					bids: []*pbsOrtbBid{&bid1p230},
+				},
+			},
+			numImps:     1,
+			preferDeals: false,
+			expectedAuction: auction{
+				winningBids: map[string]*pbsOrtbBid{
+					"imp1": &bid1p230,
+				},
+				winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{
+					"imp1": {
+						"appnexus": &bid1p123,
+						"rubicon":  &bid1p230,
+					},
+				},
+			},
+		},
+		{
+			description: "Multi-imp auction",
+			seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{
+				"appnexus": {
+					bids: []*pbsOrtbBid{&bid1p230, &bid2p123},
+				},
+				"rubicon": {
+					bids: []*pbsOrtbBid{&bid1p077, &bid2p144},
+				},
+				"openx": {
+					bids: []*pbsOrtbBid{&bid1p123},
+				},
+			},
+			numImps:     2,
+			preferDeals: false,
+			expectedAuction: auction{
+				winningBids: map[string]*pbsOrtbBid{
+					"imp1": &bid1p230,
+					"imp2": &bid2p144,
+				},
+				winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{
+					"imp1": {
+						"appnexus": &bid1p230,
+						"rubicon":  &bid1p077,
+						"openx":    &bid1p123,
+					},
+					"imp2": {
+						"appnexus": &bid2p123,
+						"rubicon":  &bid2p144,
+					},
+				},
+			},
+		},
+		{
+			description: "Basic auction with deals, no preference",
+			seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{
+				"appnexus": {
+					bids: []*pbsOrtbBid{&bid1p123},
+				},
+				"rubicon": {
+					bids: []*pbsOrtbBid{&bid1p088d},
+				},
+			},
+			numImps:     1,
+			preferDeals: false,
+			expectedAuction: auction{
+				winningBids: map[string]*pbsOrtbBid{
+					"imp1": &bid1p123,
+				},
+				winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{
+					"imp1": {
+						"appnexus": &bid1p123,
+						"rubicon":  &bid1p088d,
+					},
+				},
+			},
+		},
+		{
+			description: "Basic auction with deals, prefer deals",
+			seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{
+				"appnexus": {
+					bids: []*pbsOrtbBid{&bid1p123},
+				},
+				"rubicon": {
+					bids: []*pbsOrtbBid{&bid1p088d},
+				},
+			},
+			numImps:     1,
+			preferDeals: true,
+			expectedAuction: auction{
+				winningBids: map[string]*pbsOrtbBid{
+					"imp1": &bid1p088d,
+				},
+				winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{
+					"imp1": {
+						"appnexus": &bid1p123,
+						"rubicon":  &bid1p088d,
+					},
+				},
+			},
+		},
+		{
+			description: "Auction with 2 deals",
+			seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{
+				"appnexus": {
+					bids: []*pbsOrtbBid{&bid1p166d},
+				},
+				"rubicon": {
+					bids: []*pbsOrtbBid{&bid1p088d},
+				},
+			},
+			numImps:     1,
+			preferDeals: true,
+			expectedAuction: auction{
+				winningBids: map[string]*pbsOrtbBid{
+					"imp1": &bid1p166d,
+				},
+				winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{
+					"imp1": {
+						"appnexus": &bid1p166d,
+						"rubicon":  &bid1p088d,
+					},
+				},
+			},
+		},
+		{
+			description: "Auction with 3 bids and 2 deals",
+			seatBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{
+				"appnexus": {
+					bids: []*pbsOrtbBid{&bid1p166d},
+				},
+				"rubicon": {
+					bids: []*pbsOrtbBid{&bid1p088d},
+				},
+				"openx": {
+					bids: []*pbsOrtbBid{&bid1p230},
+				},
+			},
+			numImps:     1,
+			preferDeals: true,
+			expectedAuction: auction{
+				winningBids: map[string]*pbsOrtbBid{
+					"imp1": &bid1p166d,
+				},
+				winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{
+					"imp1": {
+						"appnexus": &bid1p166d,
+						"rubicon":  &bid1p088d,
+						"openx":    &bid1p230,
+					},
+				},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		auc := newAuction(test.seatBids, test.numImps, test.preferDeals)
+
+		assert.Equal(t, test.expectedAuction, *auc, test.description)
+	}
+
+}
+
 type cacheSpec struct {
 	BidRequest                  openrtb.BidRequest              `json:"bidRequest"`
 	PbsBids                     []pbsBid                        `json:"pbsBids"`
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index 2b91f1b0..ee7e27d0 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -955,9 +955,13 @@ func TestBidResponseCurrency(t *testing.T) {
 		},
 	}
 
+	bidResponseExt := &openrtb_ext.ExtBidResponse{
+		ResponseTimeMillis:   map[openrtb_ext.BidderName]int{openrtb_ext.BidderName("appnexus"): 5},
+		RequestTimeoutMillis: 500,
+	}
 	// Run tests
 	for i := range testCases {
-		actualBidResp, err := e.buildBidResponse(context.Background(), liveAdapters, testCases[i].adapterBids, bidRequest, adapterExtra, nil, nil, true, errList)
+		actualBidResp, err := e.buildBidResponse(context.Background(), liveAdapters, testCases[i].adapterBids, bidRequest, adapterExtra, nil, bidResponseExt, true, 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))
 	}
@@ -1888,6 +1892,114 @@ func TestNoCategoryDedupe(t *testing.T) {
 
 }
 
+func TestCategoryMappingBidderName(t *testing.T) {
+
+	categoriesFetcher, error := newCategoryFetcher("./test/category-mapping")
+	if error != nil {
+		t.Errorf("Failed to create a category Fetcher: %v", error)
+	}
+
+	requestExt := newExtRequest()
+	requestExt.Prebid.Targeting.AppendBidderNames = true
+
+	targData := &targetData{
+		priceGranularity: requestExt.Prebid.Targeting.PriceGranularity,
+		includeWinners:   true,
+	}
+
+	requestExt.Prebid.Targeting.DurationRangeSec = []int{15, 30}
+
+	adapterBids := make(map[openrtb_ext.BidderName]*pbsOrtbSeatBid)
+
+	cats1 := []string{"IAB1-1"}
+	cats2 := []string{"IAB1-2"}
+	bid1 := openrtb.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1}
+	bid2 := openrtb.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}, 0}
+	bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0}
+
+	innerBids1 := []*pbsOrtbBid{
+		&bid1_1,
+	}
+	innerBids2 := []*pbsOrtbBid{
+		&bid1_2,
+	}
+
+	seatBid1 := pbsOrtbSeatBid{innerBids1, "USD", nil, nil}
+	bidderName1 := openrtb_ext.BidderName("bidder1")
+
+	seatBid2 := pbsOrtbSeatBid{innerBids2, "USD", nil, nil}
+	bidderName2 := openrtb_ext.BidderName("bidder2")
+
+	adapterBids[bidderName1] = &seatBid1
+	adapterBids[bidderName2] = &seatBid2
+
+	bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData)
+
+	assert.NoError(t, err, "Category mapping error should be empty")
+	assert.Empty(t, rejections, "There should be 0 bid rejection messages")
+	assert.Equal(t, "10.00_VideoGames_30s_bidder1", bidCategory["bid_id1"], "Category mapping doesn't match")
+	assert.Equal(t, "10.00_HomeDecor_30s_bidder2", bidCategory["bid_id2"], "Category mapping doesn't match")
+	assert.Len(t, adapterBids[bidderName1].bids, 1, "Bidders number doesn't match")
+	assert.Len(t, adapterBids[bidderName2].bids, 1, "Bidders number doesn't match")
+	assert.Len(t, bidCategory, 2, "Bidders category mapping doesn't match")
+}
+
+func TestCategoryMappingBidderNameNoCategories(t *testing.T) {
+
+	categoriesFetcher, error := newCategoryFetcher("./test/category-mapping")
+	if error != nil {
+		t.Errorf("Failed to create a category Fetcher: %v", error)
+	}
+
+	requestExt := newExtRequestNoBrandCat()
+	requestExt.Prebid.Targeting.AppendBidderNames = true
+
+	targData := &targetData{
+		priceGranularity: requestExt.Prebid.Targeting.PriceGranularity,
+		includeWinners:   true,
+	}
+
+	requestExt.Prebid.Targeting.DurationRangeSec = []int{15, 30}
+
+	adapterBids := make(map[openrtb_ext.BidderName]*pbsOrtbSeatBid)
+
+	cats1 := []string{"IAB1-1"}
+	cats2 := []string{"IAB1-2"}
+	bid1 := openrtb.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1}
+	bid2 := openrtb.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}, 0}
+	bid1_2 := pbsOrtbBid{&bid2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, 0}
+
+	innerBids1 := []*pbsOrtbBid{
+		&bid1_1,
+	}
+	innerBids2 := []*pbsOrtbBid{
+		&bid1_2,
+	}
+
+	seatBid1 := pbsOrtbSeatBid{innerBids1, "USD", nil, nil}
+	bidderName1 := openrtb_ext.BidderName("bidder1")
+
+	seatBid2 := pbsOrtbSeatBid{innerBids2, "USD", nil, nil}
+	bidderName2 := openrtb_ext.BidderName("bidder2")
+
+	adapterBids[bidderName1] = &seatBid1
+	adapterBids[bidderName2] = &seatBid2
+
+	bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData)
+
+	assert.NoError(t, err, "Category mapping error should be empty")
+	assert.Empty(t, rejections, "There should be 0 bid rejection messages")
+	assert.Equal(t, "10.00_30s_bidder1", bidCategory["bid_id1"], "Category mapping doesn't match")
+	assert.Equal(t, "12.00_30s_bidder2", bidCategory["bid_id2"], "Category mapping doesn't match")
+	assert.Len(t, adapterBids[bidderName1].bids, 1, "Bidders number doesn't match")
+	assert.Len(t, adapterBids[bidderName2].bids, 1, "Bidders number doesn't match")
+	assert.Len(t, bidCategory, 2, "Bidders category mapping doesn't match")
+}
+
 func TestBidRejectionErrors(t *testing.T) {
 	categoriesFetcher, error := newCategoryFetcher("./test/category-mapping")
 	if error != nil {
diff --git a/exchange/exchangetest/append-bidder-names.json b/exchange/exchangetest/append-bidder-names.json
new file mode 100644
index 00000000..1247b9f0
--- /dev/null
+++ b/exchange/exchangetest/append-bidder-names.json
@@ -0,0 +1,222 @@
+{
+  "incomingRequest": {
+    "ortbRequest": {
+      "id": "some-request-id",
+      "site": {
+        "page": "test.somepage.com"
+      },
+      "imp": [
+        {
+          "id": "my-imp-id",
+          "video": {
+            "mimes": [
+              "video/mp4"
+            ]
+          },
+          "ext": {
+            "appnexus": {
+              "placementId": 1
+            }
+          }
+        },
+        {
+          "id": "imp-id-2",
+          "video": {
+            "mimes": [
+              "video/mp4"
+            ]
+          },
+          "ext": {
+            "appnexus": {
+              "placementId": 1
+            }
+          }
+        }
+      ],
+      "test": 1,
+      "ext": {
+        "prebid": {
+          "targeting": {
+            "includebrandcategory": {
+              "primaryadserver": 1,
+              "publisher": "",
+              "withcategory": true
+            },
+            "appendbiddernames": true
+          }
+        }
+      }
+    },
+    "usersyncs": {
+      "appnexus": "123"
+    }
+  },
+  "outgoingRequests": {
+    "appnexus": {
+      "mockResponse": {
+        "pbsSeatBid": {
+          "pbsBids": [
+            {
+              "ortbBid": {
+                "id": "apn-bid",
+                "impid": "my-imp-id",
+                "price": 0.3,
+                "w": 200,
+                "h": 250,
+                "crid": "creative-1",
+                "cat": [
+                  "IAB1-1"
+                ]
+              },
+              "bidType": "video",
+              "bidVideo": {
+                "duration": 30,
+                "PrimaryCategory": ""
+              }
+            },
+            {
+              "ortbBid": {
+                "id": "apn-other-bid",
+                "impid": "imp-id-2",
+                "price": 0.6,
+                "w": 300,
+                "h": 500,
+                "crid": "creative-3",
+                "cat": [
+                  "IAB1-2"
+                ]
+              },
+              "bidType": "video"
+            }
+          ]
+        }
+      }
+    }
+  },
+  "response": {
+    "bids": {
+      "id": "some-request-id",
+      "seatbid": [
+        {
+          "seat": "appnexus",
+          "bid": [
+            {
+              "id": "apn-bid",
+              "impid": "my-imp-id",
+              "price": 0.3,
+              "w": 200,
+              "h": 250,
+              "crid": "creative-1",
+              "cat": [
+                "IAB1-1"
+              ],
+              "ext": {
+                "prebid": {
+                  "type": "video",
+                  "targeting": {
+                    "hb_bidder": "appnexus",
+                    "hb_bidder_appnexus": "appnexus",
+                    "hb_cache_host": "www.pbcserver.com",
+                    "hb_cache_host_appnex": "www.pbcserver.com",
+                    "hb_cache_path": "/pbcache/endpoint",
+                    "hb_cache_path_appnex": "/pbcache/endpoint",
+                    "hb_pb": "0.20",
+                    "hb_pb_appnexus": "0.20",
+                    "hb_pb_cat_dur": "0.20_VideoGames_0s_appnexus",
+                    "hb_pb_cat_dur_appnex": "0.20_VideoGames_0s_appnexus",
+                    "hb_size": "200x250",
+                    "hb_size_appnexus": "200x250"
+                  }
+                }
+              }
+            },
+            {
+              "cat": [
+                "IAB1-2"
+              ],
+              "crid": "creative-3",
+              "ext": {
+                "prebid": {
+                  "targeting": {
+                    "hb_bidder": "appnexus",
+                    "hb_bidder_appnexus": "appnexus",
+                    "hb_cache_host": "www.pbcserver.com",
+                    "hb_cache_host_appnex": "www.pbcserver.com",
+                    "hb_cache_path": "/pbcache/endpoint",
+                    "hb_cache_path_appnex": "/pbcache/endpoint",
+                    "hb_pb": "0.50",
+                    "hb_pb_appnexus": "0.50",
+                    "hb_pb_cat_dur": "0.50_HomeDecor_0s_appnexus",
+                    "hb_pb_cat_dur_appnex": "0.50_HomeDecor_0s_appnexus",
+                    "hb_size": "300x500",
+                    "hb_size_appnexus": "300x500"
+                  },
+                  "type": "video"
+                }
+              },
+              "h": 500,
+              "id": "apn-other-bid",
+              "impid": "imp-id-2",
+              "price": 0.6,
+              "w": 300
+            }
+          ]
+        }
+      ]
+    },
+    "ext": {
+      "debug": {
+        "httpcalls": {
+          "appnexus": null
+        },
+        "resolvedrequest": {
+          "id": "some-request-id",
+          "imp": [
+            {
+              "id": "my-imp-id",
+              "video": {
+                "mimes": [
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "appnexus": {
+                  "placementId": 1
+                }
+              }
+            },
+            {
+              "id": "imp-id-2",
+              "video": {
+                "mimes": [
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "appnexus": {
+                  "placementId": 1
+                }
+              }
+            }
+          ],
+          "site": {
+            "page": "test.somepage.com"
+          },
+          "test": 1,
+          "ext": {
+            "prebid": {
+              "targeting": {
+                "includebrandcategory": {
+                  "primaryadserver": 1,
+                  "publisher": "",
+                  "withcategory": true
+                },
+                "appendbiddernames": true
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/exchange/price_granularity_test.go b/exchange/price_granularity_test.go
index 09af1fcd..13840838 100644
--- a/exchange/price_granularity_test.go
+++ b/exchange/price_granularity_test.go
@@ -1,9 +1,11 @@
 package exchange
 
 import (
+	"math"
 	"testing"
 
 	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestGetPriceBucketString(t *testing.T) {
@@ -28,32 +30,105 @@ func TestGetPriceBucketString(t *testing.T) {
 		},
 	}
 
-	price := 1.87
-	getOnePriceBucket(t, "low", low, price, "1.50")
-	getOnePriceBucket(t, "medium", medium, price, "1.80")
-	getOnePriceBucket(t, "high", high, price, "1.87")
-	getOnePriceBucket(t, "auto", auto, price, "1.85")
-	getOnePriceBucket(t, "dense", dense, price, "1.87")
-	getOnePriceBucket(t, "custom1", custom1, price, "1.86")
-
-	// test a cpm above the max in low price bucket
-	price = 5.72
-	getOnePriceBucket(t, "low", low, price, "5.00")
-	getOnePriceBucket(t, "medium", medium, price, "5.70")
-	getOnePriceBucket(t, "high", high, price, "5.72")
-	getOnePriceBucket(t, "auto", auto, price, "5.70")
-	getOnePriceBucket(t, "dense", dense, price, "5.70")
-	getOnePriceBucket(t, "custom1", custom1, price, "5.70")
+	// Define test cases
+	type aTest struct {
+		granularityId       string
+		granularity         openrtb_ext.PriceGranularity
+		expectedPriceBucket string
+	}
+	testGroups := []struct {
+		groupDesc string
+		cpm       float64
+		testCases []aTest
+	}{
+		{
+			groupDesc: "cpm below the max in every price bucket",
+			cpm:       1.87,
+			testCases: []aTest{
+				{"low", low, "1.50"},
+				{"medium", medium, "1.80"},
+				{"high", high, "1.87"},
+				{"auto", auto, "1.85"},
+				{"dense", dense, "1.87"},
+				{"custom1", custom1, "1.86"},
+			},
+		},
+		{
+			groupDesc: "cpm above the max in low price bucket",
+			cpm:       5.72,
+			testCases: []aTest{
+				{"low", low, "5.00"},
+				{"medium", medium, "5.70"},
+				{"high", high, "5.72"},
+				{"auto", auto, "5.70"},
+				{"dense", dense, "5.70"},
+				{"custom1", custom1, "5.70"},
+			},
+		},
+		{
+			groupDesc: "Precision value corner cases",
+			cpm:       1.876,
+			testCases: []aTest{
+				{
+					"Negative precision defaults to number of digits already in CPM float",
+					openrtb_ext.PriceGranularity{Precision: -1, Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 0.05}}},
+					"1.85",
+				},
+				{
+					"Precision value equals zero, we expect to round up to the nearest integer",
+					openrtb_ext.PriceGranularity{Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 0.05}}},
+					"2",
+				},
+				{
+					"Largest precision value PBS supports 15",
+					openrtb_ext.PriceGranularity{Precision: 15, Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 0.05}}},
+					"1.850000000000000",
+				},
+			},
+		},
+		{
+			groupDesc: "Increment value corner cases",
+			cpm:       1.876,
+			testCases: []aTest{
+				{
+					"Negative increment, return empty string",
+					openrtb_ext.PriceGranularity{Precision: 2, Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: -0.05}}},
+					"",
+				},
+				{
+					"Zero increment, return empty string",
+					openrtb_ext.PriceGranularity{Precision: 2, Ranges: []openrtb_ext.GranularityRange{{Max: 5}}},
+					"",
+				},
+				{
+					"Increment value is greater than CPM itself, return zero float value",
+					openrtb_ext.PriceGranularity{Precision: 2, Ranges: []openrtb_ext.GranularityRange{{Max: 5, Increment: 1.877}}},
+					"0.00",
+				},
+			},
+		},
+		{
+			groupDesc: "Negative Cpm, return empty string since it does not belong into any range",
+			cpm:       -1.876,
+			testCases: []aTest{{"low", low, ""}},
+		},
+		{
+			groupDesc: "Zero value Cpm, return the same, only in string format",
+			cpm:       0,
+			testCases: []aTest{{"low", low, "0.00"}},
+		},
+		{
+			groupDesc: "Large Cpm, return bucket Max",
+			cpm:       math.MaxFloat64,
+			testCases: []aTest{{"low", low, "5.00"}},
+		},
+	}
 
-}
+	for _, testGroup := range testGroups {
+		for _, test := range testGroup.testCases {
+			priceBucket := GetPriceBucket(testGroup.cpm, test.granularity)
 
-func getOnePriceBucket(t *testing.T, name string, granularity openrtb_ext.PriceGranularity, price float64, expected string) {
-	t.Helper()
-	priceBucket, err := GetCpmStringValue(price, granularity)
-	if err != nil {
-		t.Errorf("Granularity: %s :: GetPriceBucketString: %s", name, err.Error())
-	}
-	if priceBucket != expected {
-		t.Errorf("Granularity: %s :: Expected %s, got %s from %f", name, expected, priceBucket, price)
+			assert.Equal(t, test.expectedPriceBucket, priceBucket, "Group: %s Granularity: %s :: Expected %s, got %s from %f", testGroup.groupDesc, test.granularityId, test.expectedPriceBucket, priceBucket, testGroup.cpm)
+		}
 	}
 }
diff --git a/openrtb_ext/request_test.go b/openrtb_ext/request_test.go
index e4046a62..98a2e164 100644
--- a/openrtb_ext/request_test.go
+++ b/openrtb_ext/request_test.go
@@ -190,21 +190,43 @@ var validGranularityTests []granularityTestData = []granularityTestData{
 }
 
 func TestGranularityUnmarshalBad(t *testing.T) {
-	tests := [][]byte{
-		[]byte(`[]`),
-		[]byte(`{"precision": -1, "ranges": [{"max":20, "increment":0.5}]}`),
-		[]byte(`{"ranges":[{"max":20, "increment": -1}]}`),
-		[]byte(`{"ranges":[{"max":"20", "increment": "0.1"}]}`),
-		[]byte(`{"ranges":[{"max":20, "increment":0.1}. {"max":10, "increment":0.02}]}`),
-		[]byte(`{"ranges":[{"max":20, "min":10, "increment": 0.1}, {"max":10, "min":0, "increment":0.05}]}`),
-		[]byte(`{"ranges":[{"max":1.0, "increment": 0.07}, {"max" 1.0, "increment": 0.03}]}`),
+	testCases := []struct {
+		description          string
+		jsonPriceGranularity []byte
+	}{
+		{
+			"Malformed",
+			[]byte(`[]`),
+		},
+		{
+			"Negative precision",
+			[]byte(`{"precision": -1, "ranges": [{"max":20, "increment":0.5}]}`),
+		},
+		{
+			"Precision greater than MaxDecimalFigures supported",
+			[]byte(`{"precision": 16, "ranges": [{"max":20, "increment":0.5}]}`),
+		},
+		{
+			"Negative increment",
+			[]byte(`{"ranges":[{"max":20, "increment": -1}]}`),
+		},
+		{
+			"Range with non float64 max value",
+			[]byte(`{"ranges":[{"max":"20", "increment": "0.1"}]}`),
+		},
+		{
+			"Ranges in decreasing order",
+			[]byte(`{"ranges":[{"max":20, "increment":0.1}. {"max":10, "increment":0.02}]}`),
+		},
+		{
+			"Max equal to previous max",
+			[]byte(`{"ranges":[{"max":1.0, "increment": 0.07}, {"max" 1.0, "increment": 0.03}]}`),
+		},
 	}
-	var resolved PriceGranularity
-	for _, b := range tests {
-		resolved = PriceGranularity{}
-		err := json.Unmarshal(b, &resolved)
-		if err == nil {
-			t.Errorf("Invalid granularity unmarshalled without error.\nJSON was: %s\n Resolved to: %v", string(b), resolved)
-		}
+
+	for _, test := range testCases {
+		resolved := PriceGranularity{}
+		err := json.Unmarshal(test.jsonPriceGranularity, &resolved)
+		assert.Errorf(t, err, "Invalid granularity unmarshalled without error.\nJSON was: %s\n Resolved to: %v. Test: %s", string(test.jsonPriceGranularity), resolved, test.description)
 	}
 }
diff --git a/pbsmetrics/go_metrics_test.go b/pbsmetrics/go_metrics_test.go
index f6769916..0a763750 100644
--- a/pbsmetrics/go_metrics_test.go
+++ b/pbsmetrics/go_metrics_test.go
@@ -345,6 +345,151 @@ func TestRecordPrebidCacheRequestTimeWithNotSuccess(t *testing.T) {
 	assert.Equal(t, m.PrebidCacheRequestTimerError.Count(), int64(1))
 }
 
+func TestRecordStoredDataFetchTime(t *testing.T) {
+	tests := []struct {
+		description string
+		dataType    StoredDataType
+		fetchType   StoredDataFetchType
+	}{
+		{
+			description: "Update stored_account_fetch_time.all timer",
+			dataType:    AccountDataType,
+			fetchType:   FetchAll,
+		},
+		{
+			description: "Update stored_amp_fetch_time.all timer",
+			dataType:    AMPDataType,
+			fetchType:   FetchAll,
+		},
+		{
+			description: "Update stored_category_fetch_time.all timer",
+			dataType:    CategoryDataType,
+			fetchType:   FetchAll,
+		},
+		{
+			description: "Update stored_request_fetch_time.all timer",
+			dataType:    RequestDataType,
+			fetchType:   FetchAll,
+		},
+		{
+			description: "Update stored_video_fetch_time.all timer",
+			dataType:    VideoDataType,
+			fetchType:   FetchAll,
+		},
+		{
+			description: "Update stored_account_fetch_time.delta timer",
+			dataType:    AccountDataType,
+			fetchType:   FetchDelta,
+		},
+		{
+			description: "Update stored_amp_fetch_time.delta timer",
+			dataType:    AMPDataType,
+			fetchType:   FetchDelta,
+		},
+		{
+			description: "Update stored_category_fetch_time.delta timer",
+			dataType:    CategoryDataType,
+			fetchType:   FetchDelta,
+		},
+		{
+			description: "Update stored_request_fetch_time.delta timer",
+			dataType:    RequestDataType,
+			fetchType:   FetchDelta,
+		},
+		{
+			description: "Update stored_video_fetch_time.delta timer",
+			dataType:    VideoDataType,
+			fetchType:   FetchDelta,
+		},
+	}
+
+	for _, tt := range tests {
+		registry := metrics.NewRegistry()
+		m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus, openrtb_ext.BidderRubicon}, config.DisabledMetrics{AccountAdapterDetails: true})
+		m.RecordStoredDataFetchTime(StoredDataLabels{
+			DataType:      tt.dataType,
+			DataFetchType: tt.fetchType,
+		}, time.Duration(500))
+
+		actualCount := m.StoredDataFetchTimer[tt.dataType][tt.fetchType].Count()
+		assert.Equal(t, int64(1), actualCount, tt.description)
+
+		actualDuration := m.StoredDataFetchTimer[tt.dataType][tt.fetchType].Sum()
+		assert.Equal(t, int64(500), actualDuration, tt.description)
+	}
+}
+
+func TestRecordStoredDataError(t *testing.T) {
+	tests := []struct {
+		description string
+		dataType    StoredDataType
+		errorType   StoredDataError
+	}{
+		{
+			description: "Increment stored_account_error.network meter",
+			dataType:    AccountDataType,
+			errorType:   StoredDataErrorNetwork,
+		},
+		{
+			description: "Increment stored_amp_error.network meter",
+			dataType:    AMPDataType,
+			errorType:   StoredDataErrorNetwork,
+		},
+		{
+			description: "Increment stored_category_error.network meter",
+			dataType:    CategoryDataType,
+			errorType:   StoredDataErrorNetwork,
+		},
+		{
+			description: "Increment stored_request_error.network meter",
+			dataType:    RequestDataType,
+			errorType:   StoredDataErrorNetwork,
+		},
+		{
+			description: "Increment stored_video_error.network meter",
+			dataType:    VideoDataType,
+			errorType:   StoredDataErrorNetwork,
+		},
+		{
+			description: "Increment stored_account_error.undefined meter",
+			dataType:    AccountDataType,
+			errorType:   StoredDataErrorUndefined,
+		},
+		{
+			description: "Increment stored_amp_error.undefined meter",
+			dataType:    AMPDataType,
+			errorType:   StoredDataErrorUndefined,
+		},
+		{
+			description: "Increment stored_category_error.undefined meter",
+			dataType:    CategoryDataType,
+			errorType:   StoredDataErrorUndefined,
+		},
+		{
+			description: "Increment stored_request_error.undefined meter",
+			dataType:    RequestDataType,
+			errorType:   StoredDataErrorUndefined,
+		},
+		{
+			description: "Increment stored_video_error.undefined meter",
+			dataType:    VideoDataType,
+			errorType:   StoredDataErrorUndefined,
+		},
+	}
+
+	for _, tt := range tests {
+		registry := metrics.NewRegistry()
+		m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus, openrtb_ext.BidderRubicon}, config.DisabledMetrics{AccountAdapterDetails: true})
+		m.RecordStoredDataError(StoredDataLabels{
+			DataType: tt.dataType,
+			Error:    tt.errorType,
+		})
+
+		actualCount := m.StoredDataErrorMeter[tt.dataType][tt.errorType].Count()
+		assert.Equal(t, int64(1), actualCount, tt.description)
+	}
+}
+
 func TestRecordRequestPrivacy(t *testing.T) {
 	registry := metrics.NewRegistry()
 	m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus, openrtb_ext.BidderRubicon}, config.DisabledMetrics{AccountAdapterDetails: true})
diff --git a/pbsmetrics/prometheus/prometheus_test.go b/pbsmetrics/prometheus/prometheus_test.go
index b6153b16..46be7005 100644
--- a/pbsmetrics/prometheus/prometheus_test.go
+++ b/pbsmetrics/prometheus/prometheus_test.go
@@ -407,6 +407,193 @@ func TestRequestTimeMetric(t *testing.T) {
 	}
 }
 
+func TestRecordStoredDataFetchTime(t *testing.T) {
+	tests := []struct {
+		description string
+		dataType    pbsmetrics.StoredDataType
+		fetchType   pbsmetrics.StoredDataFetchType
+	}{
+		{
+			description: "Update stored account histogram with all label",
+			dataType:    pbsmetrics.AccountDataType,
+			fetchType:   pbsmetrics.FetchAll,
+		},
+		{
+			description: "Update stored AMP histogram with all label",
+			dataType:    pbsmetrics.AMPDataType,
+			fetchType:   pbsmetrics.FetchAll,
+		},
+		{
+			description: "Update stored category histogram with all label",
+			dataType:    pbsmetrics.CategoryDataType,
+			fetchType:   pbsmetrics.FetchAll,
+		},
+		{
+			description: "Update stored request histogram with all label",
+			dataType:    pbsmetrics.RequestDataType,
+			fetchType:   pbsmetrics.FetchAll,
+		},
+		{
+			description: "Update stored video histogram with all label",
+			dataType:    pbsmetrics.VideoDataType,
+			fetchType:   pbsmetrics.FetchAll,
+		},
+		{
+			description: "Update stored account histogram with delta label",
+			dataType:    pbsmetrics.AccountDataType,
+			fetchType:   pbsmetrics.FetchDelta,
+		},
+		{
+			description: "Update stored AMP histogram with delta label",
+			dataType:    pbsmetrics.AMPDataType,
+			fetchType:   pbsmetrics.FetchDelta,
+		},
+		{
+			description: "Update stored category histogram with delta label",
+			dataType:    pbsmetrics.CategoryDataType,
+			fetchType:   pbsmetrics.FetchDelta,
+		},
+		{
+			description: "Update stored request histogram with delta label",
+			dataType:    pbsmetrics.RequestDataType,
+			fetchType:   pbsmetrics.FetchDelta,
+		},
+		{
+			description: "Update stored video histogram with delta label",
+			dataType:    pbsmetrics.VideoDataType,
+			fetchType:   pbsmetrics.FetchDelta,
+		},
+	}
+
+	for _, tt := range tests {
+		m := createMetricsForTesting()
+
+		fetchTime := time.Duration(0.5 * float64(time.Second))
+		m.RecordStoredDataFetchTime(pbsmetrics.StoredDataLabels{
+			DataType:      tt.dataType,
+			DataFetchType: tt.fetchType,
+		}, fetchTime)
+
+		var metricsTimer *prometheus.HistogramVec
+		switch tt.dataType {
+		case pbsmetrics.AccountDataType:
+			metricsTimer = m.storedAccountFetchTimer
+		case pbsmetrics.AMPDataType:
+			metricsTimer = m.storedAMPFetchTimer
+		case pbsmetrics.CategoryDataType:
+			metricsTimer = m.storedCategoryFetchTimer
+		case pbsmetrics.RequestDataType:
+			metricsTimer = m.storedRequestFetchTimer
+		case pbsmetrics.VideoDataType:
+			metricsTimer = m.storedVideoFetchTimer
+		}
+
+		result := getHistogramFromHistogramVec(
+			metricsTimer,
+			storedDataFetchTypeLabel,
+			string(tt.fetchType))
+		assertHistogram(t, tt.description, result, 1, 0.5)
+	}
+}
+
+func TestRecordStoredDataError(t *testing.T) {
+	tests := []struct {
+		description string
+		dataType    pbsmetrics.StoredDataType
+		errorType   pbsmetrics.StoredDataError
+		metricName  string
+	}{
+		{
+			description: "Update stored_account_errors counter with network label",
+			dataType:    pbsmetrics.AccountDataType,
+			errorType:   pbsmetrics.StoredDataErrorNetwork,
+			metricName:  "stored_account_errors",
+		},
+		{
+			description: "Update stored_amp_errors counter with network label",
+			dataType:    pbsmetrics.AMPDataType,
+			errorType:   pbsmetrics.StoredDataErrorNetwork,
+			metricName:  "stored_amp_errors",
+		},
+		{
+			description: "Update stored_category_errors counter with network label",
+			dataType:    pbsmetrics.CategoryDataType,
+			errorType:   pbsmetrics.StoredDataErrorNetwork,
+			metricName:  "stored_category_errors",
+		},
+		{
+			description: "Update stored_request_errors counter with network label",
+			dataType:    pbsmetrics.RequestDataType,
+			errorType:   pbsmetrics.StoredDataErrorNetwork,
+			metricName:  "stored_request_errors",
+		},
+		{
+			description: "Update stored_video_errors counter with network label",
+			dataType:    pbsmetrics.VideoDataType,
+			errorType:   pbsmetrics.StoredDataErrorNetwork,
+			metricName:  "stored_video_errors",
+		},
+		{
+			description: "Update stored_account_errors counter with undefined label",
+			dataType:    pbsmetrics.AccountDataType,
+			errorType:   pbsmetrics.StoredDataErrorUndefined,
+			metricName:  "stored_account_errors",
+		},
+		{
+			description: "Update stored_amp_errors counter with undefined label",
+			dataType:    pbsmetrics.AMPDataType,
+			errorType:   pbsmetrics.StoredDataErrorUndefined,
+			metricName:  "stored_amp_errors",
+		},
+		{
+			description: "Update stored_category_errors counter with undefined label",
+			dataType:    pbsmetrics.CategoryDataType,
+			errorType:   pbsmetrics.StoredDataErrorUndefined,
+			metricName:  "stored_category_errors",
+		},
+		{
+			description: "Update stored_request_errors counter with undefined label",
+			dataType:    pbsmetrics.RequestDataType,
+			errorType:   pbsmetrics.StoredDataErrorUndefined,
+			metricName:  "stored_request_errors",
+		},
+		{
+			description: "Update stored_video_errors counter with undefined label",
+			dataType:    pbsmetrics.VideoDataType,
+			errorType:   pbsmetrics.StoredDataErrorUndefined,
+			metricName:  "stored_video_errors",
+		},
+	}
+
+	for _, tt := range tests {
+		m := createMetricsForTesting()
+		m.RecordStoredDataError(pbsmetrics.StoredDataLabels{
+			DataType: tt.dataType,
+			Error:    tt.errorType,
+		})
+
+		var metricsCounter *prometheus.CounterVec
+		switch tt.dataType {
+		case pbsmetrics.AccountDataType:
+			metricsCounter = m.storedAccountErrors
+		case pbsmetrics.AMPDataType:
+			metricsCounter = m.storedAMPErrors
+		case pbsmetrics.CategoryDataType:
+			metricsCounter = m.storedCategoryErrors
+		case pbsmetrics.RequestDataType:
+			metricsCounter = m.storedRequestErrors
+		case pbsmetrics.VideoDataType:
+			metricsCounter = m.storedVideoErrors
+		}
+
+		assertCounterVecValue(t, tt.description, tt.metricName, metricsCounter,
+			1,
+			prometheus.Labels{
+				storedDataErrorLabel: string(tt.errorType),
+			})
+	}
+}
+
 func TestAdapterBidReceivedMetric(t *testing.T) {
 	adapterName := "anyName"
 	performTest := func(m *Metrics, hasAdm bool) {
@@ -1240,6 +1427,8 @@ func getHistogramFromHistogramVecByTwoKeys(histogram *prometheus.HistogramVec, l
 				valInd := ind
 				if ind == 1 {
 					valInd = 0
+				} else {
+					valInd = 1
 				}
 				if m.Label[valInd].GetName() == label2Key && m.Label[valInd].GetValue() == label2Value {
 					result = *m.GetHistogram()
diff --git a/stored_requests/config/config_test.go b/stored_requests/config/config_test.go
index 33321036..712fef32 100644
--- a/stored_requests/config/config_test.go
+++ b/stored_requests/config/config_test.go
@@ -9,27 +9,45 @@ import (
 	"regexp"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
+
 	sqlmock "github.com/DATA-DOG/go-sqlmock"
 	"github.com/julienschmidt/httprouter"
 	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/pbsmetrics"
+	"github.com/prebid/prebid-server/stored_requests"
 	"github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher"
 	"github.com/prebid/prebid-server/stored_requests/backends/http_fetcher"
 	"github.com/prebid/prebid-server/stored_requests/events"
 	httpEvents "github.com/prebid/prebid-server/stored_requests/events/http"
+	"github.com/stretchr/testify/mock"
 )
 
+func typedConfig(dataType config.DataType, sr *config.StoredRequests) *config.StoredRequests {
+	sr.SetDataType(dataType)
+	return sr
+}
+
+func isEmptyCacheType(cache stored_requests.CacheJSON) bool {
+	cache.Save(context.Background(), map[string]json.RawMessage{"foo": json.RawMessage("true")})
+	objs := cache.Get(context.Background(), []string{"foo"})
+	return len(objs) == 0
+}
+
+func isMemoryCacheType(cache stored_requests.CacheJSON) bool {
+	cache.Save(context.Background(), map[string]json.RawMessage{"foo": json.RawMessage("true")})
+	objs := cache.Get(context.Background(), []string{"foo"})
+	return len(objs) == 1
+}
+
 func TestNewEmptyFetcher(t *testing.T) {
 	fetcher := newFetcher(&config.StoredRequests{}, nil, nil)
-	ampFetcher := newFetcher(&config.StoredRequests{}, nil, nil)
-	if fetcher == nil || ampFetcher == nil {
-		t.Errorf("The fetchers should be non-nil, even with an empty config.")
+	if fetcher == nil {
+		t.Errorf("The fetcher should be non-nil, even with an empty config.")
 	}
 	if _, ok := fetcher.(empty_fetcher.EmptyFetcher); !ok {
 		t.Errorf("If the config is empty, and EmptyFetcher should be returned")
 	}
-	if _, ok := ampFetcher.(empty_fetcher.EmptyFetcher); !ok {
-		t.Errorf("If the config is empty, and EmptyFetcher should be returned for AMP")
-	}
 }
 
 func TestNewHTTPFetcher(t *testing.T) {
@@ -38,47 +56,12 @@ func TestNewHTTPFetcher(t *testing.T) {
 			Endpoint: "stored-requests.prebid.com",
 		},
 	}, nil, nil)
-	ampFetcher := newFetcher(&config.StoredRequests{
-		HTTP: config.HTTPFetcherConfig{
-			Endpoint: "stored-requests.prebid.com?type=amp",
-		},
-	}, nil, nil)
 	if httpFetcher, ok := fetcher.(*http_fetcher.HttpFetcher); ok {
 		if httpFetcher.Endpoint != "stored-requests.prebid.com?" {
 			t.Errorf("The HTTP fetcher is using the wrong endpoint. Expected %s, got %s", "stored-requests.prebid.com?", httpFetcher.Endpoint)
 		}
 	} else {
-		t.Errorf("An HTTP Fetching config should return an HTTPFetcher. Got %v", ampFetcher)
-	}
-	if httpFetcher, ok := ampFetcher.(*http_fetcher.HttpFetcher); ok {
-		if httpFetcher.Endpoint != "stored-requests.prebid.com?type=amp&" {
-			t.Errorf("The AMP HTTP fetcher is using the wrong endpoint. Expected %s, got %s", "stored-requests.prebid.com?type=amp&", httpFetcher.Endpoint)
-		}
-	} else {
-		t.Errorf("An HTTP Fetching config should return an HTTPFetcher. Got %v", ampFetcher)
-	}
-}
-
-func TestNewHTTPFetcherNoAmp(t *testing.T) {
-	fetcher := newFetcher(&config.StoredRequests{
-		HTTP: config.HTTPFetcherConfig{
-			Endpoint: "stored-requests.prebid.com",
-		},
-	}, nil, nil)
-	ampFetcher := newFetcher(&config.StoredRequests{
-		HTTP: config.HTTPFetcherConfig{
-			Endpoint: "",
-		},
-	}, nil, nil)
-	if httpFetcher, ok := fetcher.(*http_fetcher.HttpFetcher); ok {
-		if httpFetcher.Endpoint != "stored-requests.prebid.com?" {
-			t.Errorf("The HTTP fetcher is using the wrong endpoint. Expected %s, got %s", "stored-requests.prebid.com?", httpFetcher.Endpoint)
-		}
-	} else {
-		t.Errorf("An HTTP Fetching config should return an HTTPFetcher. Got %v", ampFetcher)
-	}
-	if httpAmpFetcher, ok := ampFetcher.(*http_fetcher.HttpFetcher); ok && httpAmpFetcher == nil {
-		t.Errorf("An HTTP Fetching config should not return an Amp HTTP fetcher in this case. Got %v (%v)", ampFetcher, httpAmpFetcher)
+		t.Errorf("An HTTP Fetching config should return an HTTPFetcher. Got %v", fetcher)
 	}
 }
 
@@ -95,18 +78,19 @@ func TestNewHTTPEvents(t *testing.T) {
 			Timeout:     1000,
 		},
 	}
-	evProducers := newEventProducers(cfg, server1.Client(), nil, nil)
+
+	metricsMock := &pbsmetrics.MetricsEngineMock{}
+
+	evProducers := newEventProducers(cfg, server1.Client(), nil, metricsMock, nil)
 	assertSliceLength(t, evProducers, 1)
 	assertHttpWithURL(t, evProducers[0], server1.URL)
 }
 
 func TestNewEmptyCache(t *testing.T) {
 	cache := newCache(&config.StoredRequests{InMemoryCache: config.InMemoryCache{Type: "none"}})
-	cache.Requests.Save(context.Background(), map[string]json.RawMessage{"foo": json.RawMessage("true")})
-	reqs := cache.Requests.Get(context.Background(), []string{"foo"})
-	if len(reqs) != 0 {
-		t.Errorf("The newCache method should return an empty cache if the config asks for it.")
-	}
+	assert.True(t, isEmptyCacheType(cache.Requests), "The newCache method should return an empty Request cache")
+	assert.True(t, isEmptyCacheType(cache.Imps), "The newCache method should return an empty Imp cache")
+	assert.True(t, isEmptyCacheType(cache.Accounts), "The newCache method should return an empty Account cache")
 }
 
 func TestNewInMemoryCache(t *testing.T) {
@@ -117,14 +101,28 @@ func TestNewInMemoryCache(t *testing.T) {
 			ImpCacheSize:     100,
 		},
 	})
-	cache.Requests.Save(context.Background(), map[string]json.RawMessage{"foo": json.RawMessage("true")})
-	reqs := cache.Requests.Get(context.Background(), []string{"foo"})
-	if len(reqs) != 1 {
-		t.Errorf("The newCache method should return an in-memory cache if the config asks for it.")
-	}
+	assert.True(t, isMemoryCacheType(cache.Requests), "The newCache method should return an in-memory Request cache for StoredRequests config")
+	assert.True(t, isMemoryCacheType(cache.Imps), "The newCache method should return an in-memory Imp cache for StoredRequests config")
+	assert.True(t, isEmptyCacheType(cache.Accounts), "The newCache method should return an empty Account cache for StoredRequests config")
+}
+
+func TestNewInMemoryAccountCache(t *testing.T) {
+	cache := newCache(typedConfig(config.AccountDataType, &config.StoredRequests{
+		InMemoryCache: config.InMemoryCache{
+			TTL:  60,
+			Size: 100,
+		},
+	}))
+	assert.True(t, isMemoryCacheType(cache.Accounts), "The newCache method should return an in-memory Account cache for Accounts config")
+	assert.True(t, isEmptyCacheType(cache.Requests), "The newCache method should return an empty Request cache for Accounts config")
+	assert.True(t, isEmptyCacheType(cache.Imps), "The newCache method should return an empty Imp cache for Accounts config")
 }
 
 func TestNewPostgresEventProducers(t *testing.T) {
+	metricsMock := &pbsmetrics.MetricsEngineMock{}
+	metricsMock.Mock.On("RecordStoredDataFetchTime", mock.Anything, mock.Anything).Return()
+	metricsMock.Mock.On("RecordStoredDataError", mock.Anything).Return()
+
 	cfg := &config.StoredRequests{
 		Postgres: config.PostgresConfig{
 			CacheInitialization: config.PostgresCacheInitializer{
@@ -138,34 +136,18 @@ func TestNewPostgresEventProducers(t *testing.T) {
 			},
 		},
 	}
-	ampCfg := &config.StoredRequests{
-		Postgres: config.PostgresConfig{
-			CacheInitialization: config.PostgresCacheInitializer{
-				Timeout: 50,
-				Query:   "SELECT id, requestData, type FROM stored_amp_data",
-			},
-			PollUpdates: config.PostgresUpdatePolling{
-				RefreshRate: 20,
-				Timeout:     50,
-				Query:       "SELECT id, requestData, type FROM stored_amp_data WHERE last_updated > $1",
-			},
-		},
-	}
 	client := &http.Client{}
 	db, mock, err := sqlmock.New()
 	if err != nil {
 		t.Fatalf("Failed to create mock: %v", err)
 	}
 	mock.ExpectQuery("^" + regexp.QuoteMeta(cfg.Postgres.CacheInitialization.Query) + "$").WillReturnError(errors.New("Query failed"))
-	mock.ExpectQuery("^" + regexp.QuoteMeta(ampCfg.Postgres.CacheInitialization.Query) + "$").WillReturnError(errors.New("Query failed"))
 
-	evProducers := newEventProducers(cfg, client, db, nil)
+	evProducers := newEventProducers(cfg, client, db, metricsMock, nil)
 	assertProducerLength(t, evProducers, 1)
 
-	ampEvProducers := newEventProducers(ampCfg, client, db, nil)
-	assertProducerLength(t, ampEvProducers, 1)
-
 	assertExpectationsMet(t, mock)
+	metricsMock.AssertExpectations(t)
 }
 
 func TestNewEventsAPI(t *testing.T) {
diff --git a/stored_requests/events/postgres/database_test.go b/stored_requests/events/postgres/database_test.go
index da2e0868..4471ad41 100644
--- a/stored_requests/events/postgres/database_test.go
+++ b/stored_requests/events/postgres/database_test.go
@@ -7,8 +7,11 @@ import (
 	"testing"
 	"time"
 
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/pbsmetrics"
 	"github.com/prebid/prebid-server/stored_requests/events"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 
 	sqlmock "github.com/DATA-DOG/go-sqlmock"
 )
@@ -88,13 +91,21 @@ func TestFetchAllSuccess(t *testing.T) {
 	}
 
 	for _, tt := range tests {
-		db, mock, _ := sqlmock.New()
-		mock.ExpectQuery(fakeQueryRegex()).WillReturnRows(tt.giveMockRows)
+		db, dbMock, _ := sqlmock.New()
+		dbMock.ExpectQuery(fakeQueryRegex()).WillReturnRows(tt.giveMockRows)
+
+		metricsMock := &pbsmetrics.MetricsEngineMock{}
+		metricsMock.Mock.On("RecordStoredDataFetchTime", pbsmetrics.StoredDataLabels{
+			DataType:      pbsmetrics.RequestDataType,
+			DataFetchType: pbsmetrics.FetchAll,
+		}, mock.Anything).Return()
 
 		eventProducer := NewPostgresEventProducer(PostgresEventProducerConfig{
 			DB:               db,
+			RequestType:      config.RequestDataType,
 			CacheInitTimeout: 100 * time.Millisecond,
 			CacheInitQuery:   fakeQuery,
+			MetricsEngine:    metricsMock,
 		})
 		eventProducer.time = &FakeTime{time: tt.giveFakeTime}
 		err := eventProducer.Run()
@@ -119,74 +130,76 @@ func TestFetchAllSuccess(t *testing.T) {
 		assert.Equal(t, tt.wantSavedImps, saves.Imps, tt.description)
 		assert.Equal(t, tt.wantInvalidatedReqs, invalidations.Requests, tt.description)
 		assert.Equal(t, tt.wantInvalidatedImps, invalidations.Imps, tt.description)
+
+		metricsMock.AssertExpectations(t)
 	}
 }
 
 func TestFetchAllErrors(t *testing.T) {
 	tests := []struct {
-		description         string
-		giveFakeTime        time.Time
-		giveMockRows        *sqlmock.Rows
-		wantReturnedError   bool
-		wantLastUpdate      time.Time
-		wantSavedReqs       map[string]json.RawMessage
-		wantSavedImps       map[string]json.RawMessage
-		wantInvalidatedReqs []string
-		wantInvalidatedImps []string
+		description       string
+		giveFakeTime      time.Time
+		giveTimeoutMS     int
+		giveMockRows      *sqlmock.Rows
+		wantRecordedError pbsmetrics.StoredDataError
+		wantLastUpdate    time.Time
 	}{
+		{
+			description:       "fetch all timeout",
+			giveFakeTime:      time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC),
+			giveMockRows:      nil,
+			wantRecordedError: pbsmetrics.StoredDataErrorNetwork,
+			wantLastUpdate:    time.Time{},
+		},
 		{
 			description:       "fetch all query error",
 			giveFakeTime:      time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC),
+			giveTimeoutMS:     100,
 			giveMockRows:      nil,
-			wantReturnedError: true,
+			wantRecordedError: pbsmetrics.StoredDataErrorUndefined,
 			wantLastUpdate:    time.Time{},
 		},
 		{
-			description:  "fetch all row error",
-			giveFakeTime: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC),
+			description:   "fetch all row error",
+			giveFakeTime:  time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC),
+			giveTimeoutMS: 100,
 			giveMockRows: sqlmock.NewRows([]string{"id", "data", "dataType"}).
 				AddRow("stored-req-id", "true", "request").
 				RowError(0, errors.New("Some row error.")),
-			wantReturnedError: true,
+			wantRecordedError: pbsmetrics.StoredDataErrorUndefined,
 			wantLastUpdate:    time.Time{},
 		},
-		{
-			description:  "fetch all close error",
-			giveFakeTime: time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC),
-			giveMockRows: sqlmock.NewRows([]string{"id", "data", "dataType"}).
-				AddRow("req-1", "true", "request").
-				AddRow("imp-1", "true", "imp").
-				AddRow("req-2", "", "request").
-				AddRow("imp-2", "", "imp").
-				CloseError(errors.New("Some close error.")),
-			wantReturnedError: false,
-			wantLastUpdate:    time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC),
-			wantSavedReqs:     map[string]json.RawMessage{"req-1": json.RawMessage(`true`)},
-			wantSavedImps:     map[string]json.RawMessage{"imp-1": json.RawMessage(`true`)},
-		},
 	}
 
 	for _, tt := range tests {
-		db, mock, _ := sqlmock.New()
+		db, dbMock, _ := sqlmock.New()
 		if tt.giveMockRows == nil {
-			mock.ExpectQuery(fakeQueryRegex()).WillReturnError(errors.New("Query failed."))
+			dbMock.ExpectQuery(fakeQueryRegex()).WillReturnError(errors.New("Query failed."))
 		} else {
-			mock.ExpectQuery(fakeQueryRegex()).WillReturnRows(tt.giveMockRows)
+			dbMock.ExpectQuery(fakeQueryRegex()).WillReturnRows(tt.giveMockRows)
 		}
 
+		metricsMock := &pbsmetrics.MetricsEngineMock{}
+		metricsMock.Mock.On("RecordStoredDataFetchTime", pbsmetrics.StoredDataLabels{
+			DataType:      pbsmetrics.RequestDataType,
+			DataFetchType: pbsmetrics.FetchAll,
+		}, mock.Anything).Return()
+		metricsMock.Mock.On("RecordStoredDataError", pbsmetrics.StoredDataLabels{
+			DataType: pbsmetrics.RequestDataType,
+			Error:    tt.wantRecordedError,
+		}).Return()
+
 		eventProducer := NewPostgresEventProducer(PostgresEventProducerConfig{
 			DB:               db,
-			CacheInitTimeout: 100 * time.Millisecond,
+			RequestType:      config.RequestDataType,
+			CacheInitTimeout: time.Duration(tt.giveTimeoutMS) * time.Millisecond,
 			CacheInitQuery:   fakeQuery,
+			MetricsEngine:    metricsMock,
 		})
 		eventProducer.time = &FakeTime{time: tt.giveFakeTime}
 		err := eventProducer.Run()
 
-		if tt.wantReturnedError {
-			assert.NotNil(t, err, tt.description)
-		} else {
-			assert.Nil(t, err, tt.description)
-		}
+		assert.NotNil(t, err, tt.description)
 		assert.Equal(t, tt.wantLastUpdate, eventProducer.lastUpdate, tt.description)
 
 		var saves events.Save
@@ -202,10 +215,12 @@ func TestFetchAllErrors(t *testing.T) {
 		case <-time.After(10 * time.Millisecond):
 		}
 
-		assert.Equal(t, tt.wantSavedReqs, saves.Requests, tt.description)
-		assert.Equal(t, tt.wantSavedImps, saves.Imps, tt.description)
-		assert.Equal(t, tt.wantInvalidatedReqs, invalidations.Requests, tt.description)
-		assert.Equal(t, tt.wantInvalidatedImps, invalidations.Imps, tt.description)
+		assert.Nil(t, saves.Requests, tt.description)
+		assert.Nil(t, saves.Imps, tt.description)
+		assert.Nil(t, invalidations.Requests, tt.description)
+		assert.Nil(t, invalidations.Requests, tt.description)
+
+		metricsMock.AssertExpectations(t)
 	}
 }
 
@@ -289,13 +304,21 @@ func TestFetchDeltaSuccess(t *testing.T) {
 	}
 
 	for _, tt := range tests {
-		db, mock, _ := sqlmock.New()
-		mock.ExpectQuery(fakeQueryRegex()).WillReturnRows(tt.giveMockRows)
+		db, dbMock, _ := sqlmock.New()
+		dbMock.ExpectQuery(fakeQueryRegex()).WillReturnRows(tt.giveMockRows)
+
+		metricsMock := &pbsmetrics.MetricsEngineMock{}
+		metricsMock.Mock.On("RecordStoredDataFetchTime", pbsmetrics.StoredDataLabels{
+			DataType:      pbsmetrics.RequestDataType,
+			DataFetchType: pbsmetrics.FetchDelta,
+		}, mock.Anything).Return()
 
 		eventProducer := NewPostgresEventProducer(PostgresEventProducerConfig{
 			DB:                 db,
+			RequestType:        config.RequestDataType,
 			CacheUpdateTimeout: 100 * time.Millisecond,
 			CacheUpdateQuery:   fakeQuery,
+			MetricsEngine:      metricsMock,
 		})
 		eventProducer.lastUpdate = time.Date(2020, time.June, 30, 6, 0, 0, 0, time.UTC)
 		eventProducer.time = &FakeTime{time: tt.giveFakeTime}
@@ -321,81 +344,81 @@ func TestFetchDeltaSuccess(t *testing.T) {
 		assert.Equal(t, tt.wantSavedImps, saves.Imps, tt.description)
 		assert.Equal(t, tt.wantInvalidatedReqs, invalidations.Requests, tt.description)
 		assert.Equal(t, tt.wantInvalidatedImps, invalidations.Imps, tt.description)
+
+		metricsMock.AssertExpectations(t)
 	}
 }
 
 func TestFetchDeltaErrors(t *testing.T) {
 	tests := []struct {
-		description         string
-		giveFakeTime        time.Time
-		giveLastUpdate      time.Time
-		giveMockRows        *sqlmock.Rows
-		wantReturnedError   bool
-		wantLastUpdate      time.Time
-		wantSavedReqs       map[string]json.RawMessage
-		wantSavedImps       map[string]json.RawMessage
-		wantInvalidatedReqs []string
-		wantInvalidatedImps []string
+		description       string
+		giveFakeTime      time.Time
+		giveTimeoutMS     int
+		giveLastUpdate    time.Time
+		giveMockRows      *sqlmock.Rows
+		wantRecordedError pbsmetrics.StoredDataError
+		wantLastUpdate    time.Time
 	}{
+		{
+			description:       "fetch delta timeout",
+			giveFakeTime:      time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC),
+			giveLastUpdate:    time.Date(2020, time.June, 30, 6, 0, 0, 0, time.UTC),
+			giveMockRows:      nil,
+			wantRecordedError: pbsmetrics.StoredDataErrorNetwork,
+			wantLastUpdate:    time.Date(2020, time.June, 30, 6, 0, 0, 0, time.UTC),
+		},
 		{
 			description:       "fetch delta query error",
 			giveFakeTime:      time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC),
+			giveTimeoutMS:     100,
 			giveLastUpdate:    time.Date(2020, time.June, 30, 6, 0, 0, 0, time.UTC),
 			giveMockRows:      nil,
-			wantReturnedError: true,
+			wantRecordedError: pbsmetrics.StoredDataErrorUndefined,
 			wantLastUpdate:    time.Date(2020, time.June, 30, 6, 0, 0, 0, time.UTC),
 		},
 		{
-			description:    "fetch all row error",
+			description:    "fetch delta row error",
 			giveFakeTime:   time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC),
+			giveTimeoutMS:  100,
 			giveLastUpdate: time.Date(2020, time.June, 30, 6, 0, 0, 0, time.UTC),
 			giveMockRows: sqlmock.NewRows([]string{"id", "data", "dataType"}).
 				AddRow("stored-req-id", "true", "request").
 				RowError(0, errors.New("Some row error.")),
-			wantReturnedError: true,
+			wantRecordedError: pbsmetrics.StoredDataErrorUndefined,
 			wantLastUpdate:    time.Date(2020, time.June, 30, 6, 0, 0, 0, time.UTC),
 		},
-		{
-			description:    "fetch all close error",
-			giveFakeTime:   time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC),
-			giveLastUpdate: time.Date(2020, time.June, 30, 6, 0, 0, 0, time.UTC),
-			giveMockRows: sqlmock.NewRows([]string{"id", "data", "dataType"}).
-				AddRow("req-1", "true", "request").
-				AddRow("imp-1", "true", "imp").
-				AddRow("req-2", "", "request").
-				AddRow("imp-2", "", "imp").
-				CloseError(errors.New("Some close error.")),
-			wantReturnedError:   false,
-			wantLastUpdate:      time.Date(2020, time.July, 1, 12, 30, 0, 0, time.UTC),
-			wantSavedReqs:       map[string]json.RawMessage{"req-1": json.RawMessage(`true`)},
-			wantSavedImps:       map[string]json.RawMessage{"imp-1": json.RawMessage(`true`)},
-			wantInvalidatedReqs: []string{"req-2"},
-			wantInvalidatedImps: []string{"imp-2"},
-		},
 	}
 
 	for _, tt := range tests {
-		db, mock, _ := sqlmock.New()
+		db, dbMock, _ := sqlmock.New()
 		if tt.giveMockRows == nil {
-			mock.ExpectQuery(fakeQueryRegex()).WillReturnError(errors.New("Query failed."))
+			dbMock.ExpectQuery(fakeQueryRegex()).WillReturnError(errors.New("Query failed."))
 		} else {
-			mock.ExpectQuery(fakeQueryRegex()).WillReturnRows(tt.giveMockRows)
+			dbMock.ExpectQuery(fakeQueryRegex()).WillReturnRows(tt.giveMockRows)
 		}
 
+		metricsMock := &pbsmetrics.MetricsEngineMock{}
+		metricsMock.Mock.On("RecordStoredDataFetchTime", pbsmetrics.StoredDataLabels{
+			DataType:      pbsmetrics.RequestDataType,
+			DataFetchType: pbsmetrics.FetchDelta,
+		}, mock.Anything).Return()
+		metricsMock.Mock.On("RecordStoredDataError", pbsmetrics.StoredDataLabels{
+			DataType: pbsmetrics.RequestDataType,
+			Error:    tt.wantRecordedError,
+		}).Return()
+
 		eventProducer := NewPostgresEventProducer(PostgresEventProducerConfig{
 			DB:                 db,
-			CacheUpdateTimeout: 100 * time.Millisecond,
+			RequestType:        config.RequestDataType,
+			CacheUpdateTimeout: time.Duration(tt.giveTimeoutMS) * time.Millisecond,
 			CacheUpdateQuery:   fakeQuery,
+			MetricsEngine:      metricsMock,
 		})
 		eventProducer.lastUpdate = tt.giveLastUpdate
 		eventProducer.time = &FakeTime{time: tt.giveFakeTime}
 		err := eventProducer.Run()
 
-		if tt.wantReturnedError {
-			assert.NotNil(t, err, tt.description)
-		} else {
-			assert.Nil(t, err, tt.description)
-		}
+		assert.NotNil(t, err, tt.description)
 		assert.Equal(t, tt.wantLastUpdate, eventProducer.lastUpdate, tt.description)
 
 		var saves events.Save
@@ -411,9 +434,11 @@ func TestFetchDeltaErrors(t *testing.T) {
 		case <-time.After(10 * time.Millisecond):
 		}
 
-		assert.Equal(t, tt.wantSavedReqs, saves.Requests, tt.description)
-		assert.Equal(t, tt.wantSavedImps, saves.Imps, tt.description)
-		assert.Equal(t, tt.wantInvalidatedReqs, invalidations.Requests, tt.description)
-		assert.Equal(t, tt.wantInvalidatedImps, invalidations.Imps, tt.description)
+		assert.Nil(t, saves.Requests, tt.description)
+		assert.Nil(t, saves.Imps, tt.description)
+		assert.Nil(t, invalidations.Requests, tt.description)
+		assert.Nil(t, invalidations.Requests, tt.description)
+
+		metricsMock.AssertExpectations(t)
 	}
 }
diff --git a/stored_requests/fetcher_test.go b/stored_requests/fetcher_test.go
index 396ba3d0..7a6a06fb 100644
--- a/stored_requests/fetcher_test.go
+++ b/stored_requests/fetcher_test.go
@@ -7,6 +7,7 @@ import (
 	"testing"
 
 	"github.com/prebid/prebid-server/pbsmetrics"
+	"github.com/prebid/prebid-server/stored_requests/caches/nil_cache"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
@@ -17,7 +18,7 @@ func setupFetcherWithCacheDeps() (*mockCache, *mockCache, *mockFetcher, AllFetch
 	impCache := &mockCache{}
 	metricsEngine := &pbsmetrics.MetricsEngineMock{}
 	fetcher := &mockFetcher{}
-	afetcherWithCache := WithCache(fetcher, Cache{reqCache, impCache}, metricsEngine)
+	afetcherWithCache := WithCache(fetcher, Cache{reqCache, impCache, &nil_cache.NilCache{}}, metricsEngine)
 
 	return reqCache, impCache, fetcher, afetcherWithCache, metricsEngine
 }
@@ -158,6 +159,57 @@ func TestCacheSaves(t *testing.T) {
 	assert.Len(t, errs, 0, "FetchRequests with duplicate IDs shouldn't return an error")
 }
 
+func setupAccountFetcherWithCacheDeps() (*mockCache, *mockFetcher, AllFetcher, *pbsmetrics.MetricsEngineMock) {
+	accCache := &mockCache{}
+	metricsEngine := &pbsmetrics.MetricsEngineMock{}
+	fetcher := &mockFetcher{}
+	afetcherWithCache := WithCache(fetcher, Cache{&nil_cache.NilCache{}, &nil_cache.NilCache{}, accCache}, metricsEngine)
+
+	return accCache, fetcher, afetcherWithCache, metricsEngine
+}
+
+func TestAccountCacheHit(t *testing.T) {
+	accCache, fetcher, aFetcherWithCache, metricsEngine := setupAccountFetcherWithCacheDeps()
+	cachedAccounts := []string{"known"}
+	ctx := context.Background()
+
+	// Test read from cache
+	accCache.On("Get", ctx, cachedAccounts).Return(
+		map[string]json.RawMessage{
+			"known": json.RawMessage(`true`),
+		})
+
+	account, errs := aFetcherWithCache.FetchAccount(ctx, "known")
+
+	accCache.AssertExpectations(t)
+	fetcher.AssertExpectations(t)
+	metricsEngine.AssertExpectations(t)
+	assert.JSONEq(t, `true`, string(account), "FetchAccount should fetch the right account data")
+	assert.Len(t, errs, 0, "FetchAccount shouldn't return any errors")
+}
+
+func TestAccountCacheMiss(t *testing.T) {
+	accCache, fetcher, aFetcherWithCache, metricsEngine := setupAccountFetcherWithCacheDeps()
+	uncachedAccounts := []string{"uncached"}
+	uncachedAccountsData := map[string]json.RawMessage{
+		"uncached": json.RawMessage(`true`),
+	}
+	ctx := context.Background()
+
+	// Test read from cache
+	accCache.On("Get", ctx, uncachedAccounts).Return(map[string]json.RawMessage{})
+	accCache.On("Save", ctx, uncachedAccountsData)
+	fetcher.On("FetchAccount", ctx, "uncached").Return(uncachedAccountsData["uncached"], []error{})
+
+	account, errs := aFetcherWithCache.FetchAccount(ctx, "uncached")
+
+	accCache.AssertExpectations(t)
+	fetcher.AssertExpectations(t)
+	metricsEngine.AssertExpectations(t)
+	assert.JSONEq(t, `true`, string(account), "FetchAccount should fetch the right account data")
+	assert.Len(t, errs, 0, "FetchAccount shouldn't return any errors")
+}
+
 func TestComposedCache(t *testing.T) {
 	c1 := &mockCache{}
 	c2 := &mockCache{}