Repository: prebid/prebid-server
Pull request #1483 (2020-09-08 21:57:29)

Refactor: move getAccount to accounts package (from openrtb2)
Move GetAccount() to a new accounts package, so it can be invoked from other modules.
Leaving the actual config model in config, to avoid changing too much.


laurb9: > The preferred naming convention for packages in Go is to use a singular form. In this case, that would be `account` vs `accounts`.

Agree, will change it. Almost did it the first time around actually, then I saw all the other package names :) 


Patch:
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
new file mode 100644
index 00000000..d7bb50fb
--- /dev/null
+++ b/.github/workflows/validate.yml
@@ -0,0 +1,28 @@
+on:
+  push:
+    branches:
+      - master
+  pull_request:
+  release:
+    types:
+      - created
+name: Validate
+jobs:
+  Go:
+    strategy:
+      matrix:
+        go-version: [1.13.x, 1.14.x, 1.15.x]
+        os: [ubuntu-18.04]
+    runs-on: ${{ matrix.os }}
+    steps:
+    - name: Install Go
+      uses: actions/setup-go@v2
+      with:
+        go-version: ${{ matrix.go-version }}
+    - name: Checkout code
+      uses: actions/checkout@v2
+    - name: Validate
+      run: |
+        ./validate.sh --nofmt --cov --race 10
+      env:
+        GO111MODULE: "on"
diff --git a/account/account.go b/account/account.go
new file mode 100644
index 00000000..2f27b61e
--- /dev/null
+++ b/account/account.go
@@ -0,0 +1,69 @@
+package account
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+
+	jsonpatch "github.com/evanphx/json-patch"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/pbsmetrics"
+	"github.com/prebid/prebid-server/stored_requests"
+)
+
+// GetAccount looks up the config.Account object referenced by the given accountID, with access rules applied
+func GetAccount(ctx context.Context, cfg *config.Configuration, fetcher stored_requests.AccountFetcher, accountID string) (account *config.Account, errs []error) {
+	// Check BlacklistedAcctMap until we have deprecated it
+	if _, found := cfg.BlacklistedAcctMap[accountID]; found {
+		return nil, []error{&errortypes.BlacklistedAcct{
+			Message: fmt.Sprintf("Prebid-server has disabled Account ID: %s, please reach out to the prebid server host.", accountID),
+		}}
+	}
+	if cfg.AccountRequired && accountID == pbsmetrics.PublisherUnknown {
+		return nil, []error{&errortypes.AcctRequired{
+			Message: fmt.Sprintf("Prebid-server has been configured to discard requests without a valid Account ID. Please reach out to the prebid server host."),
+		}}
+	}
+	if accountJSON, accErrs := fetcher.FetchAccount(ctx, accountID); len(accErrs) > 0 || accountJSON == nil {
+		// accountID does not reference a valid account
+		for _, e := range accErrs {
+			if _, ok := e.(stored_requests.NotFoundError); !ok {
+				errs = append(errs, e)
+			}
+		}
+		if cfg.AccountRequired && cfg.AccountDefaults.Disabled {
+			errs = append(errs, &errortypes.AcctRequired{
+				Message: fmt.Sprintf("Prebid-server could not verify the Account ID. Please reach out to the prebid server host."),
+			})
+			return nil, errs
+		}
+		// Make a copy of AccountDefaults instead of taking a reference,
+		// to preserve original accountID in case is needed to check NonStandardPublisherMap
+		pubAccount := cfg.AccountDefaults
+		pubAccount.ID = accountID
+		account = &pubAccount
+	} else {
+		// accountID resolved to a valid account, merge with AccountDefaults for a complete config
+		account = &config.Account{}
+		completeJSON, err := jsonpatch.MergePatch(cfg.AccountDefaultsJSON(), accountJSON)
+		if err == nil {
+			err = json.Unmarshal(completeJSON, account)
+		}
+		if err != nil {
+			errs = append(errs, err)
+			return nil, errs
+		}
+		// Fill in ID if needed, so it can be left out of account definition
+		if len(account.ID) == 0 {
+			account.ID = accountID
+		}
+	}
+	if account.Disabled {
+		errs = append(errs, &errortypes.BlacklistedAcct{
+			Message: fmt.Sprintf("Prebid-server has disabled Account ID: %s, please reach out to the prebid server host.", accountID),
+		})
+		return nil, errs
+	}
+	return account, nil
+}
diff --git a/adapters/rubicon/rubicon.go b/adapters/rubicon/rubicon.go
index 56ae7b2f..7d6e0e12 100644
--- a/adapters/rubicon/rubicon.go
+++ b/adapters/rubicon/rubicon.go
@@ -236,6 +236,7 @@ var rubiSizeMap = map[rubiSize]int{
 	{w: 800, h: 250}:   125,
 	{w: 200, h: 600}:   126,
 	{w: 640, h: 320}:   156,
+	{w: 640, h: 360}:   198,
 }
 
 // defines the contract for bidrequest.user.ext.eids[i].ext
diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go
index 54f47069..1e92569e 100644
--- a/endpoints/openrtb2/amp_auction.go
+++ b/endpoints/openrtb2/amp_auction.go
@@ -16,6 +16,7 @@ import (
 	"github.com/golang/glog"
 	"github.com/julienschmidt/httprouter"
 	"github.com/mxmCherry/openrtb"
+	accountService "github.com/prebid/prebid-server/account"
 	"github.com/prebid/prebid-server/analytics"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/errortypes"
@@ -158,7 +159,7 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
 	}
 	labels.PubID = getAccountID(req.Site.Publisher)
 	// Look up account now that we have resolved the pubID value
-	account, acctIDErrs := deps.getAccount(ctx, labels.PubID)
+	account, acctIDErrs := accountService.GetAccount(ctx, deps.cfg, deps.accounts, labels.PubID)
 	if len(acctIDErrs) > 0 {
 		errL = append(errL, acctIDErrs...)
 		httpStatus := http.StatusBadRequest
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index bc0cd900..d6cbc228 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -22,6 +22,7 @@ import (
 	"github.com/mxmCherry/openrtb"
 	"github.com/mxmCherry/openrtb/native"
 	nativeRequests "github.com/mxmCherry/openrtb/native/request"
+	accountService "github.com/prebid/prebid-server/account"
 	"github.com/prebid/prebid-server/analytics"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/errortypes"
@@ -156,7 +157,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 	}
 
 	// Look up account now that we have resolved the pubID value
-	account, acctIDErrs := deps.getAccount(ctx, labels.PubID)
+	account, acctIDErrs := accountService.GetAccount(ctx, deps.cfg, deps.accounts, labels.PubID)
 	if len(acctIDErrs) > 0 {
 		errL = append(errL, acctIDErrs...)
 		writeError(errL, w, &labels)
@@ -765,8 +766,8 @@ func (deps *endpointDeps) validateImpExt(imp *openrtb.Imp, aliases map[string]st
 	}
 
 	// Also accept bidder exts within imp[...].ext.prebid.bidder
-	// NOTE: This is not part of the official API, we are not expecting clients
-	// migrate from imp[...].ext.${BIDDER} to imp[...].ext.prebid.bidder.${BIDDER}
+	// NOTE: This is not part of the official API yet, so we are not expecting clients
+	// to migrate from imp[...].ext.${BIDDER} to imp[...].ext.prebid.bidder.${BIDDER}
 	// at this time
 	// https://github.com/prebid/prebid-server/pull/846#issuecomment-476352224
 	if rawPrebidExt, ok := bidderExts[openrtb_ext.PrebidExtKey]; ok {
@@ -785,7 +786,7 @@ func (deps *endpointDeps) validateImpExt(imp *openrtb.Imp, aliases map[string]st
 	/* Process all the bidder exts in the request */
 	disabledBidders := []string{}
 	for bidder, ext := range bidderExts {
-		if bidder != openrtb_ext.PrebidExtKey {
+		if isBidderToValidate(bidder) {
 			coreBidder := bidder
 			if tmp, isAlias := aliases[bidder]; isAlias {
 				coreBidder = tmp
@@ -820,12 +821,20 @@ func (deps *endpointDeps) validateImpExt(imp *openrtb.Imp, aliases map[string]st
 	// TODO #713 Fix this here
 	if len(bidderExts) < 1 {
 		errL = append(errL, fmt.Errorf("request.imp[%d].ext must contain at least one bidder", impIndex))
-		return errL
 	}
 
 	return errL
 }
 
+func isBidderToValidate(bidder string) bool {
+	// PrebidExtKey is a special case for the prebid config section and is not considered a bidder.
+
+	// FirstPartyDataContextExtKey is a special case for the first party data context section
+	// and is not considered a bidder.
+
+	return bidder != openrtb_ext.PrebidExtKey && bidder != openrtb_ext.FirstPartyDataContextExtKey
+}
+
 func (deps *endpointDeps) parseBidExt(ext json.RawMessage) (*openrtb_ext.ExtRequest, error) {
 	if len(ext) < 1 {
 		return nil, nil
@@ -1309,56 +1318,3 @@ func getAccountID(pub *openrtb.Publisher) string {
 	}
 	return pbsmetrics.PublisherUnknown
 }
-
-func (deps *endpointDeps) getAccount(ctx context.Context, pubID string) (account *config.Account, errs []error) {
-	// Check BlacklistedAcctMap until we have deprecated it
-	if _, found := deps.cfg.BlacklistedAcctMap[pubID]; found {
-		return nil, []error{&errortypes.BlacklistedAcct{
-			Message: fmt.Sprintf("Prebid-server has disabled Account ID: %s, please reach out to the prebid server host.", pubID),
-		}}
-	}
-	if deps.cfg.AccountRequired && pubID == pbsmetrics.PublisherUnknown {
-		return nil, []error{&errortypes.AcctRequired{
-			Message: fmt.Sprintf("Prebid-server has been configured to discard requests without a valid Account ID. Please reach out to the prebid server host."),
-		}}
-	}
-	if accountJSON, accErrs := deps.accounts.FetchAccount(ctx, pubID); len(accErrs) > 0 || accountJSON == nil {
-		// pubID does not reference a valid account
-		if len(accErrs) > 0 {
-			errs = append(errs, errs...)
-		}
-		if deps.cfg.AccountRequired && deps.cfg.AccountDefaults.Disabled {
-			errs = append(errs, &errortypes.AcctRequired{
-				Message: fmt.Sprintf("Prebid-server has been configured to discard requests without a valid Account ID. Please reach out to the prebid server host."),
-			})
-			return nil, errs
-		}
-		// Make a copy of AccountDefaults instead of taking a reference,
-		// to preserve original pubID in case is needed to check NonStandardPublisherMap
-		pubAccount := deps.cfg.AccountDefaults
-		pubAccount.ID = pubID
-		account = &pubAccount
-	} else {
-		// pubID resolved to a valid account, merge with AccountDefaults for a complete config
-		account = &config.Account{}
-		completeJSON, err := jsonpatch.MergePatch(deps.cfg.AccountDefaultsJSON(), accountJSON)
-		if err == nil {
-			err = json.Unmarshal(completeJSON, account)
-		}
-		if err != nil {
-			errs = append(errs, err)
-			return nil, errs
-		}
-		// Fill in ID if needed, so it can be left out of account definition
-		if len(account.ID) == 0 {
-			account.ID = pubID
-		}
-	}
-	if account.Disabled {
-		errs = append(errs, &errortypes.BlacklistedAcct{
-			Message: fmt.Sprintf("Prebid-server has disabled Account ID: %s, please reach out to the prebid server host.", pubID),
-		})
-		return nil, errs
-	}
-	return
-}
diff --git a/endpoints/openrtb2/sample-requests/first-party-data/valid-context-allowed-with-ext-bidder.json b/endpoints/openrtb2/sample-requests/first-party-data/valid-context-allowed-with-ext-bidder.json
new file mode 100644
index 00000000..aa205fc5
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/first-party-data/valid-context-allowed-with-ext-bidder.json
@@ -0,0 +1,32 @@
+{
+    "description": "The imp.ext.context field is valid for First Party Data and should be exempted from bidder name validation.",
+
+    "requestPayload": {
+        "id": "some-request-id",
+        "site": {
+            "page": "test.somepage.com"
+        },
+        "imp": [{
+            "id": "some-imp-id",
+            "banner": {
+                "format": [{
+                    "w": 600,
+                    "h": 500
+                }, {
+                    "w": 300,
+                    "h": 600
+                }]
+            },
+            "ext": {
+                "appnexus": {
+                    "placementId": 12883451
+                },
+                "context": {
+                    "data": {
+                        "keywords": "prebid server example"
+                    }
+                }
+            }
+        }]
+    }
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/first-party-data/valid-context-allowed-with-prebid-bidder.json b/endpoints/openrtb2/sample-requests/first-party-data/valid-context-allowed-with-prebid-bidder.json
new file mode 100644
index 00000000..1616e84b
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/first-party-data/valid-context-allowed-with-prebid-bidder.json
@@ -0,0 +1,36 @@
+{
+    "description": "The imp.ext.context field is valid for First Party Data and should be exempted from bidder name validation.",
+
+    "requestPayload": {
+        "id": "some-request-id",
+        "site": {
+            "page": "test.somepage.com"
+        },
+        "imp": [{
+            "id": "some-imp-id",
+            "banner": {
+                "format": [{
+                    "w": 600,
+                    "h": 500
+                }, {
+                    "w": 300,
+                    "h": 600
+                }]
+            },
+            "ext": {
+                "prebid": {
+                    "bidder": {
+                        "appnexus": {
+                            "placementId": 12883451
+                        }
+                    }
+                },
+                "context": {
+                    "data": {
+                        "keywords": "prebid server example"
+                    }
+                }
+            }
+        }]
+    }
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go
index f5494751..ab5634c7 100644
--- a/endpoints/openrtb2/video_auction.go
+++ b/endpoints/openrtb2/video_auction.go
@@ -23,6 +23,7 @@ import (
 	"github.com/golang/glog"
 	"github.com/julienschmidt/httprouter"
 	"github.com/mxmCherry/openrtb"
+	accountService "github.com/prebid/prebid-server/account"
 	"github.com/prebid/prebid-server/analytics"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/exchange"
@@ -255,7 +256,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 	}
 
 	// Look up account now that we have resolved the pubID value
-	account, acctIDErrs := deps.getAccount(ctx, labels.PubID)
+	account, acctIDErrs := accountService.GetAccount(ctx, deps.cfg, deps.accounts, labels.PubID)
 	if len(acctIDErrs) > 0 {
 		handleError(&labels, w, acctIDErrs, &vo, &debugLog)
 		return
diff --git a/exchange/utils.go b/exchange/utils.go
index 2e9e4dc8..5863f6c8 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -273,13 +273,18 @@ func splitImps(imps []openrtb.Imp) (map[string][]openrtb.Imp, []error) {
 		imp := imps[i]
 		impExt := impExts[i]
 
+		var firstPartyDataContext json.RawMessage
+		if context, exists := impExt[openrtb_ext.FirstPartyDataContextExtKey]; exists {
+			firstPartyDataContext = context
+		}
+
 		rawPrebidExt, ok := impExt[openrtb_ext.PrebidExtKey]
 
 		if ok {
 			var prebidExt openrtb_ext.ExtImpPrebid
 
 			if err := json.Unmarshal(rawPrebidExt, &prebidExt); err == nil && prebidExt.Bidder != nil {
-				if errs := sanitizedImpCopy(&imp, prebidExt.Bidder, rawPrebidExt, &splitImps); errs != nil {
+				if errs := sanitizedImpCopy(&imp, prebidExt.Bidder, rawPrebidExt, firstPartyDataContext, &splitImps); errs != nil {
 					errList = append(errList, errs...)
 				}
 
@@ -287,7 +292,7 @@ func splitImps(imps []openrtb.Imp) (map[string][]openrtb.Imp, []error) {
 			}
 		}
 
-		if errs := sanitizedImpCopy(&imp, impExt, rawPrebidExt, &splitImps); errs != nil {
+		if errs := sanitizedImpCopy(&imp, impExt, rawPrebidExt, firstPartyDataContext, &splitImps); errs != nil {
 			errList = append(errList, errs...)
 		}
 	}
@@ -295,35 +300,38 @@ func splitImps(imps []openrtb.Imp) (map[string][]openrtb.Imp, []error) {
 	return splitImps, nil
 }
 
-// sanitizedImpCopy returns a copy of imp with its ext filtered so that only "prebid" and bidder params exist.
+// sanitizedImpCopy returns a copy of imp with its ext filtered so that only "prebid", "context", and bidder params exist.
 // It will not mutate the input imp.
 // This function will write the new imps to the output map passed in
 func sanitizedImpCopy(imp *openrtb.Imp,
 	bidderExts map[string]json.RawMessage,
 	rawPrebidExt json.RawMessage,
+	firstPartyDataContext json.RawMessage,
 	out *map[string][]openrtb.Imp) []error {
 
 	var prebidExt map[string]json.RawMessage
 	var errs []error
 
-	// We don't want to include other demand partners' bidder params
-	// in the sanitized imp
 	if err := json.Unmarshal(rawPrebidExt, &prebidExt); err == nil {
-		delete(prebidExt, "bidder")
-
-		var err error
-		if rawPrebidExt, err = json.Marshal(prebidExt); err != nil {
-			errs = append(errs, err)
+		// Remove the entire bidder field. We will already have the content we need in bidderExts. We
+		// don't want to include other demand partners' bidder params in the sanitized imp.
+		if _, hasBidderField := prebidExt["bidder"]; hasBidderField {
+			delete(prebidExt, "bidder")
+
+			var err error
+			if rawPrebidExt, err = json.Marshal(prebidExt); err != nil {
+				errs = append(errs, err)
+			}
 		}
 	}
 
 	for bidder, ext := range bidderExts {
-		if bidder == openrtb_ext.PrebidExtKey {
+		if bidder == openrtb_ext.PrebidExtKey || bidder == openrtb_ext.FirstPartyDataContextExtKey {
 			continue
 		}
 
 		impCopy := *imp
-		newExt := make(map[string]json.RawMessage, 2)
+		newExt := make(map[string]json.RawMessage, 3)
 
 		newExt["bidder"] = ext
 
@@ -331,6 +339,10 @@ func sanitizedImpCopy(imp *openrtb.Imp,
 			newExt[openrtb_ext.PrebidExtKey] = rawPrebidExt
 		}
 
+		if len(firstPartyDataContext) > 0 {
+			newExt[openrtb_ext.FirstPartyDataContextExtKey] = firstPartyDataContext
+		}
+
 		rawExt, err := json.Marshal(newExt)
 		if err != nil {
 			errs = append(errs, err)
@@ -392,7 +404,7 @@ func resolveBidder(bidder string, aliases map[string]string) openrtb_ext.BidderN
 }
 
 // parseImpExts does a partial-unmarshal of the imp[].Ext field.
-// The keys in the returned map are expected to be "prebid", core BidderNames, or Aliases for this request.
+// The keys in the returned map are expected to be "prebid", "context", core BidderNames, or Aliases for this request.
 func parseImpExts(imps []openrtb.Imp) ([]map[string]json.RawMessage, error) {
 	exts := make([]map[string]json.RawMessage, len(imps))
 	// Loop over every impression in the request
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 761f53d4..876eeab8 100755
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -20,6 +20,9 @@ type BidderName string
 // BidderNameGeneral is reserved for non-bidder specific messages when using a map keyed on the bidder name.
 const BidderNameGeneral = BidderName("general")
 
+// BidderNameContext is reserved for first party data.
+const BidderNameContext = BidderName("context")
+
 // These names _must_ coincide with the bidder code in Prebid.js, if an adapter also exists in that project.
 // Please keep these (and the BidderMap) alphabetized to minimize merge conflicts among adapter submissions.
 // The bidder name 'general' is not allowed since it has special meaning in message maps.
diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go
index d6edf47f..42ac9d9d 100644
--- a/openrtb_ext/request.go
+++ b/openrtb_ext/request.go
@@ -5,6 +5,10 @@ import (
 	"errors"
 )
 
+// FirstPartyDataContextExtKey defines the field name within bidrequest.ext reserved
+// for first party data support.
+const FirstPartyDataContextExtKey string = "context"
+
 // ExtRequest defines the contract for bidrequest.ext
 type ExtRequest struct {
 	Prebid ExtRequestPrebid `json:"prebid"`
diff --git a/stored_requests/caches/memory/cache.go b/stored_requests/caches/memory/cache.go
index c5702080..aea087e6 100644
--- a/stored_requests/caches/memory/cache.go
+++ b/stored_requests/caches/memory/cache.go
@@ -7,7 +7,6 @@ import (
 
 	"github.com/coocood/freecache"
 	"github.com/golang/glog"
-	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/stored_requests"
 )
 
@@ -17,68 +16,51 @@ import (
 // 2. The cache is too large. This will cause the least recently used items to be evicted.
 //
 // For no TTL, use ttlSeconds <= 0
-func NewCache(cfg *config.InMemoryCache) stored_requests.Cache {
-	return &cache{
-		requestDataCache: newCacheForWithLimits(cfg.RequestCacheSize, cfg.TTL, "Request"),
-		impDataCache:     newCacheForWithLimits(cfg.ImpCacheSize, cfg.TTL, "Imp"),
-	}
-}
-
-func newCacheForWithLimits(size int, ttl int, dataType string) mapLike {
+func NewCache(size int, ttl int, dataType string) stored_requests.CacheJSON {
 	if ttl > 0 && size <= 0 {
-		glog.Fatal("No in-memory caches defined with a finite TTL but unbounded size. Config validation should have caught this. Failing fast because something is buggy.")
+		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)
 	}
 	if size > 0 {
 		glog.Infof("Using a Stored %s in-memory cache. Max size: %d bytes. TTL: %d seconds.", dataType, size, ttl)
-		return &pbsLRUCache{
-			Cache:      freecache.NewCache(size),
-			ttlSeconds: ttl,
+		return &cache{
+			dataType: dataType,
+			cache: &pbsLRUCache{
+				Cache:      freecache.NewCache(size),
+				ttlSeconds: ttl,
+			},
 		}
 	} else {
 		glog.Infof("Using an unbounded Stored %s in-memory cache.", dataType)
-		return &pbsSyncMap{&sync.Map{}}
+		return &cache{
+			dataType: dataType,
+			cache:    &pbsSyncMap{&sync.Map{}},
+		}
 	}
 }
 
 type cache struct {
-	requestDataCache mapLike
-	impDataCache     mapLike
+	dataType string
+	cache    mapLike
 }
 
-func (c *cache) Get(ctx context.Context, requestIDs []string, impIDs []string) (requestData map[string]json.RawMessage, impData map[string]json.RawMessage) {
-	requestData = doGet(c.requestDataCache, requestIDs)
-	impData = doGet(c.impDataCache, impIDs)
-	return
-}
-
-func doGet(cache mapLike, ids []string) (data map[string]json.RawMessage) {
+func (c *cache) Get(ctx context.Context, ids []string) (data map[string]json.RawMessage) {
 	data = make(map[string]json.RawMessage, len(ids))
 	for _, id := range ids {
-		if val, ok := cache.Get(id); ok {
+		if val, ok := c.cache.Get(id); ok {
 			data[id] = val
 		}
 	}
 	return
 }
 
-func (c *cache) Save(ctx context.Context, storedRequests map[string]json.RawMessage, storedImps map[string]json.RawMessage) {
-	c.doSave(c.requestDataCache, storedRequests)
-	c.doSave(c.impDataCache, storedImps)
-}
-
-func (c *cache) doSave(cache mapLike, values map[string]json.RawMessage) {
-	for id, data := range values {
-		cache.Set(id, data)
+func (c *cache) Save(ctx context.Context, data map[string]json.RawMessage) {
+	for id, data := range data {
+		c.cache.Set(id, data)
 	}
 }
 
-func (c *cache) Invalidate(ctx context.Context, requestIDs []string, impIDs []string) {
-	doInvalidate(c.requestDataCache, requestIDs)
-	doInvalidate(c.impDataCache, impIDs)
-}
-
-func doInvalidate(cache mapLike, ids []string) {
+func (c *cache) Invalidate(ctx context.Context, ids []string) {
 	for _, id := range ids {
-		cache.Delete(id)
+		c.cache.Delete(id)
 	}
 }
diff --git a/stored_requests/caches/nil_cache/nil_cache.go b/stored_requests/caches/nil_cache/nil_cache.go
index de29156e..d043ae55 100644
--- a/stored_requests/caches/nil_cache/nil_cache.go
+++ b/stored_requests/caches/nil_cache/nil_cache.go
@@ -8,13 +8,14 @@ import (
 // NilCache is a no-op cache which does nothing useful.
 type NilCache struct{}
 
-func (c *NilCache) Get(ctx context.Context, requestIDs []string, impIDs []string) (map[string]json.RawMessage, map[string]json.RawMessage) {
-	return nil, nil
+func (c *NilCache) Get(ctx context.Context, ids []string) map[string]json.RawMessage {
+	return make(map[string]json.RawMessage)
 }
-func (c *NilCache) Save(ctx context.Context, storedRequests map[string]json.RawMessage, storedImps map[string]json.RawMessage) {
+
+func (c *NilCache) Save(ctx context.Context, data map[string]json.RawMessage) {
 	return
 }
 
-func (c *NilCache) Invalidate(ctx context.Context, requestIDs []string, impIDs []string) {
+func (c *NilCache) Invalidate(ctx context.Context, ids []string) {
 	return
 }
diff --git a/stored_requests/config/config.go b/stored_requests/config/config.go
index e81d9667..6c663cbf 100644
--- a/stored_requests/config/config.go
+++ b/stored_requests/config/config.go
@@ -178,10 +178,13 @@ 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 &nil_cache.NilCache{}
+		return stored_requests.Cache{&nil_cache.NilCache{}, &nil_cache.NilCache{}}
 	}
 
-	return memory.NewCache(&cfg.InMemoryCache)
+	return stored_requests.Cache{
+		Requests: memory.NewCache(cfg.InMemoryCache.RequestCacheSize, cfg.InMemoryCache.TTL, "Requests"),
+		Imps:     memory.NewCache(cfg.InMemoryCache.ImpCacheSize, cfg.InMemoryCache.TTL, "Imps"),
+	}
 }
 
 func newEventProducers(cfg *config.StoredRequests, client *http.Client, db *sql.DB, router *httprouter.Router) (eventProducers []events.EventProducer) {
diff --git a/stored_requests/events/events.go b/stored_requests/events/events.go
index ea67e8ee..ba08f13d 100644
--- a/stored_requests/events/events.go
+++ b/stored_requests/events/events.go
@@ -61,12 +61,14 @@ func (e *EventListener) Listen(cache stored_requests.Cache, events EventProducer
 	for {
 		select {
 		case save := <-events.Saves():
-			cache.Save(context.Background(), save.Requests, save.Imps)
+			cache.Requests.Save(context.Background(), save.Requests)
+			cache.Imps.Save(context.Background(), save.Imps)
 			if e.onSave != nil {
 				e.onSave()
 			}
 		case invalidation := <-events.Invalidations():
-			cache.Invalidate(context.Background(), invalidation.Requests, invalidation.Imps)
+			cache.Requests.Invalidate(context.Background(), invalidation.Requests)
+			cache.Imps.Invalidate(context.Background(), invalidation.Imps)
 			if e.onInvalidate != nil {
 				e.onInvalidate()
 			}
diff --git a/stored_requests/fetcher.go b/stored_requests/fetcher.go
index a31b9989..e9716e08 100644
--- a/stored_requests/fetcher.go
+++ b/stored_requests/fetcher.go
@@ -37,9 +37,9 @@ type CategoryFetcher interface {
 
 // AllFetcher is an interface that encapsulates both the original Fetcher and the CategoryFetcher
 type AllFetcher interface {
-	FetchRequests(ctx context.Context, requestIDs []string, impIDs []string) (requestData map[string]json.RawMessage, impData map[string]json.RawMessage, errs []error)
-	FetchAccount(ctx context.Context, accountID string) (json.RawMessage, []error)
-	FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error)
+	Fetcher
+	AccountFetcher
+	CategoryFetcher
 }
 
 // NotFoundError is an error type to flag that an ID was not found by the Fetcher.
@@ -62,7 +62,11 @@ func (e NotFoundError) Error() string {
 // Cache is an intermediate layer which can be used to create more complex Fetchers by composition.
 // Implementations must be safe for concurrent access by multiple goroutines.
 // To add a Cache layer in front of a Fetcher, see WithCache()
-type Cache interface {
+type Cache struct {
+	Requests CacheJSON
+	Imps     CacheJSON
+}
+type CacheJSON interface {
 	// Get works much like Fetcher.FetchRequests, with a few exceptions:
 	//
 	// 1. Any (actionable) errors should be logged by the implementation, rather than returned.
@@ -73,37 +77,33 @@ type Cache interface {
 	//
 	// Nil slices and empty strings are treated as "no ops". That is, a nil requestID will always produce a nil
 	// "stored request data" in the response.
-	Get(ctx context.Context, requestIDs []string, impIDs []string) (requestData map[string]json.RawMessage, impData map[string]json.RawMessage)
+	Get(ctx context.Context, ids []string) (data map[string]json.RawMessage)
 
 	// Invalidate will ensure that all values associated with the given IDs
 	// are no longer returned by the cache until new values are saved via Update
-	Invalidate(ctx context.Context, requestIDs []string, impIDs []string)
+	Invalidate(ctx context.Context, ids []string)
 
 	// Save will add or overwrite the data in the cache at the given keys
-	Save(ctx context.Context, requestData map[string]json.RawMessage, impData map[string]json.RawMessage)
+	Save(ctx context.Context, data map[string]json.RawMessage)
 }
 
 // ComposedCache creates an interface to treat a slice of caches as a single cache
-type ComposedCache []Cache
+type ComposedCache []CacheJSON
 
 // Get will attempt to Get from the caches in the order in which they are in the slice,
 // stopping as soon as a value is found (or when all caches have been exhausted)
-func (c ComposedCache) Get(ctx context.Context, requestIDs []string, impIDs []string) (requestData map[string]json.RawMessage, impData map[string]json.RawMessage) {
-	requestData = make(map[string]json.RawMessage, len(requestIDs))
-	impData = make(map[string]json.RawMessage, len(impIDs))
+func (c ComposedCache) Get(ctx context.Context, ids []string) (data map[string]json.RawMessage) {
+	data = make(map[string]json.RawMessage, len(ids))
 
-	remainingReqIDs := requestIDs
-	remainingImpIDs := impIDs
+	remainingIDs := ids
 
 	for _, cache := range c {
-		cachedReqData, cachedImpData := cache.Get(ctx, remainingReqIDs, remainingImpIDs)
-
-		requestData, remainingReqIDs = updateFromCache(requestData, remainingReqIDs, cachedReqData)
-		impData, remainingImpIDs = updateFromCache(impData, remainingImpIDs, cachedImpData)
+		cachedData := cache.Get(ctx, remainingIDs)
+		data, remainingIDs = updateFromCache(data, remainingIDs, cachedData)
 
-		// return if all ids filled
-		if len(remainingReqIDs) == 0 && len(remainingImpIDs) == 0 {
-			return
+		// finish early if all ids filled
+		if len(remainingIDs) == 0 {
+			break
 		}
 	}
 
@@ -129,16 +129,16 @@ func updateFromCache(data map[string]json.RawMessage, ids []string, newData map[
 }
 
 // Invalidate will propagate invalidations to all underlying caches
-func (c ComposedCache) Invalidate(ctx context.Context, requestIDs []string, impIDs []string) {
+func (c ComposedCache) Invalidate(ctx context.Context, ids []string) {
 	for _, cache := range c {
-		cache.Invalidate(ctx, requestIDs, impIDs)
+		cache.Invalidate(ctx, ids)
 	}
 }
 
 // Save will propagate saves to all underlying caches
-func (c ComposedCache) Save(ctx context.Context, requestData map[string]json.RawMessage, impData map[string]json.RawMessage) {
+func (c ComposedCache) Save(ctx context.Context, data map[string]json.RawMessage) {
 	for _, cache := range c {
-		cache.Save(ctx, requestData, impData)
+		cache.Save(ctx, data)
 	}
 }
 
@@ -148,7 +148,7 @@ type fetcherWithCache struct {
 	metricsEngine pbsmetrics.MetricsEngine
 }
 
-// WithCache returns a Fetcher which uses the given Cache before delegating to the original.
+// WithCache returns a Fetcher which uses the given Caches before delegating to the original.
 // This can be called multiple times to compose Cache layers onto the backing Fetcher, though
 // it is usually more desirable to first compose caches with Compose, ensuring propagation of updates
 // and invalidations through all cache layers.
@@ -161,7 +161,9 @@ func WithCache(fetcher AllFetcher, cache Cache, metricsEngine pbsmetrics.Metrics
 }
 
 func (f *fetcherWithCache) FetchRequests(ctx context.Context, requestIDs []string, impIDs []string) (requestData map[string]json.RawMessage, impData map[string]json.RawMessage, errs []error) {
-	requestData, impData = f.cache.Get(ctx, requestIDs, impIDs)
+
+	requestData = f.cache.Requests.Get(ctx, requestIDs)
+	impData = f.cache.Imps.Get(ctx, impIDs)
 
 	// Fixes #311
 	leftoverImps := findLeftovers(impIDs, impData)
@@ -178,7 +180,8 @@ func (f *fetcherWithCache) FetchRequests(ctx context.Context, requestIDs []strin
 		fetcherReqData, fetcherImpData, fetcherErrs := f.fetcher.FetchRequests(ctx, leftoverReqs, leftoverImps)
 		errs = fetcherErrs
 
-		f.cache.Save(ctx, fetcherReqData, fetcherImpData)
+		f.cache.Requests.Save(ctx, fetcherReqData)
+		f.cache.Imps.Save(ctx, fetcherImpData)
 
 		requestData = mergeData(requestData, fetcherReqData)
 		impData = mergeData(impData, fetcherImpData)


Test patch:
diff --git a/account/account_test.go b/account/account_test.go
new file mode 100644
index 00000000..0d192f18
--- /dev/null
+++ b/account/account_test.go
@@ -0,0 +1,94 @@
+package account
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"testing"
+
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/pbsmetrics"
+	"github.com/prebid/prebid-server/stored_requests"
+	"github.com/stretchr/testify/assert"
+)
+
+var mockAccountData = map[string]json.RawMessage{
+	"valid_acct":    json.RawMessage(`{"disabled":false}`),
+	"disabled_acct": json.RawMessage(`{"disabled":true}`),
+}
+
+type mockAccountFetcher struct {
+}
+
+func (af mockAccountFetcher) FetchAccount(ctx context.Context, accountID string) (json.RawMessage, []error) {
+	if account, ok := mockAccountData[accountID]; ok {
+		return account, nil
+	}
+	return nil, []error{stored_requests.NotFoundError{accountID, "Account"}}
+}
+
+func TestGetAccount(t *testing.T) {
+	unknown := pbsmetrics.PublisherUnknown
+	testCases := []struct {
+		accountID string
+		// account_required
+		required bool
+		// account_defaults.disabled
+		disabled bool
+		// expected error, or nil if account should be found
+		err error
+	}{
+		// Blacklisted account is always rejected even in permissive setup
+		{accountID: "bad_acct", required: false, disabled: false, err: &errortypes.BlacklistedAcct{}},
+
+		// empty pubID
+		{accountID: unknown, required: false, disabled: false, err: nil},
+		{accountID: unknown, required: true, disabled: false, err: &errortypes.AcctRequired{}},
+		{accountID: unknown, required: false, disabled: true, err: &errortypes.BlacklistedAcct{}},
+		{accountID: unknown, required: true, disabled: true, err: &errortypes.AcctRequired{}},
+
+		// pubID given but is not a valid host account (does not exist)
+		{accountID: "doesnt_exist_acct", required: false, disabled: false, err: nil},
+		{accountID: "doesnt_exist_acct", required: true, disabled: false, err: nil},
+		{accountID: "doesnt_exist_acct", required: false, disabled: true, err: &errortypes.BlacklistedAcct{}},
+		{accountID: "doesnt_exist_acct", required: true, disabled: true, err: &errortypes.AcctRequired{}},
+
+		// pubID given and matches a valid host account with Disabled: false
+		{accountID: "valid_acct", required: false, disabled: false, err: nil},
+		{accountID: "valid_acct", required: true, disabled: false, err: nil},
+		{accountID: "valid_acct", required: false, disabled: true, err: nil},
+		{accountID: "valid_acct", required: true, disabled: true, err: nil},
+
+		// pubID given and matches a host account explicitly disabled (Disabled: true on account json)
+		{accountID: "disabled_acct", required: false, disabled: false, err: &errortypes.BlacklistedAcct{}},
+		{accountID: "disabled_acct", required: true, disabled: false, err: &errortypes.BlacklistedAcct{}},
+		{accountID: "disabled_acct", required: false, disabled: true, err: &errortypes.BlacklistedAcct{}},
+		{accountID: "disabled_acct", required: true, disabled: true, err: &errortypes.BlacklistedAcct{}},
+	}
+
+	for _, test := range testCases {
+		description := fmt.Sprintf(`ID=%s/required=%t/disabled=%t`, test.accountID, test.required, test.disabled)
+		t.Run(description, func(t *testing.T) {
+			cfg := &config.Configuration{
+				BlacklistedAcctMap: map[string]bool{"bad_acct": true},
+				AccountRequired:    test.required,
+				AccountDefaults:    config.Account{Disabled: test.disabled},
+			}
+			fetcher := &mockAccountFetcher{}
+			assert.NoError(t, cfg.MarshalAccountDefaults())
+
+			account, errors := GetAccount(context.Background(), cfg, fetcher, test.accountID)
+
+			if test.err == nil {
+				assert.Empty(t, errors)
+				assert.Equal(t, test.accountID, account.ID, "account.ID must match requested ID")
+				assert.Equal(t, false, account.Disabled, "returned account must not be disabled")
+			} else {
+				assert.NotEmpty(t, errors, "expected errors but got success")
+				assert.Nil(t, account, "return account must be nil on error")
+				assert.IsType(t, test.err, errors[0], "error is of unexpected type")
+			}
+		})
+	}
+}
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index 53fea2e0..925cffce 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -141,6 +141,16 @@ func TestGoodRequests(t *testing.T) {
 	supplementary.assert(t)
 }
 
+func TestFirstPartyDataRequests(t *testing.T) {
+	validRequests := &getResponseFromDirectory{
+		dir:           "sample-requests/first-party-data",
+		payloadGetter: getRequestPayload,
+		messageGetter: nilReturner,
+		expectedCode:  http.StatusOK,
+	}
+	validRequests.assert(t)
+}
+
 // TestGoodNativeRequests makes sure we return 200s on well-formed Native requests.
 func TestGoodNativeRequests(t *testing.T) {
 	tests := &getResponseFromDirectory{
@@ -1127,10 +1137,73 @@ func TestDisabledBidder(t *testing.T) {
 	}
 }
 
-func TestValidateImpExtDisabledBidder(t *testing.T) {
-	imp := &openrtb.Imp{
-		Ext: json.RawMessage(`{"appnexus":{"placement_id":555},"unknownbidder":{"foo":"bar"}}`),
+func TestValidateImpExt(t *testing.T) {
+	testCases := []struct {
+		description    string
+		impExt         json.RawMessage
+		expectedImpExt string
+		expectedErrs   []error
+	}{
+		{
+			description:    "Empty",
+			impExt:         nil,
+			expectedImpExt: "",
+			expectedErrs:   []error{errors.New("request.imp[0].ext is required")},
+		},
+		{
+			description:    "Valid Bidder",
+			impExt:         json.RawMessage(`{"appnexus":{"placement_id":555}}`),
+			expectedImpExt: `{"appnexus":{"placement_id":555}}`,
+			expectedErrs:   []error{},
+		},
+		{
+			description:    "Valid Bidder + Disabled Bidder",
+			impExt:         json.RawMessage(`{"appnexus":{"placement_id":555},"unknownbidder":{"foo":"bar"}}`),
+			expectedImpExt: `{"appnexus":{"placement_id":555}}`,
+			expectedErrs:   []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'unknownbidder' has been disabled."}},
+		},
+		{
+			description:    "Valid Bidder + Disabled Bidder + First Party Data Context",
+			impExt:         json.RawMessage(`{"appnexus":{"placement_id":555},"unknownbidder":{"foo":"bar"},"context":{"data":{"keywords":"prebid server example"}}}`),
+			expectedImpExt: `{"appnexus":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`,
+			expectedErrs:   []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'unknownbidder' has been disabled."}},
+		},
+		{
+			description:    "Valid Bidder + First Party Data Context",
+			impExt:         json.RawMessage(`{"appnexus":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`),
+			expectedImpExt: `{"appnexus":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`,
+			expectedErrs:   []error{},
+		},
+		{
+			description:    "Valid Prebid Ext Bidder",
+			impExt:         json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555}}}}`),
+			expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555}}}}`,
+			expectedErrs:   []error{},
+			// request.imp[x].ext.prebid.bidder.{biddername} is only promoted/copied to request.ext.{biddername} if there is at least one disabled bidder.
+		},
+		{
+			description:    "Valid Prebid Ext Bidder + First Party Data Context",
+			impExt:         json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555}}} ,"context":{"data":{"keywords":"prebid server example"}}}`),
+			expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id":555}}},"context":{"data":{"keywords":"prebid server example"}}}`,
+			expectedErrs:   []error{},
+			// request.imp[x].ext.prebid.bidder.{biddername} is only promoted/copied to request.ext.{biddername} if there is at least one disabled bidder.
+		},
+		{
+			description:    "Valid Prebid Ext Bidder + Disabled Bidder",
+			impExt:         json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555},"unknownbidder":{"foo":"bar"}}}}`),
+			expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id": 555},"unknownbidder":{"foo":"bar"}}},"appnexus":{"placement_id":555}}`,
+			expectedErrs:   []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'unknownbidder' has been disabled."}},
+			// request.imp[x].ext.prebid.bidder.{biddername} disabled bidders are not removed. if there is a disabled bidder, the valid ones are promoted/copied to request.ext.{biddername}.
+		},
+		{
+			description:    "Valid Prebid Ext Bidder + Disabled Bidder + First Party Data Context",
+			impExt:         json.RawMessage(`{"prebid":{"bidder":{"appnexus":{"placement_id":555},"unknownbidder":{"foo":"bar"}}},"context":{"data":{"keywords":"prebid server example"}}}`),
+			expectedImpExt: `{"prebid":{"bidder":{"appnexus":{"placement_id": 555},"unknownbidder":{"foo":"bar"}}},"appnexus":{"placement_id":555},"context":{"data":{"keywords":"prebid server example"}}}`,
+			expectedErrs:   []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'unknownbidder' has been disabled."}},
+			// request.imp[x].ext.prebid.bidder.{biddername} disabled bidders are not removed. if there is a disabled bidder, the valid ones are promoted/copied to request.ext.{biddername}.
+		},
 	}
+
 	deps := &endpointDeps{
 		&nobidExchange{},
 		newParamsValidator(t),
@@ -1149,9 +1222,19 @@ func TestValidateImpExtDisabledBidder(t *testing.T) {
 		nil,
 		hardcodedResponseIPValidator{response: true},
 	}
-	errs := deps.validateImpExt(imp, nil, 0)
-	assert.JSONEq(t, `{"appnexus":{"placement_id":555}}`, string(imp.Ext))
-	assert.Equal(t, []error{&errortypes.BidderTemporarilyDisabled{Message: "The bidder 'unknownbidder' has been disabled."}}, errs)
+
+	for _, test := range testCases {
+		imp := &openrtb.Imp{Ext: test.impExt}
+
+		errs := deps.validateImpExt(imp, nil, 0)
+
+		if len(test.expectedImpExt) > 0 {
+			assert.JSONEq(t, test.expectedImpExt, string(imp.Ext))
+		} else {
+			assert.Empty(t, imp.Ext)
+		}
+		assert.Equal(t, test.expectedErrs, errs)
+	}
 }
 
 func validRequest(t *testing.T, filename string) string {
@@ -1901,8 +1984,7 @@ func (cf mockStoredReqFetcher) FetchRequests(ctx context.Context, requestIDs []s
 }
 
 var mockAccountData = map[string]json.RawMessage{
-	"valid_acct":    json.RawMessage(`{"disabled":false}`),
-	"disabled_acct": json.RawMessage(`{"disabled":true}`),
+	"valid_acct": json.RawMessage(`{"disabled":false}`),
 }
 
 type mockAccountFetcher struct {
@@ -1975,70 +2057,3 @@ type hardcodedResponseIPValidator struct {
 func (v hardcodedResponseIPValidator) IsValid(net.IP, iputil.IPVersion) bool {
 	return v.response
 }
-
-func TestGetAccount(t *testing.T) {
-	unknown := pbsmetrics.PublisherUnknown
-	testCases := []struct {
-		accountID string
-		// account_required
-		required bool
-		// account_defaults.disabled
-		disabled bool
-		// expected error, or nil if account should be found
-		err error
-	}{
-		// Blacklisted account is always rejected even in permissive setup
-		{accountID: "bad_acct", required: false, disabled: false, err: &errortypes.BlacklistedAcct{}},
-
-		// empty pubID
-		{accountID: unknown, required: false, disabled: false, err: nil},
-		{accountID: unknown, required: true, disabled: false, err: &errortypes.AcctRequired{}},
-		{accountID: unknown, required: false, disabled: true, err: &errortypes.BlacklistedAcct{}},
-		{accountID: unknown, required: true, disabled: true, err: &errortypes.AcctRequired{}},
-
-		// pubID given but is not a valid host account (does not exist)
-		{accountID: "doesnt_exist_acct", required: false, disabled: false, err: nil},
-		{accountID: "doesnt_exist_acct", required: true, disabled: false, err: nil},
-		{accountID: "doesnt_exist_acct", required: false, disabled: true, err: &errortypes.BlacklistedAcct{}},
-		{accountID: "doesnt_exist_acct", required: true, disabled: true, err: &errortypes.AcctRequired{}},
-
-		// pubID given and matches a valid host account with Disabled: false
-		{accountID: "valid_acct", required: false, disabled: false, err: nil},
-		{accountID: "valid_acct", required: true, disabled: false, err: nil},
-		{accountID: "valid_acct", required: false, disabled: true, err: nil},
-		{accountID: "valid_acct", required: true, disabled: true, err: nil},
-
-		// pubID given and matches a host account explicitly disabled (Disabled: true on account json)
-		{accountID: "disabled_acct", required: false, disabled: false, err: &errortypes.BlacklistedAcct{}},
-		{accountID: "disabled_acct", required: true, disabled: false, err: &errortypes.BlacklistedAcct{}},
-		{accountID: "disabled_acct", required: false, disabled: true, err: &errortypes.BlacklistedAcct{}},
-		{accountID: "disabled_acct", required: true, disabled: true, err: &errortypes.BlacklistedAcct{}},
-	}
-
-	for _, test := range testCases {
-		description := fmt.Sprintf(`ID=%s/required=%t/disabled=%t`, test.accountID, test.required, test.disabled)
-		t.Run(description, func(t *testing.T) {
-			deps := &endpointDeps{
-				cfg: &config.Configuration{
-					BlacklistedAcctMap: map[string]bool{"bad_acct": true},
-					AccountRequired:    test.required,
-					AccountDefaults:    config.Account{Disabled: test.disabled},
-				},
-				accounts: &mockAccountFetcher{},
-			}
-			assert.NoError(t, deps.cfg.MarshalAccountDefaults())
-
-			account, errors := deps.getAccount(context.Background(), test.accountID)
-
-			if test.err == nil {
-				assert.Empty(t, errors)
-				assert.Equal(t, test.accountID, account.ID, "account.ID must match requested ID")
-				assert.Equal(t, false, account.Disabled, "returned account must not be disabled")
-			} else {
-				assert.NotEmpty(t, errors, "expected errors but got success")
-				assert.Nil(t, account, "return account must be nil on error")
-				assert.IsType(t, test.err, errors[0], "error is of unexpected type")
-			}
-		})
-	}
-}
diff --git a/exchange/exchangetest/firstpartydata-imp-ext-multiple-bidders.json b/exchange/exchangetest/firstpartydata-imp-ext-multiple-bidders.json
new file mode 100644
index 00000000..8004c3c2
--- /dev/null
+++ b/exchange/exchangetest/firstpartydata-imp-ext-multiple-bidders.json
@@ -0,0 +1,173 @@
+{
+    "incomingRequest": {
+        "ortbRequest": {
+            "id": "some-request-id",
+            "site": {
+                "page": "test.somepage.com"
+            },
+            "imp": [{
+                "id": "some-imp-id",
+                "banner": {
+                    "format": [{
+                        "w": 600,
+                        "h": 500
+                    }, {
+                        "w": 300,
+                        "h": 600
+                    }]
+                },
+                "ext": {
+                    "appnexus": {
+                        "placementId": 1
+                    },
+                    "rubicon": {
+                        "accountId": 1,
+                        "siteId": 2,
+                        "zoneId": 3
+                    },
+                    "context": {
+                        "data": {
+                            "keywords": "prebid server example"
+                        }
+                    }
+                }
+            }]
+        }
+    },
+    "outgoingRequests": {
+        "appnexus": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [{
+                        "id": "some-imp-id",
+                        "banner": {
+                            "format": [{
+                                "w": 600,
+                                "h": 500
+                            }, {
+                                "w": 300,
+                                "h": 600
+                            }]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "placementId": 1
+                            },
+                            "context": {
+                                "data": {
+                                    "keywords": "prebid server example"
+                                }
+                            }
+                        }
+                    }]
+                },
+                "bidAdjustment": 1.0
+            },
+            "mockResponse": {
+                "pbsSeatBid": {
+                    "pbsBids": [{
+                        "ortbBid": {
+                            "id": "apn-bid",
+                            "impid": "some-imp-id",
+                            "price": 0.3,
+                            "w": 200,
+                            "h": 500,
+                            "crid": "creative-1"
+                        },
+                        "bidType": "banner"
+                    }]
+                }
+            }
+        },
+        "rubicon": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [{
+                        "id": "some-imp-id",
+                        "banner": {
+                            "format": [{
+                                "w": 600,
+                                "h": 500
+                            }, {
+                                "w": 300,
+                                "h": 600
+                            }]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "accountId": 1,
+                                "siteId": 2,
+                                "zoneId": 3
+                            },
+                            "context": {
+                                "data": {
+                                    "keywords": "prebid server example"
+                                }
+                            }
+                        }
+                    }]
+                },
+                "bidAdjustment": 1.0
+            },
+            "mockResponse": {
+                "pbsSeatBid": {
+                    "pbsBids": [{
+                        "ortbBid": {
+                            "id": "rubi-bid",
+                            "impid": "some-imp-id",
+                            "price": 0.4,
+                            "w": 200,
+                            "h": 500,
+                            "crid": "creative-2"
+                        },
+                        "bidType": "banner"
+                    }]
+                }
+            }
+        }
+    },
+    "response": {
+        "bids": {
+            "id": "some-request-id",
+            "seatbid": [{
+                "seat": "appnexus",
+                "bid": [{
+                    "id": "apn-bid",
+                    "impid": "some-imp-id",
+                    "price": 0.3,
+                    "w": 200,
+                    "h": 500,
+                    "crid": "creative-1",
+                    "ext": {
+                        "prebid": {
+                            "type": "banner"
+                        }
+                    }
+                }]
+            }, {
+                "seat": "rubicon",
+                "bid": [{
+                    "id": "rubi-bid",
+                    "impid": "some-imp-id",
+                    "price": 0.4,
+                    "w": 200,
+                    "h": 500,
+                    "crid": "creative-2",
+                    "ext": {
+                        "prebid": {
+                            "type": "banner"
+                        }
+                    }
+                }]
+            }]
+        }
+    }
+}
\ No newline at end of file
diff --git a/exchange/exchangetest/firstpartydata-imp-ext-multiple-prebid-bidders.json b/exchange/exchangetest/firstpartydata-imp-ext-multiple-prebid-bidders.json
new file mode 100644
index 00000000..d62afccf
--- /dev/null
+++ b/exchange/exchangetest/firstpartydata-imp-ext-multiple-prebid-bidders.json
@@ -0,0 +1,179 @@
+{
+    "incomingRequest": {
+        "ortbRequest": {
+            "id": "some-request-id",
+            "site": {
+                "page": "test.somepage.com"
+            },
+            "imp": [{
+                "id": "some-imp-id",
+                "banner": {
+                    "format": [{
+                        "w": 600,
+                        "h": 500
+                    }, {
+                        "w": 300,
+                        "h": 600
+                    }]
+                },
+                "ext": {
+                    "prebid": {
+                        "bidder": {
+                            "appnexus": {
+                                "placementId": 1
+                            },
+                            "rubicon": {
+                                "accountId": 1,
+                                "siteId": 2,
+                                "zoneId": 3
+                            }
+                        }
+                    },
+                    "context": {
+                        "data": {
+                            "keywords": "prebid server example"
+                        }
+                    }
+                }
+            }]
+        }
+    },
+    "outgoingRequests": {
+        "appnexus": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [{
+                        "id": "some-imp-id",
+                        "banner": {
+                            "format": [{
+                                "w": 600,
+                                "h": 500
+                            }, {
+                                "w": 300,
+                                "h": 600
+                            }]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "placementId": 1
+                            },
+                            "prebid": {},
+                            "context": {
+                                "data": {
+                                    "keywords": "prebid server example"
+                                }
+                            }
+                        }
+                    }]
+                },
+                "bidAdjustment": 1.0
+            },
+            "mockResponse": {
+                "pbsSeatBid": {
+                    "pbsBids": [{
+                        "ortbBid": {
+                            "id": "apn-bid",
+                            "impid": "some-imp-id",
+                            "price": 0.3,
+                            "w": 200,
+                            "h": 500,
+                            "crid": "creative-1"
+                        },
+                        "bidType": "banner"
+                    }]
+                }
+            }
+        },
+        "rubicon": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [{
+                        "id": "some-imp-id",
+                        "banner": {
+                            "format": [{
+                                "w": 600,
+                                "h": 500
+                            }, {
+                                "w": 300,
+                                "h": 600
+                            }]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "accountId": 1,
+                                "siteId": 2,
+                                "zoneId": 3
+                            },
+                            "prebid": {},
+                            "context": {
+                                "data": {
+                                    "keywords": "prebid server example"
+                                }
+                            }
+                        }
+                    }]
+                },
+                "bidAdjustment": 1.0
+            },
+            "mockResponse": {
+                "pbsSeatBid": {
+                    "pbsBids": [{
+                        "ortbBid": {
+                            "id": "rubi-bid",
+                            "impid": "some-imp-id",
+                            "price": 0.4,
+                            "w": 200,
+                            "h": 500,
+                            "crid": "creative-2"
+                        },
+                        "bidType": "banner"
+                    }]
+                }
+            }
+        }
+    },
+    "response": {
+        "bids": {
+            "id": "some-request-id",
+            "seatbid": [{
+                "seat": "appnexus",
+                "bid": [{
+                    "id": "apn-bid",
+                    "impid": "some-imp-id",
+                    "price": 0.3,
+                    "w": 200,
+                    "h": 500,
+                    "crid": "creative-1",
+                    "ext": {
+                        "prebid": {
+                            "type": "banner"
+                        }
+                    }
+                }]
+            }, {
+                "seat": "rubicon",
+                "bid": [{
+                    "id": "rubi-bid",
+                    "impid": "some-imp-id",
+                    "price": 0.4,
+                    "w": 200,
+                    "h": 500,
+                    "crid": "creative-2",
+                    "ext": {
+                        "prebid": {
+                            "type": "banner"
+                        }
+                    }
+                }]
+            }]
+        }
+    }
+}
\ No newline at end of file
diff --git a/exchange/exchangetest/firstpartydata-imp-ext-one-bidder.json b/exchange/exchangetest/firstpartydata-imp-ext-one-bidder.json
new file mode 100644
index 00000000..6f0bab95
--- /dev/null
+++ b/exchange/exchangetest/firstpartydata-imp-ext-one-bidder.json
@@ -0,0 +1,103 @@
+{
+    "incomingRequest": {
+        "ortbRequest": {
+            "id": "some-request-id",
+            "site": {
+                "page": "test.somepage.com"
+            },
+            "imp": [{
+                "id": "some-imp-id",
+                "banner": {
+                    "format": [{
+                        "w": 600,
+                        "h": 500
+                    }, {
+                        "w": 300,
+                        "h": 600
+                    }]
+                },
+                "ext": {
+                    "appnexus": {
+                        "placementId": 1
+                    },
+                    "context": {
+                        "data": {
+                            "keywords": "prebid server example"
+                        }
+                    }
+                }
+            }]
+        }
+    },
+    "outgoingRequests": {
+        "appnexus": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [{
+                        "id": "some-imp-id",
+                        "banner": {
+                            "format": [{
+                                "w": 600,
+                                "h": 500
+                            }, {
+                                "w": 300,
+                                "h": 600
+                            }]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "placementId": 1
+                            },
+                            "context": {
+                                "data": {
+                                    "keywords": "prebid server example"
+                                }
+                            }
+                        }
+                    }]
+                },
+                "bidAdjustment": 1.0
+            },
+            "mockResponse": {
+                "pbsSeatBid": {
+                    "pbsBids": [{
+                        "ortbBid": {
+                            "id": "apn-bid",
+                            "impid": "some-imp-id",
+                            "price": 0.3,
+                            "w": 200,
+                            "h": 500,
+                            "crid": "creative-1"
+                        },
+                        "bidType": "banner"
+                    }]
+                }
+            }
+        }
+    },
+    "response": {
+        "bids": {
+            "id": "some-request-id",
+            "seatbid": [{
+                "seat": "appnexus",
+                "bid": [{
+                    "id": "apn-bid",
+                    "impid": "some-imp-id",
+                    "price": 0.3,
+                    "w": 200,
+                    "h": 500,
+                    "crid": "creative-1",
+                    "ext": {
+                        "prebid": {
+                            "type": "banner"
+                        }
+                    }
+                }]
+            }]
+        }
+    }
+}
\ No newline at end of file
diff --git a/exchange/exchangetest/firstpartydata-imp-ext-one-prebid-bidder.json b/exchange/exchangetest/firstpartydata-imp-ext-one-prebid-bidder.json
new file mode 100644
index 00000000..1610b9ea
--- /dev/null
+++ b/exchange/exchangetest/firstpartydata-imp-ext-one-prebid-bidder.json
@@ -0,0 +1,108 @@
+{
+    "incomingRequest": {
+        "ortbRequest": {
+            "id": "some-request-id",
+            "site": {
+                "page": "test.somepage.com"
+            },
+            "imp": [{
+                "id": "some-imp-id",
+                "banner": {
+                    "format": [{
+                        "w": 600,
+                        "h": 500
+                    }, {
+                        "w": 300,
+                        "h": 600
+                    }]
+                },
+                "ext": {
+                    "prebid": {
+                        "bidder": {
+                            "appnexus": {
+                                "placementId": 1
+                            }
+                        }
+                    },
+                    "context": {
+                        "data": {
+                            "keywords": "prebid server example"
+                        }
+                    }
+                }
+            }]
+        }
+    },
+    "outgoingRequests": {
+        "appnexus": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [{
+                        "id": "some-imp-id",
+                        "banner": {
+                            "format": [{
+                                "w": 600,
+                                "h": 500
+                            }, {
+                                "w": 300,
+                                "h": 600
+                            }]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "placementId": 1
+                            },
+                            "prebid": {},
+                            "context": {
+                                "data": {
+                                    "keywords": "prebid server example"
+                                }
+                            }
+                        }
+                    }]
+                },
+                "bidAdjustment": 1.0
+            },
+            "mockResponse": {
+                "pbsSeatBid": {
+                    "pbsBids": [{
+                        "ortbBid": {
+                            "id": "apn-bid",
+                            "impid": "some-imp-id",
+                            "price": 0.3,
+                            "w": 200,
+                            "h": 500,
+                            "crid": "creative-1"
+                        },
+                        "bidType": "banner"
+                    }]
+                }
+            }
+        }
+    },
+    "response": {
+        "bids": {
+            "id": "some-request-id",
+            "seatbid": [{
+                "seat": "appnexus",
+                "bid": [{
+                    "id": "apn-bid",
+                    "impid": "some-imp-id",
+                    "price": 0.3,
+                    "w": 200,
+                    "h": 500,
+                    "crid": "creative-1",
+                    "ext": {
+                        "prebid": {
+                            "type": "banner"
+                        }
+                    }
+                }]
+            }]
+        }
+    }
+}
\ No newline at end of file
diff --git a/openrtb_ext/bidders_test.go b/openrtb_ext/bidders_test.go
index d49b2323..9f05f526 100644
--- a/openrtb_ext/bidders_test.go
+++ b/openrtb_ext/bidders_test.go
@@ -61,3 +61,8 @@ func TestBidderListDoesNotDefineGeneral(t *testing.T) {
 	bidders := BidderList()
 	assert.NotContains(t, bidders, BidderNameGeneral)
 }
+
+func TestBidderListDoesNotDefineContext(t *testing.T) {
+	bidders := BidderList()
+	assert.NotContains(t, bidders, BidderNameContext)
+}
diff --git a/stored_requests/caches/cachestest/reliable.go b/stored_requests/caches/cachestest/reliable.go
index e08a20e9..7fbaf723 100644
--- a/stored_requests/caches/cachestest/reliable.go
+++ b/stored_requests/caches/cachestest/reliable.go
@@ -11,8 +11,6 @@ import (
 const (
 	reqCacheKey = "known-req"
 	reqCacheVal = `{"req":true}`
-	impCacheKey = "known-imp"
-	impCacheVal = `{"imp":true}`
 )
 
 // AssertCacheRobustness runs tests which can be used to validate any Cache that is 100% reliable.
@@ -20,84 +18,41 @@ const (
 //
 // The cacheSupplier should be a function which returns a new Cache (with no data inside) on every call.
 // This will be called from separate Goroutines to make sure that different tests don't conflict.
-func AssertCacheRobustness(t *testing.T, cacheSupplier func() stored_requests.Cache) {
+func AssertCacheRobustness(t *testing.T, cacheSupplier func() stored_requests.CacheJSON) {
 	t.Run("TestCacheMiss", cacheMissTester(cacheSupplier()))
 	t.Run("TestCacheHit", cacheHitTester(cacheSupplier()))
-	t.Run("TestCacheMixed", cacheMixedTester(cacheSupplier()))
-	t.Run("TestCacheOverlap", cacheOverlapTester(cacheSupplier()))
 	t.Run("TestCacheSaveInvalidate", cacheSaveInvalidateTester(cacheSupplier()))
 }
 
-func cacheMissTester(cache stored_requests.Cache) func(*testing.T) {
+func cacheMissTester(cache stored_requests.CacheJSON) func(*testing.T) {
 	return func(t *testing.T) {
-		storedReqs, storedImps := cache.Get(context.Background(), []string{"unknown"}, nil)
-		assertMapLength(t, 0, storedReqs)
-		assertMapLength(t, 0, storedImps)
+		storedData := cache.Get(context.Background(), []string{"unknown"})
+		assertMapLength(t, 0, storedData)
 	}
 }
 
-func cacheHitTester(cache stored_requests.Cache) func(*testing.T) {
+func cacheHitTester(cache stored_requests.CacheJSON) func(*testing.T) {
 	return func(t *testing.T) {
 		cache.Save(context.Background(), map[string]json.RawMessage{
 			reqCacheKey: json.RawMessage(reqCacheVal),
-		}, map[string]json.RawMessage{
-			impCacheKey: json.RawMessage(impCacheVal),
 		})
-		reqData, impData := cache.Get(context.Background(), []string{reqCacheKey}, []string{impCacheKey})
-		if len(reqData) != 1 {
-			t.Errorf("The cache should have returned the data.")
-		}
+		reqData := cache.Get(context.Background(), []string{reqCacheKey})
 		assertMapLength(t, 1, reqData)
 		assertHasValue(t, reqData, reqCacheKey, reqCacheVal)
-
-		assertMapLength(t, 1, impData)
-		assertHasValue(t, impData, impCacheKey, impCacheVal)
-	}
-}
-
-func cacheMixedTester(cache stored_requests.Cache) func(*testing.T) {
-	return func(t *testing.T) {
-		cache.Save(context.Background(), map[string]json.RawMessage{
-			reqCacheKey: json.RawMessage(reqCacheVal),
-		}, nil)
-		reqData, impData := cache.Get(context.Background(), []string{reqCacheKey, "unknown-req"}, nil)
-		assertMapLength(t, 1, reqData)
-		assertHasValue(t, reqData, reqCacheKey, reqCacheVal)
-		assertMapLength(t, 0, impData)
 	}
 }
 
-func cacheOverlapTester(cache stored_requests.Cache) func(*testing.T) {
-	commonKey := "id"
+func cacheSaveInvalidateTester(cache stored_requests.CacheJSON) func(*testing.T) {
 	return func(t *testing.T) {
 		cache.Save(context.Background(), map[string]json.RawMessage{
-			commonKey: json.RawMessage(reqCacheVal),
-		}, map[string]json.RawMessage{
-			commonKey: json.RawMessage(impCacheVal),
-		})
-		reqData, impData := cache.Get(context.Background(), []string{commonKey}, []string{commonKey})
-		assertMapLength(t, 1, reqData)
-		assertHasValue(t, reqData, commonKey, reqCacheVal)
-		assertMapLength(t, 1, impData)
-		assertHasValue(t, impData, commonKey, impCacheVal)
-	}
-}
-
-func cacheSaveInvalidateTester(cache stored_requests.Cache) func(*testing.T) {
-	return func(t *testing.T) {
-		cache.Save(context.Background(), map[string]json.RawMessage{
-			reqCacheKey: json.RawMessage(reqCacheVal),
-		}, map[string]json.RawMessage{
 			reqCacheKey: json.RawMessage(reqCacheVal),
 		})
-		reqData, impData := cache.Get(context.Background(), []string{reqCacheKey}, []string{reqCacheKey})
+		reqData := cache.Get(context.Background(), []string{reqCacheKey})
 		assertMapLength(t, 1, reqData)
-		assertMapLength(t, 1, impData)
 
-		cache.Invalidate(context.Background(), []string{reqCacheKey}, []string{reqCacheKey})
-		reqData, impData = cache.Get(context.Background(), []string{reqCacheKey}, []string{reqCacheKey})
+		cache.Invalidate(context.Background(), []string{reqCacheKey})
+		reqData = cache.Get(context.Background(), []string{reqCacheKey})
 		assertMapLength(t, 0, reqData)
-		assertMapLength(t, 0, impData)
 	}
 }
 
diff --git a/stored_requests/caches/memory/cache_test.go b/stored_requests/caches/memory/cache_test.go
index ba4703ef..b89bd5af 100644
--- a/stored_requests/caches/memory/cache_test.go
+++ b/stored_requests/caches/memory/cache_test.go
@@ -7,52 +7,34 @@ import (
 	"strconv"
 	"testing"
 
-	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/stored_requests"
 	"github.com/prebid/prebid-server/stored_requests/caches/cachestest"
 )
 
 func TestLRURobustness(t *testing.T) {
-	cachestest.AssertCacheRobustness(t, func() stored_requests.Cache {
-		return NewCache(&config.InMemoryCache{
-			RequestCacheSize: 256 * 1024,
-			ImpCacheSize:     256 * 1024,
-			TTL:              -1,
-		})
+	cachestest.AssertCacheRobustness(t, func() stored_requests.CacheJSON {
+		return NewCache(256*1024, -1, "TestData")
 	})
 }
 
 func TestUnboundedRobustness(t *testing.T) {
-	cachestest.AssertCacheRobustness(t, func() stored_requests.Cache {
-		return NewCache(&config.InMemoryCache{
-			RequestCacheSize: 0,
-			ImpCacheSize:     0,
-			TTL:              -1,
-		})
+	cachestest.AssertCacheRobustness(t, func() stored_requests.CacheJSON {
+		return NewCache(0, -1, "TestData")
 	})
 }
 
 func TestRaceLRUConcurrency(t *testing.T) {
-	cache := NewCache(&config.InMemoryCache{
-		RequestCacheSize: 256 * 1024,
-		ImpCacheSize:     256 * 1024,
-		TTL:              -1,
-	})
-
+	cache := NewCache(256*1024, -1, "TestData")
 	doRaceTest(t, cache)
 }
 
 func TestRaceUnboundedConcurrency(t *testing.T) {
-	cache := NewCache(&config.InMemoryCache{
-		RequestCacheSize: 0,
-		ImpCacheSize:     0,
-		TTL:              -1,
-	})
+	cache := NewCache(0, -1, "TestData")
 
 	doRaceTest(t, cache)
 }
 
-func doRaceTest(t *testing.T, cache stored_requests.Cache) {
+func doRaceTest(t *testing.T, cache stored_requests.CacheJSON) {
 	done := make(chan struct{})
 	sets := [][]int{rand.Perm(100), rand.Perm(100), rand.Perm(100)}
 
@@ -70,26 +52,26 @@ func doRaceTest(t *testing.T, cache stored_requests.Cache) {
 	}
 }
 
-func readLots(cache stored_requests.Cache, done chan<- struct{}, reads []int) {
+func readLots(cache stored_requests.CacheJSON, done chan<- struct{}, reads []int) {
 	var s struct{}
 	for _, i := range reads {
-		cache.Get(context.Background(), sliceForVal(i), sliceForVal(-i))
+		cache.Get(context.Background(), sliceForVal(i))
 	}
 	done <- s
 }
 
-func writeLots(cache stored_requests.Cache, done chan<- struct{}, writes []int) {
+func writeLots(cache stored_requests.CacheJSON, done chan<- struct{}, writes []int) {
 	var s struct{}
 	for _, i := range writes {
-		cache.Save(context.Background(), mapForVal(i), mapForVal(-i))
+		cache.Save(context.Background(), mapForVal(i))
 	}
 	done <- s
 }
 
-func invalidateLots(cache stored_requests.Cache, done chan<- struct{}, invalidates []int) {
+func invalidateLots(cache stored_requests.CacheJSON, done chan<- struct{}, invalidates []int) {
 	var s struct{}
 	for _, i := range invalidates {
-		cache.Invalidate(context.Background(), sliceForVal(i), sliceForVal(-i))
+		cache.Invalidate(context.Background(), sliceForVal(i))
 	}
 	done <- s
 }
diff --git a/stored_requests/config/config_test.go b/stored_requests/config/config_test.go
index 4c3943ea..af6f4a4f 100644
--- a/stored_requests/config/config_test.go
+++ b/stored_requests/config/config_test.go
@@ -102,8 +102,8 @@ func TestNewHTTPEvents(t *testing.T) {
 
 func TestNewEmptyCache(t *testing.T) {
 	cache := newCache(&config.StoredRequests{InMemoryCache: config.InMemoryCache{Type: "none"}})
-	cache.Save(context.Background(), map[string]json.RawMessage{"foo": json.RawMessage("true")}, nil)
-	reqs, _ := cache.Get(context.Background(), []string{"foo"}, nil)
+	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.")
 	}
@@ -117,8 +117,8 @@ func TestNewInMemoryCache(t *testing.T) {
 			ImpCacheSize:     100,
 		},
 	})
-	cache.Save(context.Background(), map[string]json.RawMessage{"foo": json.RawMessage("true")}, nil)
-	reqs, _ := cache.Get(context.Background(), []string{"foo"}, nil)
+	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.")
 	}
diff --git a/stored_requests/events/api/api_test.go b/stored_requests/events/api/api_test.go
index 64cf68b0..d2db4557 100644
--- a/stored_requests/events/api/api_test.go
+++ b/stored_requests/events/api/api_test.go
@@ -9,22 +9,21 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/stored_requests"
 	"github.com/prebid/prebid-server/stored_requests/caches/memory"
 	"github.com/prebid/prebid-server/stored_requests/events"
 )
 
 func TestGoodRequests(t *testing.T) {
-	cache := memory.NewCache(&config.InMemoryCache{
-		RequestCacheSize: 256 * 1024,
-		ImpCacheSize:     256 * 1024,
-		TTL:              -1,
-	})
-
+	cache := stored_requests.Cache{
+		Requests: memory.NewCache(256*1024, -1, "Requests"),
+		Imps:     memory.NewCache(256*1024, -1, "Imps"),
+	}
 	id := "1"
 	config := fmt.Sprintf(`{"id": "%s"}`, id)
 	initialValue := map[string]json.RawMessage{id: json.RawMessage(config)}
-	cache.Save(context.Background(), initialValue, initialValue)
+	cache.Requests.Save(context.Background(), initialValue)
+	cache.Imps.Save(context.Background(), initialValue)
 
 	apiEvents, endpoint := NewEventsAPI()
 
@@ -51,7 +50,8 @@ func TestGoodRequests(t *testing.T) {
 	}
 
 	<-updateOccurred
-	reqData, impData := cache.Get(context.Background(), []string{id}, []string{id})
+	reqData := cache.Requests.Get(context.Background(), []string{id})
+	impData := cache.Imps.Get(context.Background(), []string{id})
 	assertHasValue(t, reqData, id, config)
 	assertHasValue(t, impData, id, config)
 
@@ -66,18 +66,17 @@ func TestGoodRequests(t *testing.T) {
 	}
 
 	<-invalidateOccurred
-	reqData, impData = cache.Get(context.Background(), []string{id}, []string{id})
+	reqData = cache.Requests.Get(context.Background(), []string{id})
+	impData = cache.Imps.Get(context.Background(), []string{id})
 	assertMapLength(t, 0, reqData)
 	assertMapLength(t, 0, impData)
 }
 
 func TestBadRequests(t *testing.T) {
-	cache := memory.NewCache(&config.InMemoryCache{
-		RequestCacheSize: 256 * 1024,
-		ImpCacheSize:     256 * 1024,
-		TTL:              -1,
-	})
-
+	cache := stored_requests.Cache{
+		Requests: memory.NewCache(256*1024, -1, "Requests"),
+		Imps:     memory.NewCache(256*1024, -1, "Imps"),
+	}
 	apiEvents, endpoint := NewEventsAPI()
 	listener := events.SimpleEventListener()
 	go listener.Listen(cache, apiEvents)
diff --git a/stored_requests/events/events_test.go b/stored_requests/events/events_test.go
index 240a6975..74263d14 100644
--- a/stored_requests/events/events_test.go
+++ b/stored_requests/events/events_test.go
@@ -7,7 +7,7 @@ import (
 	"reflect"
 	"testing"
 
-	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/stored_requests"
 	"github.com/prebid/prebid-server/stored_requests/caches/memory"
 )
 
@@ -16,12 +16,10 @@ func TestListen(t *testing.T) {
 		saves:         make(chan Save),
 		invalidations: make(chan Invalidation),
 	}
-
-	cache := memory.NewCache(&config.InMemoryCache{
-		RequestCacheSize: 256 * 1024,
-		ImpCacheSize:     256 * 1024,
-		TTL:              -1,
-	})
+	cache := stored_requests.Cache{
+		Requests: memory.NewCache(256*1024, -1, "Requests"),
+		Imps:     memory.NewCache(256*1024, -1, "Imps"),
+	}
 
 	// create channels to synchronize
 	saveOccurred := make(chan struct{})
@@ -42,7 +40,8 @@ func TestListen(t *testing.T) {
 		Requests: data,
 		Imps:     data,
 	}
-	cache.Save(context.Background(), save.Requests, save.Imps)
+	cache.Requests.Save(context.Background(), save.Requests)
+	cache.Requests.Save(context.Background(), save.Imps)
 
 	config = fmt.Sprintf(`{"id": "%s", "updated": true}`, id)
 	data = map[string]json.RawMessage{id: json.RawMessage(config)}
@@ -54,7 +53,8 @@ func TestListen(t *testing.T) {
 	ep.saves <- save
 	<-saveOccurred
 
-	requestData, impData := cache.Get(context.Background(), idSlice, idSlice)
+	requestData := cache.Requests.Get(context.Background(), idSlice)
+	impData := cache.Imps.Get(context.Background(), idSlice)
 	if !reflect.DeepEqual(requestData, data) || !reflect.DeepEqual(impData, data) {
 		t.Error("Update failed")
 	}
@@ -67,7 +67,8 @@ func TestListen(t *testing.T) {
 	ep.invalidations <- invalidation
 	<-invalidateOccurred
 
-	requestData, impData = cache.Get(context.Background(), idSlice, idSlice)
+	requestData = cache.Requests.Get(context.Background(), idSlice)
+	impData = cache.Imps.Get(context.Background(), idSlice)
 	if len(requestData) > 0 || len(impData) > 0 {
 		t.Error("Invalidate failed")
 	}
diff --git a/stored_requests/fetcher_test.go b/stored_requests/fetcher_test.go
index 1928d116..396ba3d0 100644
--- a/stored_requests/fetcher_test.go
+++ b/stored_requests/fetcher_test.go
@@ -12,25 +12,27 @@ import (
 	"github.com/stretchr/testify/mock"
 )
 
-func setupFetcherWithCacheDeps() (*mockCache, *mockFetcher, AllFetcher, *pbsmetrics.MetricsEngineMock) {
-	cache := &mockCache{}
+func setupFetcherWithCacheDeps() (*mockCache, *mockCache, *mockFetcher, AllFetcher, *pbsmetrics.MetricsEngineMock) {
+	reqCache := &mockCache{}
+	impCache := &mockCache{}
 	metricsEngine := &pbsmetrics.MetricsEngineMock{}
 	fetcher := &mockFetcher{}
-	afetcherWithCache := WithCache(fetcher, cache, metricsEngine)
+	afetcherWithCache := WithCache(fetcher, Cache{reqCache, impCache}, metricsEngine)
 
-	return cache, fetcher, afetcherWithCache, metricsEngine
+	return reqCache, impCache, fetcher, afetcherWithCache, metricsEngine
 }
 
 func TestPerfectCache(t *testing.T) {
-	cache, fetcher, aFetcherWithCache, metricsEngine := setupFetcherWithCacheDeps()
+	reqCache, impCache, fetcher, aFetcherWithCache, metricsEngine := setupFetcherWithCacheDeps()
 	impIDs := []string{"known"}
 	reqIDs := []string{"req-id"}
 	ctx := context.Background()
 
-	cache.On("Get", ctx, reqIDs, impIDs).Return(
+	reqCache.On("Get", ctx, reqIDs).Return(
 		map[string]json.RawMessage{
 			"req-id": json.RawMessage(`{"req":true}`),
-		},
+		})
+	impCache.On("Get", ctx, impIDs).Return(
 		map[string]json.RawMessage{
 			"known": json.RawMessage(`{}`),
 		})
@@ -41,7 +43,8 @@ func TestPerfectCache(t *testing.T) {
 
 	reqData, impData, errs := aFetcherWithCache.FetchRequests(ctx, reqIDs, impIDs)
 
-	cache.AssertExpectations(t)
+	reqCache.AssertExpectations(t)
+	impCache.AssertExpectations(t)
 	fetcher.AssertExpectations(t)
 	metricsEngine.AssertExpectations(t)
 	assert.JSONEq(t, `{"req":true}`, string(reqData["req-id"]), "Fetch requests should fetch the right request data")
@@ -50,15 +53,16 @@ func TestPerfectCache(t *testing.T) {
 }
 
 func TestImperfectCache(t *testing.T) {
-	cache, fetcher, aFetcherWithCache, metricsEngine := setupFetcherWithCacheDeps()
+	reqCache, impCache, fetcher, aFetcherWithCache, metricsEngine := setupFetcherWithCacheDeps()
 	impIDs := []string{"cached", "uncached"}
 	ctx := context.Background()
 
-	cache.On("Get", ctx, []string(nil), impIDs).Return(
-		map[string]json.RawMessage{},
+	impCache.On("Get", ctx, impIDs).Return(
 		map[string]json.RawMessage{
 			"cached": json.RawMessage(`true`),
 		})
+	reqCache.On("Get", ctx, []string(nil)).Return(
+		map[string]json.RawMessage{})
 
 	fetcher.On("FetchRequests", ctx, []string{}, []string{"uncached"}).Return(
 		map[string]json.RawMessage{},
@@ -67,11 +71,11 @@ func TestImperfectCache(t *testing.T) {
 		},
 		[]error{},
 	)
-	cache.On("Save", ctx,
-		map[string]json.RawMessage{},
+	impCache.On("Save", ctx,
 		map[string]json.RawMessage{
 			"uncached": json.RawMessage(`false`),
 		})
+	reqCache.On("Save", ctx, map[string]json.RawMessage{})
 	metricsEngine.On("RecordStoredReqCacheResult", pbsmetrics.CacheHit, 0)
 	metricsEngine.On("RecordStoredReqCacheResult", pbsmetrics.CacheMiss, 0)
 	metricsEngine.On("RecordStoredImpCacheResult", pbsmetrics.CacheHit, 1)
@@ -79,7 +83,7 @@ func TestImperfectCache(t *testing.T) {
 
 	reqData, impData, errs := aFetcherWithCache.FetchRequests(ctx, nil, impIDs)
 
-	cache.AssertExpectations(t)
+	impCache.AssertExpectations(t)
 	fetcher.AssertExpectations(t)
 	metricsEngine.AssertExpectations(t)
 	assert.Len(t, reqData, 0, "Fetch requests should return nil if no request IDs were passed")
@@ -89,14 +93,15 @@ func TestImperfectCache(t *testing.T) {
 }
 
 func TestMissingData(t *testing.T) {
-	cache, fetcher, aFetcherWithCache, metricsEngine := setupFetcherWithCacheDeps()
+	reqCache, impCache, fetcher, aFetcherWithCache, metricsEngine := setupFetcherWithCacheDeps()
 	impIDs := []string{"unknown"}
 	ctx := context.Background()
 
-	cache.On("Get", ctx, []string(nil), impIDs).Return(
-		map[string]json.RawMessage{},
+	impCache.On("Get", ctx, impIDs).Return(
 		map[string]json.RawMessage{},
 	)
+	reqCache.On("Get", ctx, []string(nil)).Return(
+		map[string]json.RawMessage{})
 	fetcher.On("FetchRequests", ctx, []string{}, impIDs).Return(
 		map[string]json.RawMessage{},
 		map[string]json.RawMessage{},
@@ -104,8 +109,10 @@ func TestMissingData(t *testing.T) {
 			errors.New("Data not found"),
 		},
 	)
-	cache.On("Save", ctx,
+	impCache.On("Save", ctx,
 		map[string]json.RawMessage{},
+	)
+	reqCache.On("Save", ctx,
 		map[string]json.RawMessage{},
 	)
 	metricsEngine.On("RecordStoredReqCacheResult", pbsmetrics.CacheHit, 0)
@@ -115,7 +122,8 @@ func TestMissingData(t *testing.T) {
 
 	reqData, impData, errs := aFetcherWithCache.FetchRequests(ctx, nil, impIDs)
 
-	cache.AssertExpectations(t)
+	reqCache.AssertExpectations(t)
+	impCache.AssertExpectations(t)
 	fetcher.AssertExpectations(t)
 	metricsEngine.AssertExpectations(t)
 	assert.Len(t, errs, 1, "FetchRequests for missing data should return an error")
@@ -125,15 +133,16 @@ func TestMissingData(t *testing.T) {
 
 // Prevents #311
 func TestCacheSaves(t *testing.T) {
-	cache, fetcher, aFetcherWithCache, metricsEngine := setupFetcherWithCacheDeps()
+	reqCache, impCache, fetcher, aFetcherWithCache, metricsEngine := setupFetcherWithCacheDeps()
 	impIDs := []string{"abc", "abc"}
 	ctx := context.Background()
 
-	cache.On("Get", ctx, []string(nil), impIDs).Return(
-		map[string]json.RawMessage{},
+	impCache.On("Get", ctx, impIDs).Return(
 		map[string]json.RawMessage{
 			"abc": json.RawMessage(`{}`),
 		})
+	reqCache.On("Get", ctx, []string(nil)).Return(
+		map[string]json.RawMessage{})
 	metricsEngine.On("RecordStoredReqCacheResult", pbsmetrics.CacheHit, 0)
 	metricsEngine.On("RecordStoredReqCacheResult", pbsmetrics.CacheMiss, 0)
 	metricsEngine.On("RecordStoredImpCacheResult", pbsmetrics.CacheHit, 2)
@@ -141,7 +150,7 @@ func TestCacheSaves(t *testing.T) {
 
 	_, impData, errs := aFetcherWithCache.FetchRequests(ctx, nil, []string{"abc", "abc"})
 
-	cache.AssertExpectations(t)
+	impCache.AssertExpectations(t)
 	fetcher.AssertExpectations(t)
 	metricsEngine.AssertExpectations(t)
 	assert.Len(t, impData, 1, "FetchRequests should return data only once for duplicate requests")
@@ -154,38 +163,34 @@ func TestComposedCache(t *testing.T) {
 	c2 := &mockCache{}
 	c3 := &mockCache{}
 	c4 := &mockCache{}
-	cache := ComposedCache{c1, c2, c3, c4}
+	impCache := &mockCache{}
+	cache := Cache{
+		Requests: ComposedCache{c1, c2, c3, c4},
+		Imps:     impCache,
+	}
 	metricsEngine := &pbsmetrics.MetricsEngineMock{}
 	fetcher := &mockFetcher{}
 	aFetcherWithCache := WithCache(fetcher, cache, metricsEngine)
-	impIDs := []string{"1", "2", "3"}
 	reqIDs := []string{"1", "2", "3"}
+	impIDs := []string{}
 	ctx := context.Background()
 
-	c1.On("Get", ctx, reqIDs, impIDs).Return(
-		map[string]json.RawMessage{
-			"1": json.RawMessage(`{"id": "1"}`),
-		},
+	c1.On("Get", ctx, reqIDs).Return(
 		map[string]json.RawMessage{
 			"1": json.RawMessage(`{"id": "1"}`),
 		})
-	c2.On("Get", ctx, []string{"2", "3"}, []string{"2", "3"}).Return(
-		map[string]json.RawMessage{
-			"2": json.RawMessage(`{"id": "2"}`),
-		},
+	c2.On("Get", ctx, []string{"2", "3"}).Return(
 		map[string]json.RawMessage{
 			"2": json.RawMessage(`{"id": "2"}`),
 		})
-	c3.On("Get", ctx, []string{"3"}, []string{"3"}).Return(
-		map[string]json.RawMessage{
-			"3": json.RawMessage(`{"id": "3"}`),
-		},
+	c3.On("Get", ctx, []string{"3"}).Return(
 		map[string]json.RawMessage{
 			"3": json.RawMessage(`{"id": "3"}`),
 		})
+	impCache.On("Get", ctx, []string{}).Return(map[string]json.RawMessage{})
 	metricsEngine.On("RecordStoredReqCacheResult", pbsmetrics.CacheHit, 3)
 	metricsEngine.On("RecordStoredReqCacheResult", pbsmetrics.CacheMiss, 0)
-	metricsEngine.On("RecordStoredImpCacheResult", pbsmetrics.CacheHit, 3)
+	metricsEngine.On("RecordStoredImpCacheResult", pbsmetrics.CacheHit, 0)
 	metricsEngine.On("RecordStoredImpCacheResult", pbsmetrics.CacheMiss, 0)
 
 	reqData, impData, errs := aFetcherWithCache.FetchRequests(ctx, reqIDs, impIDs)
@@ -193,14 +198,12 @@ func TestComposedCache(t *testing.T) {
 	c1.AssertExpectations(t)
 	c2.AssertExpectations(t)
 	c3.AssertExpectations(t)
+	impCache.AssertExpectations(t)
 	fetcher.AssertExpectations(t)
 	metricsEngine.AssertExpectations(t)
 	assert.Len(t, reqData, len(reqIDs), "FetchRequests should be able to return all request data from a composed cache")
 	assert.Len(t, impData, len(impIDs), "FetchRequests should be able to return all imp data from a composed cache")
 	assert.Len(t, errs, 0, "FetchRequests shouldn't return an error when trying to use a composed cache")
-	assert.JSONEq(t, `{"id": "1"}`, string(impData["1"]), "FetchRequests should fetch the right imp data")
-	assert.JSONEq(t, `{"id": "2"}`, string(impData["2"]), "FetchRequests should fetch the right imp data")
-	assert.JSONEq(t, `{"id": "3"}`, string(impData["3"]), "FetchRequests should fetch the right imp data")
 	assert.JSONEq(t, `{"id": "1"}`, string(reqData["1"]), "FetchRequests should fetch the right req data")
 	assert.JSONEq(t, `{"id": "2"}`, string(reqData["2"]), "FetchRequests should fetch the right req data")
 	assert.JSONEq(t, `{"id": "3"}`, string(reqData["3"]), "FetchRequests should fetch the right req data")
@@ -228,15 +231,15 @@ type mockCache struct {
 	mock.Mock
 }
 
-func (c *mockCache) Get(ctx context.Context, requestIDs []string, impIDs []string) (map[string]json.RawMessage, map[string]json.RawMessage) {
-	args := c.Called(ctx, requestIDs, impIDs)
-	return args.Get(0).(map[string]json.RawMessage), args.Get(1).(map[string]json.RawMessage)
+func (c *mockCache) Get(ctx context.Context, ids []string) map[string]json.RawMessage {
+	args := c.Called(ctx, ids)
+	return args.Get(0).(map[string]json.RawMessage)
 }
 
-func (c *mockCache) Save(ctx context.Context, storedRequests map[string]json.RawMessage, storedImps map[string]json.RawMessage) {
-	c.Called(ctx, storedRequests, storedImps)
+func (c *mockCache) Save(ctx context.Context, data map[string]json.RawMessage) {
+	c.Called(ctx, data)
 }
 
-func (c *mockCache) Invalidate(ctx context.Context, requestIDs []string, impIDs []string) {
-	c.Called(ctx, requestIDs, impIDs)
+func (c *mockCache) Invalidate(ctx context.Context, ids []string) {
+	c.Called(ctx, ids)
 }