Repository: prebid/prebid-server
Pull request #1496 (2020-09-14 21:13:16)

Add bidder name key support
Original issue and more info for this change: https://github.com/prebid/prebid-server/issues/1458

SyntaxNode: The build failure is due to an unexpected change to our validation process. For now, please merge from the master branch to fix the issue.
camrice: Sorry for the late update. I added an Video endpoint test and Exchange test for this change. I also fixed the unit test failure. If I should move my commits over to the Upstream repo and open a new PR let me know.

Patch:
diff --git a/adapters/adgeneration/adgeneration.go b/adapters/adgeneration/adgeneration.go
index 054fa7f6..08a7be08 100644
--- a/adapters/adgeneration/adgeneration.go
+++ b/adapters/adgeneration/adgeneration.go
@@ -54,6 +54,9 @@ func (adg *AdgenerationAdapter) MakeRequests(request *openrtb.BidRequest, reqInf
 	headers := http.Header{}
 	headers.Add("Content-Type", "application/json;charset=utf-8")
 	headers.Add("Accept", "application/json")
+	if request.Device != nil && len(request.Device.UA) > 0 {
+		headers.Add("User-Agent", request.Device.UA)
+	}
 
 	bidRequestArray := make([]*adapters.RequestData, 0, numRequests)
 
@@ -106,11 +109,14 @@ func (adg *AdgenerationAdapter) getRawQuery(id string, request *openrtb.BidReque
 	v.Set("adapterver", adg.version)
 	adSize := getSizes(imp)
 	if adSize != "" {
-		v.Set("size", adSize)
+		v.Set("sizes", adSize)
 	}
 	if request.Site != nil && request.Site.Page != "" {
 		v.Set("tp", request.Site.Page)
 	}
+	if request.Source != nil && request.Source.TID != "" {
+		v.Set("transactionid", request.Source.TID)
+	}
 	return &v
 }
 
@@ -135,7 +141,7 @@ func getSizes(imp *openrtb.Imp) string {
 	}
 	var sizeStr string
 	for _, v := range imp.Banner.Format {
-		sizeStr += strconv.FormatUint(v.W, 10) + "×" + strconv.FormatUint(v.H, 10) + ","
+		sizeStr += strconv.FormatUint(v.W, 10) + "x" + strconv.FormatUint(v.H, 10) + ","
 	}
 	if len(sizeStr) > 0 && strings.LastIndex(sizeStr, ",") == len(sizeStr)-1 {
 		sizeStr = sizeStr[:len(sizeStr)-1]
@@ -255,7 +261,7 @@ func removeWrapper(ad string) string {
 func NewAdgenerationAdapter(endpoint string) *AdgenerationAdapter {
 	return &AdgenerationAdapter{
 		endpoint,
-		"1.0.1",
+		"1.0.2",
 		"JPY",
 	}
 }
diff --git a/adapters/rubicon/rubicon.go b/adapters/rubicon/rubicon.go
index 812095f9..b9272489 100644
--- a/adapters/rubicon/rubicon.go
+++ b/adapters/rubicon/rubicon.go
@@ -275,6 +275,7 @@ var rubiSizeMap = map[rubiSize]int{
 	{w: 320, h: 500}:   278,
 	{w: 320, h: 400}:   282,
 	{w: 640, h: 380}:   288,
+	{w: 500, h: 1000}:  548,
 }
 
 // defines the contract for bidrequest.user.ext.eids[i].ext
diff --git a/config/config.go b/config/config.go
index 7825a5f3..218ffca3 100755
--- a/config/config.go
+++ b/config/config.go
@@ -746,7 +746,7 @@ func (cfg *Configuration) setDerivedDefaults() {
 	// openrtb_ext.BidderRTBHouse doesn't have a good default.
 	// openrtb_ext.BidderRubicon doesn't have a good default.
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSharethrough, "https://match.sharethrough.com/FGMrCMMc/v1?redirectUri="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dsharethrough%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID")
-	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSmartadserver, "https://ssbsync.smartadserver.com/api/sync?callerId=5&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirectUri="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dsmartadserver%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5Bssb_sync_pid%5D")
+	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSmartadserver, "https://ssbsync-global.smartadserver.com/api/sync?callerId=5&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirectUri="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dsmartadserver%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5Bssb_sync_pid%5D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSmartRTB, "https://market-global.smrtb.com/sync/all?nid=smartrtb&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&rr="+url.QueryEscape(externalURL)+"%252Fsetuid%253Fbidder%253Dsmartrtb%2526gdpr%253D{{.GDPR}}%2526gdpr_consent%253D{{.GDPRConsent}}%2526uid%253D%257BXID%257D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSmartyAds, "https://as.ck-ie.com/prebid.gif?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dsmartyads%26uid%3D%5BUID%5D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSomoaudience, "https://publisher-east.mobileadtrading.com/usersync?ru="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dsomoaudience%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUID%7D")
@@ -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")
@@ -983,7 +983,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.rubicon.endpoint", "http://exapi-us-east.rubiconproject.com/a/api/exchange.json")
 	v.SetDefault("adapters.sharethrough.endpoint", "http://btlr.sharethrough.com/FGMrCMMc/v1")
 	v.SetDefault("adapters.smaato.endpoint", "https://prebid.ad.smaato.net/oapi/prebid")
-	v.SetDefault("adapters.smartadserver.endpoint", "https://ssb.smartadserver.com")
+	v.SetDefault("adapters.smartadserver.endpoint", "https://ssb-global.smartadserver.com")
 	v.SetDefault("adapters.smartrtb.endpoint", "http://market-east.smrtb.com/json/publisher/rtb?pubid={{.PublisherID}}")
 	v.SetDefault("adapters.smartyads.endpoint", "http://{{.Host}}.smartyads.com/bid?rtb_seat_id={{.SourceId}}&secret_key={{.AccountID}}")
 	v.SetDefault("adapters.somoaudience.endpoint", "http://publisher-east.mobileadtrading.com/rtb/bid")
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/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/exchange.go b/exchange/exchange.go
index 62fb9a2c..64b0043c 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -535,6 +535,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
@@ -630,6 +631,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/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..08df7c59 100644
--- a/openrtb_ext/request.go
+++ b/openrtb_ext/request.go
@@ -101,6 +101,7 @@ type ExtRequestTargeting struct {
 	IncludeBrandCategory *ExtIncludeBrandCategory `json:"includebrandcategory"`
 	IncludeFormat        bool                     `json:"includeformat"`
 	DurationRangeSec     []int                    `json:"durationrangesec"`
+	AppendBidderNames    bool                     `json:"appendbiddernames,omitempty"`
 }
 
 type ExtIncludeBrandCategory struct {
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..d32773a7 100644
--- a/stored_requests/config/config.go
+++ b/stored_requests/config/config.go
@@ -177,15 +177,17 @@ 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) {
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/adgeneration/adgeneration_test.go b/adapters/adgeneration/adgeneration_test.go
index 3c795ea2..130f6d15 100644
--- a/adapters/adgeneration/adgeneration_test.go
+++ b/adapters/adgeneration/adgeneration_test.go
@@ -14,7 +14,7 @@ func TestJsonSamples(t *testing.T) {
 	adapterstest.RunJSONBidderTest(t, "adgenerationtest", NewAdgenerationAdapter("https://d.socdm.com/adsv/v1"))
 }
 
-func TestgetRequestUri(t *testing.T) {
+func TestGetRequestUri(t *testing.T) {
 	bidder := NewAdgenerationAdapter("https://d.socdm.com/adsv/v1")
 	// Test items
 	failedRequest := &openrtb.BidRequest{
@@ -24,6 +24,7 @@ func TestgetRequestUri(t *testing.T) {
 			{ID: "extImpBidder-failed-test", Banner: &openrtb.Banner{Format: []openrtb.Format{{W: 300, H: 250}}}, Ext: json.RawMessage(`{"_bidder": { "id": "58278" }}`)},
 			{ID: "extImpAdgeneration-failed-test", Banner: &openrtb.Banner{Format: []openrtb.Format{{W: 300, H: 250}}}, Ext: json.RawMessage(`{"bidder": { "_id": "58278" }}`)},
 		},
+		Source: &openrtb.Source{TID: "SourceTID"},
 		Device: &openrtb.Device{UA: "testUA", IP: "testIP"},
 		Site:   &openrtb.Site{Page: "https://supership.com"},
 		User:   &openrtb.User{BuyerUID: "buyerID"},
@@ -33,6 +34,7 @@ func TestgetRequestUri(t *testing.T) {
 		Imp: []openrtb.Imp{
 			{ID: "bidRequest-success-test", Banner: &openrtb.Banner{Format: []openrtb.Format{{W: 300, H: 250}}}, Ext: json.RawMessage(`{"bidder": { "id": "58278" }}`)},
 		},
+		Source: &openrtb.Source{TID: "SourceTID"},
 		Device: &openrtb.Device{UA: "testUA", IP: "testIP"},
 		Site:   &openrtb.Site{Page: "https://supership.com"},
 		User:   &openrtb.User{BuyerUID: "buyerID"},
@@ -50,14 +52,6 @@ func TestgetRequestUri(t *testing.T) {
 	}
 	numRequests = len(successRequest.Imp)
 	for index := 0; index < numRequests; index++ {
-		// RequestUri Test.
-		httpRequests, err := bidder.getRequestUri(successRequest, index)
-		if err != nil {
-			t.Errorf("getRequestUri: %v did throw an error: %v", successRequest.Imp[index], err)
-		}
-		if httpRequests == "adapterver="+bidder.version+"&currency=JPY&hb=true&id=58278&posall=SSPLOC&sdkname=prebidserver&sdktype=0&size=300%C3%97250&t=json3&tp=http%3A%2F%2Fexample.com%2Ftest.html" {
-			t.Errorf("getRequestUri: %v did return Request: %s", successRequest.Imp[index], httpRequests)
-		}
 		// getRawQuery Test.
 		adgExt, err := unmarshalExtImpAdgeneration(&successRequest.Imp[index])
 		if err != nil {
@@ -65,27 +59,33 @@ func TestgetRequestUri(t *testing.T) {
 		}
 		rawQuery := bidder.getRawQuery(adgExt.Id, successRequest, &successRequest.Imp[index])
 		expectQueries := map[string]string{
-			"posall":     "SSPLOC",
-			"id":         adgExt.Id,
-			"sdktype":    "0",
-			"hb":         "true",
-			"currency":   bidder.getCurrency(successRequest),
-			"sdkname":    "prebidserver",
-			"adapterver": bidder.version,
-			"size":       getSizes(&successRequest.Imp[index]),
-			"tp":         successRequest.Site.Name,
+			"posall":        "SSPLOC",
+			"id":            adgExt.Id,
+			"sdktype":       "0",
+			"hb":            "true",
+			"currency":      bidder.getCurrency(successRequest),
+			"sdkname":       "prebidserver",
+			"adapterver":    bidder.version,
+			"sizes":         getSizes(&successRequest.Imp[index]),
+			"tp":            successRequest.Site.Page,
+			"transactionid": successRequest.Source.TID,
 		}
 		for key, expectedValue := range expectQueries {
 			actualValue := rawQuery.Get(key)
-			if actualValue == "" {
-				if !(key == "size" || key == "tp") {
-					t.Errorf("getRawQuery: key %s is required value.", key)
-				}
-			}
 			if actualValue != expectedValue {
 				t.Errorf("getRawQuery: %s value does not match expected %s, actual %s", key, expectedValue, actualValue)
 			}
 		}
+
+		// RequestUri Test.
+		actualUri, err := bidder.getRequestUri(successRequest, index)
+		if err != nil {
+			t.Errorf("getRequestUri: %v did throw an error: %v", successRequest.Imp[index], err)
+		}
+		expectedUri := "https://d.socdm.com/adsv/v1?adapterver=" + bidder.version + "&currency=JPY&hb=true&id=58278&posall=SSPLOC&sdkname=prebidserver&sdktype=0&sizes=300x250&t=json3&tp=https%3A%2F%2Fsupership.com&transactionid=SourceTID"
+		if actualUri != expectedUri {
+			t.Errorf("getRequestUri: does not match expected %s, actual %s", expectedUri, actualUri)
+		}
 	}
 }
 
@@ -99,7 +99,7 @@ func TestGetSizes(t *testing.T) {
 
 	request = &openrtb.Imp{Banner: multiFormatBanner}
 	size = getSizes(request)
-	if size != "300×250,320×50" {
+	if size != "300x250,320x50" {
 		t.Errorf("%v does not match size.", multiFormatBanner)
 	}
 	request = &openrtb.Imp{Banner: noFormatBanner}
diff --git a/adapters/adgeneration/adgenerationtest/exemplary/single-banner.json b/adapters/adgeneration/adgenerationtest/exemplary/single-banner.json
index 10bf1c4a..655f6b75 100644
--- a/adapters/adgeneration/adgenerationtest/exemplary/single-banner.json
+++ b/adapters/adgeneration/adgenerationtest/exemplary/single-banner.json
@@ -4,6 +4,9 @@
         "site": {
             "page": "http://example.com/test.html"
         },
+        "device": {
+            "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36"
+        },
         "imp": [
             {
                 "id": "some-impression-id",
@@ -52,13 +55,16 @@
                 "tmax": 500
             },
             "expectedRequest":{
-                "uri": "https://d.socdm.com/adsv/v1?adapterver=1.0.1&currency=JPY&hb=true&id=58278&posall=SSPLOC&sdkname=prebidserver&sdktype=0&size=300%C3%97250&t=json3&tp=http%3A%2F%2Fexample.com%2Ftest.html",
+                "uri": "https://d.socdm.com/adsv/v1?adapterver=1.0.2&currency=JPY&hb=true&id=58278&posall=SSPLOC&sdkname=prebidserver&sdktype=0&sizes=300x250&t=json3&tp=http%3A%2F%2Fexample.com%2Ftest.html",
                 "headers": {
                     "Accept": [
                         "application/json"
                     ],
                     "Content-Type": [
                         "application/json;charset=utf-8"
+                    ],
+                    "User-Agent": [
+                        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36"
                     ]
                 }
             },
diff --git a/adapters/adgeneration/adgenerationtest/supplemental/204-bid-response.json b/adapters/adgeneration/adgenerationtest/supplemental/204-bid-response.json
index bc469a1e..b9a79688 100644
--- a/adapters/adgeneration/adgenerationtest/supplemental/204-bid-response.json
+++ b/adapters/adgeneration/adgenerationtest/supplemental/204-bid-response.json
@@ -4,6 +4,9 @@
         "site": {
             "page": "http://example.com/test.html"
         },
+        "device": {
+            "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36"
+        },
         "imp": [
             {
                 "id": "some-impression-id",
@@ -52,13 +55,16 @@
                 "tmax": 500
             },
             "expectedRequest":{
-                "uri": "https://d.socdm.com/adsv/v1?adapterver=1.0.1&currency=JPY&hb=true&id=58278&posall=SSPLOC&sdkname=prebidserver&sdktype=0&size=300%C3%97250&t=json3&tp=http%3A%2F%2Fexample.com%2Ftest.html",
+                "uri": "https://d.socdm.com/adsv/v1?adapterver=1.0.2&currency=JPY&hb=true&id=58278&posall=SSPLOC&sdkname=prebidserver&sdktype=0&sizes=300x250&t=json3&tp=http%3A%2F%2Fexample.com%2Ftest.html",
                 "headers": {
                     "Accept": [
                         "application/json"
                     ],
                     "Content-Type": [
                         "application/json;charset=utf-8"
+                    ],
+                    "User-Agent": [
+                        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36"
                     ]
                 }
             },
diff --git a/adapters/adgeneration/adgenerationtest/supplemental/400-bid-response.json b/adapters/adgeneration/adgenerationtest/supplemental/400-bid-response.json
index 6ac92d9a..9744e8d2 100644
--- a/adapters/adgeneration/adgenerationtest/supplemental/400-bid-response.json
+++ b/adapters/adgeneration/adgenerationtest/supplemental/400-bid-response.json
@@ -4,6 +4,9 @@
         "site": {
             "page": "http://example.com/test.html"
         },
+        "device": {
+            "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36"
+        },
         "imp": [
             {
                 "id": "some-impression-id",
@@ -52,13 +55,16 @@
                 "tmax": 500
             },
             "expectedRequest":{
-                "uri": "https://d.socdm.com/adsv/v1?adapterver=1.0.1&currency=JPY&hb=true&id=58278&posall=SSPLOC&sdkname=prebidserver&sdktype=0&size=300%C3%97250&t=json3&tp=http%3A%2F%2Fexample.com%2Ftest.html",
+                "uri": "https://d.socdm.com/adsv/v1?adapterver=1.0.2&currency=JPY&hb=true&id=58278&posall=SSPLOC&sdkname=prebidserver&sdktype=0&sizes=300x250&t=json3&tp=http%3A%2F%2Fexample.com%2Ftest.html",
                 "headers": {
                     "Accept": [
                         "application/json"
                     ],
                     "Content-Type": [
                         "application/json;charset=utf-8"
+                    ],
+                    "User-Agent": [
+                        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36"
                     ]
                 }
             },
diff --git a/adapters/adgeneration/adgenerationtest/supplemental/no-bid-response.json b/adapters/adgeneration/adgenerationtest/supplemental/no-bid-response.json
index a0abb66d..3b3bc82a 100644
--- a/adapters/adgeneration/adgenerationtest/supplemental/no-bid-response.json
+++ b/adapters/adgeneration/adgenerationtest/supplemental/no-bid-response.json
@@ -4,6 +4,9 @@
         "site": {
             "page": "http://example.com/test.html"
         },
+        "device": {
+            "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36"
+        },
         "imp": [
             {
                 "id": "some-impression-id",
@@ -52,13 +55,16 @@
                 "tmax": 500
             },
             "expectedRequest":{
-                "uri": "https://d.socdm.com/adsv/v1?adapterver=1.0.1&currency=JPY&hb=true&id=58278&posall=SSPLOC&sdkname=prebidserver&sdktype=0&size=300%C3%97250&t=json3&tp=http%3A%2F%2Fexample.com%2Ftest.html",
+                "uri": "https://d.socdm.com/adsv/v1?adapterver=1.0.2&currency=JPY&hb=true&id=58278&posall=SSPLOC&sdkname=prebidserver&sdktype=0&sizes=300x250&t=json3&tp=http%3A%2F%2Fexample.com%2Ftest.html",
                 "headers": {
                     "Accept": [
                         "application/json"
                     ],
                     "Content-Type": [
                         "application/json;charset=utf-8"
+                    ],
+                    "User-Agent": [
+                        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36"
                     ]
                 }
             },
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/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/exchange_test.go b/exchange/exchange_test.go
index 2b91f1b0..99c351c4 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -1888,6 +1888,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/stored_requests/config/config_test.go b/stored_requests/config/config_test.go
index 33321036..f225f74b 100644
--- a/stored_requests/config/config_test.go
+++ b/stored_requests/config/config_test.go
@@ -9,27 +9,43 @@ 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/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"
 )
 
+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 +54,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)
 	}
 }
 
@@ -102,11 +83,9 @@ func TestNewHTTPEvents(t *testing.T) {
 
 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,11 +96,21 @@ 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) {
@@ -138,33 +127,16 @@ 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)
 	assertProducerLength(t, evProducers, 1)
 
-	ampEvProducers := newEventProducers(ampCfg, client, db, nil)
-	assertProducerLength(t, ampEvProducers, 1)
-
 	assertExpectationsMet(t, mock)
 }
 
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{}