Repository: prebid/prebid-server
Pull request #1479 (2020-09-04 17:48:13)

Pass Through First Party Context Data
PBS-Go does not yet support the First Party Data feature (https://github.com/prebid/prebid-server/issues/879), but there is a conflict with how we map bidder data and it's causing issues for some publishers. We'll need to fix this eventually, so why not now?

The First Party Data feature allows for context to be provided on an imp to be shared among all bidders, such as:
```
{
    imp: [
        ...
        ext: {
            context: {
                data: {
                    // everyone sees this data
                    ADUNIT SPECIFIC CONTEXT DATA
                }
            }
         }
    ]
}
```

However, PBS-Go expects all fields within the "ext" to either be bidder data or the prebid section, such as:
```
{
    imp: [
        ...
        ext: {
            appnexus: {
                        placementId: 1
            },
            context: {
                data: {
                    // everyone sees this data
                    ADUNIT SPECIFIC CONTEXT DATA
                }
            }
        }
    ]
}
```

We attempt to validate that context is a valid bidder and return an error. We already have a special case for the prebid section. This PR also adds a special case for the context section, to pass it down to all bid requests.


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/adapters/brightroll/brightroll.go b/adapters/brightroll/brightroll.go
index 0ae95dd3..83b253a0 100644
--- a/adapters/brightroll/brightroll.go
+++ b/adapters/brightroll/brightroll.go
@@ -6,6 +6,7 @@ import (
 	"net/http"
 	"strconv"
 
+	"github.com/golang/glog"
 	"github.com/mxmCherry/openrtb"
 	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/errortypes"
@@ -13,7 +14,20 @@ import (
 )
 
 type BrightrollAdapter struct {
-	URI string
+	URI       string
+	extraInfo ExtraInfo
+}
+
+type ExtraInfo struct {
+	Accounts []Account `json:"accounts"`
+}
+
+type Account struct {
+	ID       string   `json:"id"`
+	Badv     []string `json:"badv"`
+	Bcat     []string `json:"bcat"`
+	Battr    []int8   `json:"battr"`
+	BidFloor float64  `json:"bidfloor"`
 }
 
 func (a *BrightrollAdapter) MakeRequests(requestIn *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
@@ -55,6 +69,23 @@ func (a *BrightrollAdapter) MakeRequests(requestIn *openrtb.BidRequest, reqInfo
 		errors = append(errors, err)
 		return nil, errors
 	}
+
+	var account *Account
+	for _, a := range a.extraInfo.Accounts {
+		if a.ID == brightrollExt.Publisher {
+			account = &a
+			break
+		}
+	}
+
+	if account == nil {
+		err = &errortypes.BadInput{
+			Message: "Invalid publisher",
+		}
+		errors = append(errors, err)
+		return nil, errors
+	}
+
 	validImpExists := false
 	for i := 0; i < len(request.Imp); i++ {
 		//Brightroll supports only banner and video impressions as of now
@@ -65,9 +96,9 @@ func (a *BrightrollAdapter) MakeRequests(requestIn *openrtb.BidRequest, reqInfo
 				bannerCopy.W = &(firstFormat.W)
 				bannerCopy.H = &(firstFormat.H)
 			}
-			if brightrollExt.Publisher == "adthrive" {
-				bannerCopy.BAttr = getBlockedCreativetypesForAdThrive()
 
+			if len(account.Battr) > 0 {
+				bannerCopy.BAttr = getBlockedCreativetypes(account.Battr)
 			}
 			request.Imp[i].Banner = &bannerCopy
 			validImpExists = true
@@ -75,10 +106,15 @@ func (a *BrightrollAdapter) MakeRequests(requestIn *openrtb.BidRequest, reqInfo
 			validImpExists = true
 			if brightrollExt.Publisher == "adthrive" {
 				videoCopy := *request.Imp[i].Video
-				videoCopy.BAttr = getBlockedCreativetypesForAdThrive()
+				if len(account.Battr) > 0 {
+					videoCopy.BAttr = getBlockedCreativetypes(account.Battr)
+				}
 				request.Imp[i].Video = &videoCopy
 			}
 		}
+		if validImpExists && request.Imp[i].BidFloor == 0 && account.BidFloor > 0 {
+			request.Imp[i].BidFloor = account.BidFloor
+		}
 	}
 	if !validImpExists {
 		err := &errortypes.BadInput{
@@ -90,8 +126,12 @@ func (a *BrightrollAdapter) MakeRequests(requestIn *openrtb.BidRequest, reqInfo
 
 	request.AT = 1 //Defaulting to first price auction for all prebid requests
 
-	if brightrollExt.Publisher == "adthrive" {
-		request.BCat = getBlockedCategoriesForAdthrive()
+	if len(account.Bcat) > 0 {
+		request.BCat = account.Bcat
+	}
+
+	if len(account.Badv) > 0 {
+		request.BAdv = account.Badv
 	}
 	reqJSON, err := json.Marshal(request)
 	if err != nil {
@@ -159,13 +199,12 @@ func (a *BrightrollAdapter) MakeBids(internalRequest *openrtb.BidRequest, extern
 	return bidResponse, nil
 }
 
-//customized request, need following blocked categories
-func getBlockedCategoriesForAdthrive() []string {
-	return []string{"IAB8-5", "IAB8-18", "IAB15-1", "IAB7-30", "IAB14-1", "IAB22-1", "IAB3-7", "IAB7-3", "IAB14-3", "IAB11", "IAB11-1", "IAB11-2", "IAB11-3", "IAB11-4", "IAB11-5", "IAB23", "IAB23-1", "IAB23-2", "IAB23-3", "IAB23-4", "IAB23-5", "IAB23-6", "IAB23-7", "IAB23-8", "IAB23-9", "IAB23-10", "IAB7-39", "IAB9-30", "IAB7-44", "IAB25", "IAB25-1", "IAB25-2", "IAB25-3", "IAB25-4", "IAB25-5", "IAB25-6", "IAB25-7", "IAB26", "IAB26-1", "IAB26-2", "IAB26-3", "IAB26-4"}
-}
-
-func getBlockedCreativetypesForAdThrive() []openrtb.CreativeAttribute {
-	return []openrtb.CreativeAttribute{openrtb.CreativeAttribute(1), openrtb.CreativeAttribute(2), openrtb.CreativeAttribute(3), openrtb.CreativeAttribute(6), openrtb.CreativeAttribute(9), openrtb.CreativeAttribute(10)}
+func getBlockedCreativetypes(attr []int8) []openrtb.CreativeAttribute {
+	var creativeAttr []openrtb.CreativeAttribute
+	for i := 0; i < len(attr); i++ {
+		creativeAttr = append(creativeAttr, openrtb.CreativeAttribute(attr[i]))
+	}
+	return creativeAttr
 }
 
 //Adding header fields to request header
@@ -189,8 +228,18 @@ func getMediaTypeForImp(impId string, imps []openrtb.Imp) openrtb_ext.BidType {
 	return mediaType
 }
 
-func NewBrightrollBidder(endpoint string) *BrightrollAdapter {
-	return &BrightrollAdapter{
-		URI: endpoint,
+func NewBrightrollBidder(endpoint string, extraAdapterInfo string) *BrightrollAdapter {
+
+	var extraInfo ExtraInfo
+
+	if len(extraAdapterInfo) == 0 {
+		extraAdapterInfo = "{\"accounts\":[]}"
+	}
+	err := json.Unmarshal([]byte(extraAdapterInfo), &extraInfo)
+
+	if err != nil {
+		glog.Fatalf("Invalid Brightroll extra adapter info: " + err.Error())
+		return nil
 	}
+	return &BrightrollAdapter{URI: endpoint, extraInfo: extraInfo}
 }
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index bc0cd900..41c1c167 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -765,8 +765,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 +785,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 +820,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
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/exchange/adapter_map.go b/exchange/adapter_map.go
index d056de66..a160e87a 100755
--- a/exchange/adapter_map.go
+++ b/exchange/adapter_map.go
@@ -117,7 +117,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter
 		openrtb_ext.BidderAvocet:       avocet.NewAvocetAdapter(cfg.Adapters[string(openrtb_ext.BidderAvocet)].Endpoint),
 		openrtb_ext.BidderBeachfront:   beachfront.NewBeachfrontBidder(cfg.Adapters[string(openrtb_ext.BidderBeachfront)].Endpoint, cfg.Adapters[string(openrtb_ext.BidderBeachfront)].ExtraAdapterInfo),
 		openrtb_ext.BidderBeintoo:      beintoo.NewBeintooBidder(cfg.Adapters[string(openrtb_ext.BidderBeintoo)].Endpoint),
-		openrtb_ext.BidderBrightroll:   brightroll.NewBrightrollBidder(cfg.Adapters[string(openrtb_ext.BidderBrightroll)].Endpoint),
+		openrtb_ext.BidderBrightroll:   brightroll.NewBrightrollBidder(cfg.Adapters[string(openrtb_ext.BidderBrightroll)].Endpoint, cfg.Adapters[string(openrtb_ext.BidderBrightroll)].ExtraAdapterInfo),
 		openrtb_ext.BidderConsumable:   consumable.NewConsumableBidder(cfg.Adapters[string(openrtb_ext.BidderConsumable)].Endpoint),
 		openrtb_ext.BidderCpmstar:      cpmstar.NewCpmstarBidder(cfg.Adapters[string(openrtb_ext.BidderCpmstar)].Endpoint),
 		openrtb_ext.BidderDatablocks:   datablocks.NewDatablocksBidder(cfg.Adapters[string(openrtb_ext.BidderDatablocks)].Endpoint),
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/gdpr/gdpr.go b/gdpr/gdpr.go
index 04db8cb9..6d447beb 100644
--- a/gdpr/gdpr.go
+++ b/gdpr/gdpr.go
@@ -29,9 +29,10 @@ type Permissions interface {
 	AMPException() bool
 }
 
+// Versions of the GDPR TCF technical specification.
 const (
-	tCF1 uint8 = 1
-	tCF2 uint8 = 2
+	tcf1SpecVersion uint8 = 1
+	tcf2SpecVersion uint8 = 2
 )
 
 // NewPermissions gets an instance of the Permissions for use elsewhere in the project.
@@ -45,8 +46,8 @@ func NewPermissions(ctx context.Context, cfg config.GDPR, vendorIDs map[openrtb_
 		cfg:       cfg,
 		vendorIDs: vendorIDs,
 		fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
-			tCF1: newVendorListFetcher(ctx, cfg, client, vendorListURLMaker, tCF1),
-			tCF2: newVendorListFetcher(ctx, cfg, client, vendorListURLMaker, tCF2)},
+			tcf1SpecVersion: newVendorListFetcher(ctx, cfg, client, vendorListURLMaker, tcf1SpecVersion),
+			tcf2SpecVersion: newVendorListFetcher(ctx, cfg, client, vendorListURLMaker, tcf2SpecVersion)},
 	}
 }
 
diff --git a/gdpr/vendorlist-fetching.go b/gdpr/vendorlist-fetching.go
index 1442f81c..66a3f4ad 100644
--- a/gdpr/vendorlist-fetching.go
+++ b/gdpr/vendorlist-fetching.go
@@ -26,67 +26,83 @@ type saveVendors func(uint16, api.VendorList)
 //
 // Nothing in this file is exported. Public APIs can be found in gdpr.go
 
-func newVendorListFetcher(initCtx context.Context, cfg config.GDPR, client *http.Client, urlMaker func(uint16, uint8) string, TCFVer uint8) func(ctx context.Context, id uint16) (vendorlist.VendorList, error) {
-	var fallbackVL api.VendorList = nil
-
-	if TCFVer == tCF1 && len(cfg.TCF1.FallbackGVLPath) > 0 {
-		fallbackVL = loadFallbackGVL(cfg.TCF1.FallbackGVLPath)
-	}
-
-	// If we are not going to try fetching the GVL dynamically, we have a simple fetcher
-	if !cfg.TCF1.FetchGVL && TCFVer == tCF1 && fallbackVL != nil {
-		return func(ctx context.Context, id uint16) (vendorlist.VendorList, error) {
-			return fallbackVL, nil
+func newVendorListFetcher(initCtx context.Context, cfg config.GDPR, client *http.Client, urlMaker func(uint16, uint8) string, tcfSpecVersion uint8) func(ctx context.Context, id uint16) (vendorlist.VendorList, error) {
+	var fallback api.VendorList
+	if tcfSpecVersion == tcf1SpecVersion && len(cfg.TCF1.FallbackGVLPath) > 0 {
+		fallback = loadFallbackGVL(cfg.TCF1.FallbackGVLPath)
+	}
+
+	// If we are not going to try fetching the GVL dynamically, we have a simple fetcher.
+	if !cfg.TCF1.FetchGVL && tcfSpecVersion == tcf1SpecVersion {
+		if fallback != nil {
+			return func(ctx context.Context, vendorListVersion uint16) (vendorlist.VendorList, error) {
+				return fallback, nil
+			}
+		}
+		return func(ctx context.Context, vendorListVersion uint16) (vendorlist.VendorList, error) {
+			return nil, makeVendorListNotFoundError(vendorListVersion)
 		}
 	}
-	// These save and load functions can be used to store & retrieve lists from our cache.
-	save, load := newVendorListCache(fallbackVL)
 
-	withTimeout, cancel := context.WithTimeout(initCtx, cfg.Timeouts.InitTimeout())
-	defer cancel()
-	populateCache(withTimeout, client, urlMaker, save, TCFVer)
+	cacheSave, cacheLoad := newVendorListCache(fallback)
 
-	saveOneSometimes := newOccasionalSaver(cfg.Timeouts.ActiveTimeout(), TCFVer)
+	preloadContext, cancel := context.WithTimeout(initCtx, cfg.Timeouts.InitTimeout())
+	defer cancel()
+	preloadCache(preloadContext, client, urlMaker, cacheSave, tcfSpecVersion)
 
-	return func(ctx context.Context, id uint16) (vendorlist.VendorList, error) {
-		list := load(id)
-		if list != nil {
+	saveOneRateLimited := newOccasionalSaver(cfg.Timeouts.ActiveTimeout(), tcfSpecVersion)
+	return func(ctx context.Context, vendorListVersion uint16) (vendorlist.VendorList, error) {
+		// Attempt To Load From Cache
+		if list := cacheLoad(vendorListVersion); list != nil {
 			return list, nil
 		}
-		saveOneSometimes(ctx, client, urlMaker(id, TCFVer), save)
-		list = load(id)
-		if list != nil {
+
+		// Attempt To Download
+		// - May not add to cache immediately.
+		saveOneRateLimited(ctx, client, urlMaker(vendorListVersion, tcfSpecVersion), cacheSave)
+
+		// Attempt To Load From Cache Again
+		// - May have been added by the call to saveOneRateLimited.
+		if list := cacheLoad(vendorListVersion); list != nil {
 			return list, nil
 		}
-		if fallbackVL != nil {
-			return fallbackVL, nil
+
+		// Attempt To Use Hardcoded Fallback
+		if fallback != nil {
+			return fallback, nil
 		}
-		return nil, fmt.Errorf("gdpr vendor list version %d does not exist, or has not been loaded yet. Try again in a few minutes", id)
+
+		// Give Up
+		return nil, makeVendorListNotFoundError(vendorListVersion)
 	}
 }
 
-// populateCache saves all the known versions of the vendor list for future use.
-func populateCache(ctx context.Context, client *http.Client, urlMaker func(uint16, uint8) string, saver saveVendors, TCFVer uint8) {
-	latestVersion := saveOne(ctx, client, urlMaker(0, TCFVer), saver, TCFVer)
+func makeVendorListNotFoundError(vendorListVersion uint16) error {
+	return fmt.Errorf("gdpr vendor list version %d does not exist, or has not been loaded yet. Try again in a few minutes", vendorListVersion)
+}
+
+// preloadCache saves all the known versions of the vendor list for future use.
+func preloadCache(ctx context.Context, client *http.Client, urlMaker func(uint16, uint8) string, saver saveVendors, tcfSpecVersion uint8) {
+	latestVersion := saveOne(ctx, client, urlMaker(0, tcfSpecVersion), saver, tcfSpecVersion)
 
 	for i := uint16(1); i < latestVersion; i++ {
-		saveOne(ctx, client, urlMaker(i, TCFVer), saver, TCFVer)
+		saveOne(ctx, client, urlMaker(i, tcfSpecVersion), saver, tcfSpecVersion)
 	}
 }
 
 // Make a URL which can be used to fetch a given version of the Global Vendor List. If the version is 0,
 // this will fetch the latest version.
-func vendorListURLMaker(version uint16, TCFVer uint8) string {
-	if TCFVer == 2 {
-		if version == 0 {
+func vendorListURLMaker(vendorListVersion uint16, tcfSpecVersion uint8) string {
+	if tcfSpecVersion == tcf2SpecVersion {
+		if vendorListVersion == 0 {
 			return "https://vendorlist.consensu.org/v2/vendor-list.json"
 		}
-		return "https://vendorlist.consensu.org/v2/archives/vendor-list-v" + strconv.Itoa(int(version)) + ".json"
+		return "https://vendorlist.consensu.org/v2/archives/vendor-list-v" + strconv.Itoa(int(vendorListVersion)) + ".json"
 	}
-	if version == 0 {
+	if vendorListVersion == 0 {
 		return "https://vendorlist.consensu.org/vendorlist.json"
 	}
-	return "https://vendorlist.consensu.org/v-" + strconv.Itoa(int(version)) + "/vendorlist.json"
+	return "https://vendorlist.consensu.org/v-" + strconv.Itoa(int(vendorListVersion)) + "/vendorlist.json"
 }
 
 // newOccasionalSaver returns a wrapped version of saveOne() which only activates every few minutes.
@@ -94,22 +110,24 @@ func vendorListURLMaker(version uint16, TCFVer uint8) string {
 // The goal here is to update quickly when new versions of the VendorList are released, but not wreck
 // server performance if a bad CMP starts sending us malformed consent strings that advertize a version
 // that doesn't exist yet.
-func newOccasionalSaver(timeout time.Duration, TCFVer uint8) func(ctx context.Context, client *http.Client, url string, saver saveVendors) {
+func newOccasionalSaver(timeout time.Duration, tcfSpecVersion uint8) func(ctx context.Context, client *http.Client, url string, saver saveVendors) {
 	lastSaved := &atomic.Value{}
 	lastSaved.Store(time.Time{})
 
 	return func(ctx context.Context, client *http.Client, url string, saver saveVendors) {
 		now := time.Now()
-		if now.Sub(lastSaved.Load().(time.Time)).Minutes() > 10 {
+		timeSinceLastSave := now.Sub(lastSaved.Load().(time.Time))
+
+		if timeSinceLastSave.Minutes() > 10 {
 			withTimeout, cancel := context.WithTimeout(ctx, timeout)
 			defer cancel()
-			saveOne(withTimeout, client, url, saver, TCFVer)
+			saveOne(withTimeout, client, url, saver, tcfSpecVersion)
 			lastSaved.Store(now)
 		}
 	}
 }
 
-func saveOne(ctx context.Context, client *http.Client, url string, saver saveVendors, cTFVer uint8) uint16 {
+func saveOne(ctx context.Context, client *http.Client, url string, saver saveVendors, tcfSpecVersion uint8) uint16 {
 	req, err := http.NewRequest("GET", url, nil)
 	if err != nil {
 		glog.Errorf("Failed to build GET %s request. Cookie syncs may be affected: %v", url, err)
@@ -133,7 +151,7 @@ func saveOne(ctx context.Context, client *http.Client, url string, saver saveVen
 		return 0
 	}
 	var newList api.VendorList
-	if cTFVer == 2 {
+	if tcfSpecVersion == tcf2SpecVersion {
 		newList, err = vendorlist2.ParseEagerly(respBody)
 	} else {
 		newList, err = vendorlist.ParseEagerly(respBody)
@@ -147,14 +165,15 @@ func saveOne(ctx context.Context, client *http.Client, url string, saver saveVen
 	return newList.Version()
 }
 
-func newVendorListCache(fallbackVL api.VendorList) (save func(id uint16, list api.VendorList), load func(id uint16) api.VendorList) {
+func newVendorListCache(fallbackVL api.VendorList) (save func(vendorListVersion uint16, list api.VendorList), load func(vendorListVersion uint16) api.VendorList) {
 	cache := &sync.Map{}
 
-	save = func(id uint16, list api.VendorList) {
-		cache.Store(id, list)
+	save = func(vendorListVersion uint16, list api.VendorList) {
+		cache.Store(vendorListVersion, list)
 	}
-	load = func(id uint16) api.VendorList {
-		list, ok := cache.Load(id)
+
+	load = func(vendorListVersion uint16) api.VendorList {
+		list, ok := cache.Load(vendorListVersion)
 		if ok {
 			return list.(vendorlist.VendorList)
 		}
@@ -164,13 +183,14 @@ func newVendorListCache(fallbackVL api.VendorList) (save func(id uint16, list ap
 }
 
 func loadFallbackGVL(fallbackGVLPath string) vendorlist.VendorList {
-	fallbackVLbody, err := ioutil.ReadFile(fallbackGVLPath)
+	fallbackContents, err := ioutil.ReadFile(fallbackGVLPath)
 	if err != nil {
 		glog.Fatalf("Error reading from file %s: %v", fallbackGVLPath, err)
 	}
-	fallbackVL, err := vendorlist.ParseEagerly(fallbackVLbody)
+
+	fallback, err := vendorlist.ParseEagerly(fallbackContents)
 	if err != nil {
 		glog.Fatalf("Error processing default GVL from %s: %v", fallbackGVLPath, err)
 	}
-	return fallbackVL
+	return fallback
 }
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/adapters/brightroll/brightroll_test.go b/adapters/brightroll/brightroll_test.go
index 0a6c2c44..5f1c64fd 100644
--- a/adapters/brightroll/brightroll_test.go
+++ b/adapters/brightroll/brightroll_test.go
@@ -1,11 +1,37 @@
 package brightroll
 
 import (
+	"github.com/stretchr/testify/assert"
 	"testing"
 
 	"github.com/prebid/prebid-server/adapters/adapterstest"
 )
 
+func TestEmptyConfig(t *testing.T) {
+	output := NewBrightrollBidder("http://test-bid.ybp.yahoo.com/bid/appnexuspbs", "")
+	ex := ExtraInfo{
+		Accounts: []Account{},
+	}
+	expected := &BrightrollAdapter{
+		URI:       "http://test-bid.ybp.yahoo.com/bid/appnexuspbs",
+		extraInfo: ex,
+	}
+	assert.Equal(t, expected, output, "")
+}
+
+func TestNonEmptyConfig(t *testing.T) {
+	output := NewBrightrollBidder("http://test-bid.ybp.yahoo.com/bid/appnexuspbs", "{\"accounts\": [{\"id\": \"test\",\"bidfloor\":0.1}]}")
+	ex := ExtraInfo{
+		Accounts: []Account{{ID: "test", BidFloor: 0.1}},
+	}
+
+	expected := &BrightrollAdapter{
+		URI:       "http://test-bid.ybp.yahoo.com/bid/appnexuspbs",
+		extraInfo: ex,
+	}
+	assert.Equal(t, expected, output, "")
+}
+
 func TestJsonSamples(t *testing.T) {
-	adapterstest.RunJSONBidderTest(t, "brightrolltest", NewBrightrollBidder("http://test-bid.ybp.yahoo.com/bid/appnexuspbs"))
+	adapterstest.RunJSONBidderTest(t, "brightrolltest", NewBrightrollBidder("http://test-bid.ybp.yahoo.com/bid/appnexuspbs", "{\"accounts\": [{\"id\": \"adthrive\",\"badv\": [], \"bcat\": [\"IAB8-5\",\"IAB8-18\"],\"battr\": [1,2,3], \"bidfloor\":0.0}]}"))
 }
diff --git a/adapters/brightroll/brightrolltest/exemplary/banner-native-audio.json b/adapters/brightroll/brightrolltest/exemplary/banner-native-audio.json
index e8ef5b68..e67a1485 100644
--- a/adapters/brightroll/brightrolltest/exemplary/banner-native-audio.json
+++ b/adapters/brightroll/brightrolltest/exemplary/banner-native-audio.json
@@ -18,7 +18,7 @@
         },
         "ext": {
           "bidder": {
-            "publisher": "cafemom"
+            "publisher": "adthrive"
           }
         }
       },
@@ -30,7 +30,7 @@
         },
         "ext": {
           "bidder": {
-            "publisher": "cafemom"
+            "publisher": "adthrive"
           }
         }
       },
@@ -43,7 +43,7 @@
         },
         "ext": {
           "bidder": {
-            "publisher": "cafemom"
+            "publisher": "adthrive"
           }
         }
       }
@@ -52,14 +52,23 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=cafemom",
+        "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=adthrive",
         "body": {
           "id": "test-request-id",
           "at":1,
+          "bcat": [
+            "IAB8-5",
+            "IAB8-18"
+          ],
           "imp": [
             {
               "id": "test-imp-id",
               "banner": {
+                "battr": [
+                  1,
+                  2,
+                  3
+                ],
                 "format": [
                   {
                     "w": 300,
@@ -75,7 +84,7 @@
               },
               "ext": {
                 "bidder": {
-                  "publisher": "cafemom"
+                  "publisher": "adthrive"
                 }
               }
             },
@@ -87,7 +96,7 @@
               },
               "ext": {
                 "bidder": {
-                  "publisher": "cafemom"
+                  "publisher": "adthrive"
                 }
               }
             },
@@ -100,7 +109,7 @@
               },
               "ext": {
                 "bidder": {
-                  "publisher": "cafemom"
+                  "publisher": "adthrive"
                 }
               }
             }
diff --git a/adapters/brightroll/brightrolltest/exemplary/banner-video-native.json b/adapters/brightroll/brightrolltest/exemplary/banner-video-native.json
index 4df7ab5d..0fffbd2f 100644
--- a/adapters/brightroll/brightrolltest/exemplary/banner-video-native.json
+++ b/adapters/brightroll/brightrolltest/exemplary/banner-video-native.json
@@ -18,7 +18,7 @@
         },
         "ext": {
           "bidder": {
-            "publisher": "cafemom"
+            "publisher": "adthrive"
           }
         }
       },
@@ -30,7 +30,7 @@
         },
         "ext": {
           "bidder": {
-            "publisher": "cafemom"
+            "publisher": "adthrive"
           }
         }
       },
@@ -44,7 +44,7 @@
         },
         "ext": {
           "bidder": {
-            "publisher": "cafemom"
+            "publisher": "adthrive"
           }
         }
       }
@@ -53,14 +53,23 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=cafemom",
+        "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=adthrive",
         "body": {
           "id": "test-request-id",
           "at":1,
+          "bcat": [
+            "IAB8-5",
+            "IAB8-18"
+          ],
           "imp": [
             {
               "id": "test-imp-id",
               "banner": {
+                "battr": [
+                  1,
+                  2,
+                  3
+                ],
                 "format": [
                   {
                     "w": 300,
@@ -76,7 +85,7 @@
               },
               "ext": {
                 "bidder": {
-                  "publisher": "cafemom"
+                  "publisher": "adthrive"
                 }
               }
             },
@@ -88,13 +97,18 @@
               },
               "ext": {
                 "bidder": {
-                  "publisher": "cafemom"
+                  "publisher": "adthrive"
                 }
               }
             },
             {
               "id": "test-imp-video-id",
               "video": {
+                "battr": [
+                  1,
+                  2,
+                  3
+                ],
                 "mimes": ["video/mp4"],
                 "protocols": [2, 5],
                 "w": 1024,
@@ -102,7 +116,7 @@
               },
               "ext": {
                 "bidder": {
-                  "publisher": "cafemom"
+                  "publisher": "adthrive"
                 }
               }
             }
diff --git a/adapters/brightroll/brightrolltest/exemplary/banner-video.json b/adapters/brightroll/brightrolltest/exemplary/banner-video.json
index 50053102..f58dd9b1 100644
--- a/adapters/brightroll/brightrolltest/exemplary/banner-video.json
+++ b/adapters/brightroll/brightrolltest/exemplary/banner-video.json
@@ -18,7 +18,7 @@
         },
         "ext": {
           "bidder": {
-            "publisher": "cafemom"
+            "publisher": "adthrive"
           }
         }
       },
@@ -32,7 +32,7 @@
         },
         "ext": {
           "bidder": {
-            "publisher": "cafemom"
+            "publisher": "adthrive"
           }
         }
       }
@@ -41,14 +41,23 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=cafemom",
+        "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=adthrive",
         "body": {
           "id": "test-request-id",
           "at":1,
+          "bcat": [
+            "IAB8-5",
+            "IAB8-18"
+          ],
           "imp": [
             {
               "id": "test-imp-id",
               "banner": {
+                "battr": [
+                  1,
+                  2,
+                  3
+                ],
                 "format": [
                   {
                     "w": 300,
@@ -64,13 +73,18 @@
               },
               "ext": {
                 "bidder": {
-                  "publisher": "cafemom"
+                  "publisher": "adthrive"
                 }
               }
             },
             {
               "id": "test-imp-video-id",
               "video": {
+                "battr": [
+                  1,
+                  2,
+                  3
+                ],
                 "mimes": ["video/mp4"],
                 "protocols": [2, 5],
                 "w": 1024,
@@ -78,7 +92,7 @@
               },
               "ext": {
                 "bidder": {
-                  "publisher": "cafemom"
+                  "publisher": "adthrive"
                 }
               }
             }
diff --git a/adapters/brightroll/brightrolltest/exemplary/simple-banner.json b/adapters/brightroll/brightrolltest/exemplary/simple-banner.json
index 96fa0cbc..66bc06cf 100644
--- a/adapters/brightroll/brightrolltest/exemplary/simple-banner.json
+++ b/adapters/brightroll/brightrolltest/exemplary/simple-banner.json
@@ -18,7 +18,7 @@
         },
         "ext": {
           "bidder": {
-            "publisher": "cafemom"
+            "publisher": "adthrive"
           }
         }
       }
@@ -28,14 +28,23 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=cafemom",
+        "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=adthrive",
         "body": {
           "id": "test-request-id",
           "at":1,
+          "bcat": [
+            "IAB8-5",
+            "IAB8-18"
+          ],
           "imp": [
             {
               "id": "test-imp-id",
               "banner": {
+                "battr": [
+                  1,
+                  2,
+                  3
+                ],
                 "format": [
                   {
                     "w": 300,
@@ -51,7 +60,7 @@
               },
               "ext": {
                 "bidder": {
-                  "publisher": "cafemom"
+                  "publisher": "adthrive"
                 }
               }
             }
diff --git a/adapters/brightroll/brightrolltest/exemplary/simple-video.json b/adapters/brightroll/brightrolltest/exemplary/simple-video.json
index f2466b2f..3f9b8091 100644
--- a/adapters/brightroll/brightrolltest/exemplary/simple-video.json
+++ b/adapters/brightroll/brightrolltest/exemplary/simple-video.json
@@ -12,7 +12,7 @@
         },
         "ext":{
           "bidder":{
-            "publisher": "cafemom"
+            "publisher": "adthrive"
           }
         }
       }
@@ -22,14 +22,23 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=cafemom",
+        "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=adthrive",
         "body": {
           "id": "test-request-id",
           "at":1,
+          "bcat": [
+            "IAB8-5",
+            "IAB8-18"
+          ],
           "imp": [
             {
               "id": "test-imp-id",
               "video": {
+                "battr": [
+                  1,
+                  2,
+                  3
+                ],
                 "mimes": ["video/mp4"],
                 "protocols": [2, 5],
                 "w": 1024,
@@ -37,7 +46,7 @@
               },
               "ext": {
                 "bidder": {
-                  "publisher": "cafemom"
+                  "publisher": "adthrive"
                 }
               }
             }
diff --git a/adapters/brightroll/brightrolltest/exemplary/valid-extension.json b/adapters/brightroll/brightrolltest/exemplary/valid-extension.json
index 970b4ade..da38c62b 100644
--- a/adapters/brightroll/brightrolltest/exemplary/valid-extension.json
+++ b/adapters/brightroll/brightrolltest/exemplary/valid-extension.json
@@ -12,7 +12,7 @@
         },
         "ext":{
           "bidder":{
-            "publisher": "cafemom"
+            "publisher": "adthrive"
            }
         }
       }
@@ -22,14 +22,23 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=cafemom",
+        "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=adthrive",
         "body": {
           "id": "test-request-id",
           "at":1,
+          "bcat": [
+            "IAB8-5",
+            "IAB8-18"
+          ],
           "imp": [
             {
               "id": "test-imp-id",
               "video": {
+                "battr": [
+                  1,
+                  2,
+                  3
+                ],
                 "mimes": ["video/mp4"],
                 "protocols": [2, 5],
                 "w": 1024,
@@ -37,7 +46,7 @@
               },
               "ext": {
                 "bidder": {
-                  "publisher": "cafemom"
+                  "publisher": "adthrive"
                 }
               }
             }
diff --git a/adapters/brightroll/brightrolltest/exemplary/video-and-audio.json b/adapters/brightroll/brightrolltest/exemplary/video-and-audio.json
index 9f24a471..d3295e5b 100644
--- a/adapters/brightroll/brightrolltest/exemplary/video-and-audio.json
+++ b/adapters/brightroll/brightrolltest/exemplary/video-and-audio.json
@@ -12,7 +12,7 @@
         },
         "ext": {
           "bidder": {
-            "publisher": "cafemom"
+            "publisher": "adthrive"
           }
         }
       },
@@ -25,7 +25,7 @@
         },
         "ext": {
           "bidder": {
-            "publisher": "cafemom"
+            "publisher": "adthrive"
           }
         }
       }
@@ -34,14 +34,23 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=cafemom",
+        "uri": "http://test-bid.ybp.yahoo.com/bid/appnexuspbs?publisher=adthrive",
         "body": {
           "id": "test-request-id",
           "at":1,
+          "bcat": [
+            "IAB8-5",
+            "IAB8-18"
+          ],
           "imp": [
             {
               "id": "test-imp-video-id",
               "video": {
+                "battr": [
+                  1,
+                  2,
+                  3
+                ],
                 "mimes": ["video/mp4"],
                 "protocols": [2, 5],
                 "w": 1024,
@@ -49,7 +58,7 @@
               },
               "ext": {
                 "bidder": {
-                  "publisher": "cafemom"
+                  "publisher": "adthrive"
                 }
               }
             },
@@ -62,7 +71,7 @@
               },
               "ext": {
                 "bidder": {
-                  "publisher": "cafemom"
+                  "publisher": "adthrive"
                 }
               }
             }
diff --git a/adapters/brightroll/brightrolltest/params/race/banner.json b/adapters/brightroll/brightrolltest/params/race/banner.json
index 91517e36..6eb4ec6a 100644
--- a/adapters/brightroll/brightrolltest/params/race/banner.json
+++ b/adapters/brightroll/brightrolltest/params/race/banner.json
@@ -1,3 +1,3 @@
 {
-  "publisher": "cafemom"
+  "publisher": "adthrive"
 }
diff --git a/adapters/brightroll/brightrolltest/params/race/video.json b/adapters/brightroll/brightrolltest/params/race/video.json
index 91517e36..6eb4ec6a 100644
--- a/adapters/brightroll/brightrolltest/params/race/video.json
+++ b/adapters/brightroll/brightrolltest/params/race/video.json
@@ -1,3 +1,3 @@
 {
-  "publisher": "cafemom"
+  "publisher": "adthrive"
 }
diff --git a/adapters/brightroll/brightrolltest/supplemental/invalid-imp.json b/adapters/brightroll/brightrolltest/supplemental/invalid-imp.json
index a6362c40..01beec71 100644
--- a/adapters/brightroll/brightrolltest/supplemental/invalid-imp.json
+++ b/adapters/brightroll/brightrolltest/supplemental/invalid-imp.json
@@ -3,7 +3,7 @@
     "id": "test-request-id",
     "ext": {
       "bidder": {
-        "publisher": "cafemom"
+        "publisher": "adthrive"
       }
     }
   },
diff --git a/adapters/brightroll/brightrolltest/supplemental/invalid-publisher.json b/adapters/brightroll/brightrolltest/supplemental/invalid-publisher.json
new file mode 100644
index 00000000..da48108a
--- /dev/null
+++ b/adapters/brightroll/brightrolltest/supplemental/invalid-publisher.json
@@ -0,0 +1,34 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-missing-req-param-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "publisher":"test"
+          }
+        }
+
+      }
+    ]
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "Invalid publisher",
+      "comparison": "literal"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index 72da2a36..7dc244a2 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{
@@ -279,13 +289,14 @@ func TestRejectAccountRequired(t *testing.T) {
 		},
 		{
 			// Account is required, was provided, not blacklisted and is a valid account
-			dir:           "sample-requests/account-required",
-			file:          "valid-acct.json",
-			payloadGetter: getRequestPayload,
-			messageGetter: nilReturner,
-			expectedCode:  http.StatusOK,
-			aliased:       true,
-			accountReq:    true,
+			dir:                    "sample-requests/account-required",
+			file:                   "valid-acct.json",
+			payloadGetter:          getRequestPayload,
+			messageGetter:          nilReturner,
+			expectedCode:           http.StatusOK,
+			aliased:                true,
+			accountReq:             true,
+			accountDefaultDisabled: true,
 		},
 		{
 			// Account is required, was provided in request and is found in the  blacklisted accounts map
@@ -1126,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),
@@ -1148,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 {
@@ -1996,10 +2080,10 @@ func TestGetAccount(t *testing.T) {
 		{accountID: unknown, required: true, disabled: true, err: &errortypes.AcctRequired{}},
 
 		// pubID given but is not a valid host account (does not exist)
-		{accountID: "not_bad_acct", required: false, disabled: false, err: nil},
-		{accountID: "not_bad_acct", required: true, disabled: false, err: nil},
-		{accountID: "not_bad_acct", required: false, disabled: true, err: &errortypes.BlacklistedAcct{}},
-		{accountID: "not_bad_acct", required: true, disabled: true, err: &errortypes.AcctRequired{}},
+		{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},
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/gdpr/impl_test.go b/gdpr/impl_test.go
index 053e8753..d5114454 100644
--- a/gdpr/impl_test.go
+++ b/gdpr/impl_test.go
@@ -23,8 +23,8 @@ func TestNoConsentButAllowByDefault(t *testing.T) {
 		},
 		vendorIDs: nil,
 		fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
-			tCF1: failedListFetcher,
-			tCF2: failedListFetcher,
+			tcf1SpecVersion: failedListFetcher,
+			tcf2SpecVersion: failedListFetcher,
 		},
 	}
 	allowSync, err := perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderAppnexus, "")
@@ -43,8 +43,8 @@ func TestNoConsentAndRejectByDefault(t *testing.T) {
 		},
 		vendorIDs: nil,
 		fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
-			tCF1: failedListFetcher,
-			tCF2: failedListFetcher,
+			tcf1SpecVersion: failedListFetcher,
+			tcf2SpecVersion: failedListFetcher,
 		},
 	}
 	allowSync, err := perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderAppnexus, "")
@@ -56,12 +56,11 @@ func TestNoConsentAndRejectByDefault(t *testing.T) {
 }
 
 func TestAllowedSyncs(t *testing.T) {
-	vendorListData := mockVendorListData(t, 1, map[uint16]*purposes{
-		2: {
-			purposes: []int{1},
-		},
-		3: {
-			purposes: []int{1},
+	vendorListData := tcf1MarshalVendorList(tcf1VendorList{
+		VendorListVersion: 1,
+		Vendors: []tcf1Vendor{
+			{ID: 2, Purposes: []int{1}},
+			{ID: 3, Purposes: []int{1}},
 		},
 	})
 	perms := permissionsImpl{
@@ -73,10 +72,10 @@ func TestAllowedSyncs(t *testing.T) {
 			openrtb_ext.BidderPubmatic: 3,
 		},
 		fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
-			tCF1: listFetcher(map[uint16]vendorlist.VendorList{
+			tcf1SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
 				1: parseVendorListData(t, vendorListData),
 			}),
-			tCF2: listFetcher(map[uint16]vendorlist.VendorList{
+			tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
 				1: parseVendorListData(t, vendorListData),
 			}),
 		},
@@ -92,12 +91,11 @@ func TestAllowedSyncs(t *testing.T) {
 }
 
 func TestProhibitedPurposes(t *testing.T) {
-	vendorListData := mockVendorListData(t, 1, map[uint16]*purposes{
-		2: {
-			purposes: []int{1}, // cookie reads/writes
-		},
-		3: {
-			purposes: []int{3}, // ad personalization
+	vendorListData := tcf1MarshalVendorList(tcf1VendorList{
+		VendorListVersion: 1,
+		Vendors: []tcf1Vendor{
+			{ID: 2, Purposes: []int{1}}, // cookie reads/writes
+			{ID: 3, Purposes: []int{3}}, // ad personalization
 		},
 	})
 	perms := permissionsImpl{
@@ -109,10 +107,10 @@ func TestProhibitedPurposes(t *testing.T) {
 			openrtb_ext.BidderPubmatic: 3,
 		},
 		fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
-			tCF1: listFetcher(map[uint16]vendorlist.VendorList{
+			tcf1SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
 				1: parseVendorListData(t, vendorListData),
 			}),
-			tCF2: listFetcher(map[uint16]vendorlist.VendorList{
+			tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
 				1: parseVendorListData(t, vendorListData),
 			}),
 		},
@@ -128,12 +126,11 @@ func TestProhibitedPurposes(t *testing.T) {
 }
 
 func TestProhibitedVendors(t *testing.T) {
-	vendorListData := mockVendorListData(t, 1, map[uint16]*purposes{
-		2: {
-			purposes: []int{1}, // cookie reads/writes
-		},
-		3: {
-			purposes: []int{3}, // ad personalization
+	vendorListData := tcf1MarshalVendorList(tcf1VendorList{
+		VendorListVersion: 1,
+		Vendors: []tcf1Vendor{
+			{ID: 2, Purposes: []int{1}}, // cookie reads/writes
+			{ID: 3, Purposes: []int{3}}, // ad personalization
 		},
 	})
 	perms := permissionsImpl{
@@ -145,10 +142,10 @@ func TestProhibitedVendors(t *testing.T) {
 			openrtb_ext.BidderPubmatic: 3,
 		},
 		fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
-			tCF1: listFetcher(map[uint16]vendorlist.VendorList{
+			tcf1SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
 				1: parseVendorListData(t, vendorListData),
 			}),
-			tCF2: listFetcher(map[uint16]vendorlist.VendorList{
+			tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
 				1: parseVendorListData(t, vendorListData),
 			}),
 		},
@@ -169,8 +166,8 @@ func TestMalformedConsent(t *testing.T) {
 			HostVendorID: 2,
 		},
 		fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
-			tCF1: listFetcher(nil),
-			tCF2: listFetcher(nil),
+			tcf1SpecVersion: listFetcher(nil),
+			tcf2SpecVersion: listFetcher(nil),
 		},
 	}
 
@@ -180,12 +177,11 @@ func TestMalformedConsent(t *testing.T) {
 }
 
 func TestAllowPersonalInfo(t *testing.T) {
-	vendorListData := mockVendorListData(t, 1, map[uint16]*purposes{
-		2: {
-			purposes: []int{1}, // cookie reads/writes
-		},
-		3: {
-			purposes: []int{1, 3}, // ad personalization
+	vendorListData := tcf1MarshalVendorList(tcf1VendorList{
+		VendorListVersion: 1,
+		Vendors: []tcf1Vendor{
+			{ID: 2, Purposes: []int{1}},    // cookie reads/writes
+			{ID: 3, Purposes: []int{1, 3}}, // ad personalization
 		},
 	})
 	perms := permissionsImpl{
@@ -197,10 +193,10 @@ func TestAllowPersonalInfo(t *testing.T) {
 			openrtb_ext.BidderPubmatic: 3,
 		},
 		fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
-			tCF1: listFetcher(map[uint16]vendorlist.VendorList{
+			tcf1SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
 				1: parseVendorListData(t, vendorListData),
 			}),
-			tCF2: listFetcher(map[uint16]vendorlist.VendorList{
+			tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
 				1: parseVendorListData(t, vendorListData),
 			}),
 		},
@@ -222,24 +218,39 @@ func TestAllowPersonalInfo(t *testing.T) {
 	assertBoolsEqual(t, true, allowPI)
 }
 
-var tcf2BasicPurposes = map[uint16]*purposes{
-	2:  {purposes: []int{1}},       //cookie reads/writes
-	6:  {purposes: []int{1, 2, 4}}, // ad personalization
-	8:  {purposes: []int{1, 7}},
-	10: {purposes: []int{2, 4, 7}},
-	32: {purposes: []int{1, 2, 4, 7}},
-}
-var tcf2LegitInterests = map[uint16]*purposes{
-	6: {purposes: []int{7}},
-	8: {purposes: []int{2, 4}},
-}
-var tcf2SpecialPuproses = map[uint16]*purposes{
-	6:  {purposes: []int{1}},
-	10: {purposes: []int{1}},
-}
-var tcf2FlexPurposes = map[uint16]*purposes{
-	6: {purposes: []int{1, 2, 4, 7}},
+func buildTCF2VendorList34() tcf2VendorList {
+	return tcf2VendorList{
+		VendorListVersion: 2,
+		Vendors: map[string]*tcf2Vendor{
+			"2": {
+				ID:       2,
+				Purposes: []int{1},
+			},
+			"6": {
+				ID:               6,
+				Purposes:         []int{1, 2, 4},
+				LegIntPurposes:   []int{7},
+				SpecialPurposes:  []int{1},
+				FlexiblePurposes: []int{1, 2, 4, 7},
+			},
+			"8": {
+				ID:             8,
+				Purposes:       []int{1, 7},
+				LegIntPurposes: []int{2, 4},
+			},
+			"10": {
+				ID:              10,
+				Purposes:        []int{2, 4, 7},
+				SpecialPurposes: []int{1},
+			},
+			"32": {
+				ID:       32,
+				Purposes: []int{1, 2, 4, 7},
+			},
+		},
+	}
 }
+
 var tcf2Config = config.GDPR{
 	HostVendorID: 2,
 	TCF2: config.TCF2{
@@ -261,7 +272,7 @@ type tcf2TestDef struct {
 }
 
 func TestAllowPersonalInfoTCF2(t *testing.T) {
-	vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses)
+	vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34())
 	perms := permissionsImpl{
 		cfg: tcf2Config,
 		vendorIDs: map[openrtb_ext.BidderName]uint16{
@@ -270,8 +281,8 @@ func TestAllowPersonalInfoTCF2(t *testing.T) {
 			openrtb_ext.BidderRubicon:  8,
 		},
 		fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
-			tCF1: nil,
-			tCF2: listFetcher(map[uint16]vendorlist.VendorList{
+			tcf1SpecVersion: nil,
+			tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
 				34: parseVendorListDataV2(t, vendorListData),
 			}),
 		},
@@ -316,7 +327,7 @@ func TestAllowPersonalInfoTCF2(t *testing.T) {
 }
 
 func TestAllowPersonalInfoWhitelistTCF2(t *testing.T) {
-	vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses)
+	vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34())
 	perms := permissionsImpl{
 		cfg: tcf2Config,
 		vendorIDs: map[openrtb_ext.BidderName]uint16{
@@ -325,8 +336,8 @@ func TestAllowPersonalInfoWhitelistTCF2(t *testing.T) {
 			openrtb_ext.BidderRubicon:  8,
 		},
 		fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
-			tCF1: nil,
-			tCF2: listFetcher(map[uint16]vendorlist.VendorList{
+			tcf1SpecVersion: nil,
+			tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
 				34: parseVendorListDataV2(t, vendorListData),
 			}),
 		},
@@ -338,11 +349,10 @@ func TestAllowPersonalInfoWhitelistTCF2(t *testing.T) {
 	assert.EqualValuesf(t, true, allowPI, "AllowPI failure")
 	assert.EqualValuesf(t, true, allowGeo, "AllowGeo failure")
 	assert.EqualValuesf(t, true, allowID, "AllowID failure")
-
 }
 
 func TestAllowPersonalInfoTCF2PubRestrict(t *testing.T) {
-	vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses)
+	vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34())
 	perms := permissionsImpl{
 		cfg: tcf2Config,
 		vendorIDs: map[openrtb_ext.BidderName]uint16{
@@ -351,8 +361,8 @@ func TestAllowPersonalInfoTCF2PubRestrict(t *testing.T) {
 			openrtb_ext.BidderRubicon:  8,
 		},
 		fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
-			tCF1: nil,
-			tCF2: listFetcher(map[uint16]vendorlist.VendorList{
+			tcf1SpecVersion: nil,
+			tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
 				15: parseVendorListDataV2(t, vendorListData),
 			}),
 		},
@@ -397,7 +407,7 @@ func TestAllowPersonalInfoTCF2PubRestrict(t *testing.T) {
 }
 
 func TestAllowPersonalInfoTCF2PurposeOneTrue(t *testing.T) {
-	vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses)
+	vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34())
 	perms := permissionsImpl{
 		cfg: tcf2Config,
 		vendorIDs: map[openrtb_ext.BidderName]uint16{
@@ -406,8 +416,8 @@ func TestAllowPersonalInfoTCF2PurposeOneTrue(t *testing.T) {
 			openrtb_ext.BidderRubicon:  8,
 		},
 		fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
-			tCF1: nil,
-			tCF2: listFetcher(map[uint16]vendorlist.VendorList{
+			tcf1SpecVersion: nil,
+			tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
 				34: parseVendorListDataV2(t, vendorListData),
 			}),
 		},
@@ -453,7 +463,7 @@ func TestAllowPersonalInfoTCF2PurposeOneTrue(t *testing.T) {
 }
 
 func TestAllowPersonalInfoTCF2PurposeOneFalse(t *testing.T) {
-	vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses)
+	vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34())
 	perms := permissionsImpl{
 		cfg: tcf2Config,
 		vendorIDs: map[openrtb_ext.BidderName]uint16{
@@ -462,8 +472,8 @@ func TestAllowPersonalInfoTCF2PurposeOneFalse(t *testing.T) {
 			openrtb_ext.BidderRubicon:  8,
 		},
 		fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
-			tCF1: nil,
-			tCF2: listFetcher(map[uint16]vendorlist.VendorList{
+			tcf1SpecVersion: nil,
+			tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
 				34: parseVendorListDataV2(t, vendorListData),
 			}),
 		},
@@ -510,7 +520,7 @@ func TestAllowPersonalInfoTCF2PurposeOneFalse(t *testing.T) {
 }
 
 func TestAllowSyncTCF2(t *testing.T) {
-	vendorListData := mockVendorListDataTCF2(t, 2, tcf2BasicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses)
+	vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34())
 	perms := permissionsImpl{
 		cfg: tcf2Config,
 		vendorIDs: map[openrtb_ext.BidderName]uint16{
@@ -519,8 +529,8 @@ func TestAllowSyncTCF2(t *testing.T) {
 			openrtb_ext.BidderRubicon:  8,
 		},
 		fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
-			tCF1: nil,
-			tCF2: listFetcher(map[uint16]vendorlist.VendorList{
+			tcf1SpecVersion: nil,
+			tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
 				34: parseVendorListDataV2(t, vendorListData),
 			}),
 		},
@@ -537,9 +547,9 @@ func TestAllowSyncTCF2(t *testing.T) {
 }
 
 func TestProhibitedPurposeSyncTCF2(t *testing.T) {
-	basicPurposes := tcf2BasicPurposes
-	basicPurposes[8] = &purposes{purposes: []int{7}}
-	vendorListData := mockVendorListDataTCF2(t, 2, basicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses)
+	tcf2VendorList34 := buildTCF2VendorList34()
+	tcf2VendorList34.Vendors["8"].Purposes = []int{7}
+	vendorListData := tcf2MarshalVendorList(tcf2VendorList34)
 	perms := permissionsImpl{
 		cfg: tcf2Config,
 		vendorIDs: map[openrtb_ext.BidderName]uint16{
@@ -548,15 +558,15 @@ func TestProhibitedPurposeSyncTCF2(t *testing.T) {
 			openrtb_ext.BidderRubicon:  8,
 		},
 		fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
-			tCF1: nil,
-			tCF2: listFetcher(map[uint16]vendorlist.VendorList{
+			tcf1SpecVersion: nil,
+			tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
 				34: parseVendorListDataV2(t, vendorListData),
 			}),
 		},
 	}
 	perms.cfg.HostVendorID = 8
 
-	// COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : TCF2 with full consensts to purposes and vendors 2, 6, 8
+	// COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : TCF2 with full consents to purposes for vendors 2, 6, 8
 	allowSync, err := perms.HostCookiesAllowed(context.Background(), "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA")
 	assert.NoErrorf(t, err, "Error processing HostCookiesAllowed")
 	assert.EqualValuesf(t, false, allowSync, "HostCookiesAllowed failure")
@@ -567,9 +577,7 @@ func TestProhibitedPurposeSyncTCF2(t *testing.T) {
 }
 
 func TestProhibitedVendorSyncTCF2(t *testing.T) {
-	basicPurposes := tcf2BasicPurposes
-	basicPurposes[10] = &purposes{purposes: []int{1}}
-	vendorListData := mockVendorListDataTCF2(t, 2, basicPurposes, tcf2LegitInterests, tcf2FlexPurposes, tcf2SpecialPuproses)
+	vendorListData := tcf2MarshalVendorList(buildTCF2VendorList34())
 	perms := permissionsImpl{
 		cfg: tcf2Config,
 		vendorIDs: map[openrtb_ext.BidderName]uint16{
@@ -579,20 +587,21 @@ func TestProhibitedVendorSyncTCF2(t *testing.T) {
 			openrtb_ext.BidderOpenx:    10,
 		},
 		fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
-			tCF1: nil,
-			tCF2: listFetcher(map[uint16]vendorlist.VendorList{
+			tcf1SpecVersion: nil,
+			tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
 				34: parseVendorListDataV2(t, vendorListData),
 			}),
 		},
 	}
 	perms.cfg.HostVendorID = 10
 
-	// COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : TCF2 with full consensts to purposes and vendors 2, 4, 6
+	// COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : TCF2 with full consents to purposes for vendors 2, 6, 8
 	allowSync, err := perms.HostCookiesAllowed(context.Background(), "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA")
 	assert.NoErrorf(t, err, "Error processing HostCookiesAllowed")
 	assert.EqualValuesf(t, false, allowSync, "HostCookiesAllowed failure")
 
-	allowSync, err = perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderRubicon, "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA")
+	// Permission disallowed due to consent string not including vendor 10.
+	allowSync, err = perms.BidderSyncAllowed(context.Background(), openrtb_ext.BidderOpenx, "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA")
 	assert.NoErrorf(t, err, "Error processing BidderSyncAllowed")
 	assert.EqualValuesf(t, false, allowSync, "BidderSyncAllowed failure")
 }
diff --git a/gdpr/vendorlist-fetching_test.go b/gdpr/vendorlist-fetching_test.go
index 484a0a54..e5ad8793 100644
--- a/gdpr/vendorlist-fetching_test.go
+++ b/gdpr/vendorlist-fetching_test.go
@@ -7,233 +7,697 @@ import (
 	"net/http/httptest"
 	"strconv"
 	"testing"
-	"time"
 
 	"github.com/stretchr/testify/assert"
 
+	"github.com/prebid/go-gdpr/consentconstants"
 	"github.com/prebid/prebid-server/config"
 )
 
-func TestVendorFetch(t *testing.T) {
-	vendorListOne := mockVendorListData(t, 1, map[uint16]*purposes{
-		32: {
-			purposes: []int{1, 2},
-		},
-	})
-	vendorListTwo := mockVendorListData(t, 2, map[uint16]*purposes{
-		32: {
-			purposes: []int{1, 2, 3},
+func TestTCF1FetcherInitialLoad(t *testing.T) {
+	// Loads two vendor lists during initialization by setting the latest vendor list version to 2.
+
+	server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{
+		vendorListLatestVersion: 2,
+		vendorLists: map[int]string{
+			1: tcf1VendorList1,
+			2: tcf1VendorList2,
 		},
-	})
-	server := httptest.NewServer(http.HandlerFunc(mockServer(2, map[int]string{
-		1: vendorListOne,
-		2: vendorListTwo,
 	})))
 	defer server.Close()
 
-	fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), 1)
-	list, err := fetcher(context.Background(), 1)
-	assertNilErr(t, err)
-	vendor := list.Vendor(32)
-	assertBoolsEqual(t, true, vendor.Purpose(1))
-	assertBoolsEqual(t, false, vendor.Purpose(3))
-	assertBoolsEqual(t, false, vendor.Purpose(4))
-
-	list, err = fetcher(context.Background(), 2)
-	assertNilErr(t, err)
-	vendor = list.Vendor(32)
-	assertBoolsEqual(t, true, vendor.Purpose(1))
-	assertBoolsEqual(t, true, vendor.Purpose(3))
-}
-
-func TestLazyFetch(t *testing.T) {
-	firstVendorList := mockVendorListData(t, 1, map[uint16]*purposes{
-		32: {
-			purposes: []int{1, 2},
-		},
-	})
-	secondVendorList := mockVendorListData(t, 2, map[uint16]*purposes{
-		3: {
-			purposes: []int{1},
-		},
-	})
-	server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{
-		1: firstVendorList,
-		2: secondVendorList,
+	testCases := []test{
+		{
+			description: "Fetch - No Fallback - Vendor List 1",
+			setup: testSetup{
+				enableTCF1Fetch:    true,
+				enableTCF1Fallback: false,
+				vendorListVersion:  1,
+			},
+			expected: vendorList1Expected,
+		},
+		{
+			description: "Fetch - No Fallback - Vendor List 2",
+			setup: testSetup{
+				enableTCF1Fetch:    true,
+				enableTCF1Fallback: false,
+				vendorListVersion:  2,
+			},
+			expected: vendorList2Expected,
+		},
+		{
+			description: "Fetch - Fallback - Vendor List 1",
+			setup: testSetup{
+				enableTCF1Fetch:    true,
+				enableTCF1Fallback: true,
+				vendorListVersion:  1,
+			},
+			expected: vendorList1Expected,
+		},
+		{
+			description: "Fetch - Fallback - Vendor List 2",
+			setup: testSetup{
+				enableTCF1Fetch:    true,
+				enableTCF1Fallback: true,
+				vendorListVersion:  2,
+			},
+			expected: vendorList2Expected,
+		},
+		{
+			description: "No Fetch - Fallback - Vendor List 1",
+			setup: testSetup{
+				enableTCF1Fetch:    false,
+				enableTCF1Fallback: true,
+				vendorListVersion:  1,
+			},
+			expected: vendorListFallbackExpected,
+		},
+		{
+			description: "No Fetch - Fallback - Vendor List 2",
+			setup: testSetup{
+				enableTCF1Fetch:    false,
+				enableTCF1Fallback: true,
+				vendorListVersion:  2,
+			},
+			expected: vendorListFallbackExpected,
+		},
+		{
+			description: "No Fetch - No Fallback - Vendor List 1",
+			setup: testSetup{
+				enableTCF1Fetch:    false,
+				enableTCF1Fallback: false,
+				vendorListVersion:  1,
+			},
+			expected: testExpected{
+				errorMessage: "gdpr vendor list version 1 does not exist, or has not been loaded yet. Try again in a few minutes",
+			},
+		},
+		{
+			description: "No Fetch - No Fallback - Vendor List 2",
+			setup: testSetup{
+				enableTCF1Fetch:    false,
+				enableTCF1Fallback: false,
+				vendorListVersion:  2,
+			},
+			expected: testExpected{
+				errorMessage: "gdpr vendor list version 2 does not exist, or has not been loaded yet. Try again in a few minutes",
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		runTest(t, test, tcf1SpecVersion, server)
+	}
+}
+
+func TestTCF2FetcherInitialLoad(t *testing.T) {
+	// Loads two vendor lists during initialization by setting the latest vendor list version to 2.
+	// Ensures TCF1 fetch settings have no effect on TCF2.
+
+	server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{
+		vendorListLatestVersion: 2,
+		vendorLists: map[int]string{
+			1: tcf2VendorList1,
+			2: tcf2VendorList2,
+		},
 	})))
 	defer server.Close()
 
-	fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), 1)
-	list, err := fetcher(context.Background(), 2)
-	assertNilErr(t, err)
+	testCases := []test{
+		{
+			description: "Fetch - No Fallback - Vendor List 1",
+			setup: testSetup{
+				enableTCF1Fetch:    true,
+				enableTCF1Fallback: false,
+				vendorListVersion:  1,
+			},
+			expected: vendorList1Expected,
+		},
+		{
+			description: "Fetch - No Fallback - Vendor List 2",
+			setup: testSetup{
+				enableTCF1Fetch:    true,
+				enableTCF1Fallback: false,
+				vendorListVersion:  2,
+			},
+			expected: vendorList2Expected,
+		},
+		{
+			description: "Fetch - Fallback - Vendor List 1",
+			setup: testSetup{
+				enableTCF1Fetch:    true,
+				enableTCF1Fallback: true,
+				vendorListVersion:  1,
+			},
+			expected: vendorList1Expected,
+		},
+		{
+			description: "Fetch - Fallback - Vendor List 2",
+			setup: testSetup{
+				enableTCF1Fetch:    true,
+				enableTCF1Fallback: true,
+				vendorListVersion:  2,
+			},
+			expected: vendorList2Expected,
+		},
+		{
+			description: "No Fetch - Fallback - Vendor List 1",
+			setup: testSetup{
+				enableTCF1Fetch:    false,
+				enableTCF1Fallback: true,
+				vendorListVersion:  1,
+			},
+			expected: vendorList1Expected,
+		},
+		{
+			description: "No Fetch - Fallback - Vendor List 2",
+			setup: testSetup{
+				enableTCF1Fetch:    false,
+				enableTCF1Fallback: true,
+				vendorListVersion:  2,
+			},
+			expected: vendorList2Expected,
+		},
+		{
+			description: "No Fetch - No Fallback - Vendor List 1",
+			setup: testSetup{
+				enableTCF1Fetch:    false,
+				enableTCF1Fallback: false,
+				vendorListVersion:  1,
+			},
+			expected: vendorList1Expected,
+		},
+		{
+			description: "No Fetch - No Fallback - Vendor List 2",
+			setup: testSetup{
+				enableTCF1Fetch:    false,
+				enableTCF1Fallback: false,
+				vendorListVersion:  2,
+			},
+			expected: vendorList2Expected,
+		},
+	}
 
-	vendor := list.Vendor(3)
-	assertBoolsEqual(t, true, vendor.Purpose(1))
-	assertBoolsEqual(t, false, vendor.Purpose(2))
+	for _, test := range testCases {
+		runTest(t, test, tcf2SpecVersion, server)
+	}
 }
 
-func TestInitialTimeout(t *testing.T) {
-	list := mockVendorListData(t, 1, map[uint16]*purposes{
-		32: {
-			purposes: []int{1, 2},
+func TestTCF1FetcherDynamicLoadListExists(t *testing.T) {
+	// Loads the first vendor list during initialization by setting the latest vendor list version to 1.
+	// All other vendor lists will be dynamically loaded.
+
+	server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{
+		vendorListLatestVersion: 1,
+		vendorLists: map[int]string{
+			1: tcf1VendorList1,
+			2: tcf1VendorList2,
 		},
-	})
-	server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{
-		1: list,
 	})))
 	defer server.Close()
 
-	ctx, cancel := context.WithDeadline(context.Background(), time.Time{})
-	defer cancel()
-	fetcher := newVendorListFetcher(ctx, testConfig(), server.Client(), testURLMaker(server), 1)
-	_, err := fetcher(context.Background(), 1) // This should do a lazy fetch, even though the initial call failed
-	assertNilErr(t, err)
+	testCases := []test{
+		{
+			description: "Fetch - No Fallback - Vendor List 2",
+			setup: testSetup{
+				enableTCF1Fetch:    true,
+				enableTCF1Fallback: false,
+				vendorListVersion:  2,
+			},
+			expected: vendorList2Expected,
+		},
+		{
+			description: "Fetch - Fallback - Vendor List 2",
+			setup: testSetup{
+				enableTCF1Fetch:    true,
+				enableTCF1Fallback: true,
+				vendorListVersion:  2,
+			},
+			expected: vendorList2Expected,
+		},
+		{
+			description: "No Fetch - Fallback - Vendor List 2",
+			setup: testSetup{
+				enableTCF1Fetch:    false,
+				enableTCF1Fallback: true,
+				vendorListVersion:  2,
+			},
+			expected: vendorListFallbackExpected,
+		},
+		{
+			description: "No Fetch - No Fallback - Vendor List 2",
+			setup: testSetup{
+				enableTCF1Fetch:    false,
+				enableTCF1Fallback: false,
+				vendorListVersion:  2,
+			},
+			expected: testExpected{
+				errorMessage: "gdpr vendor list version 2 does not exist, or has not been loaded yet. Try again in a few minutes",
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		runTest(t, test, tcf1SpecVersion, server)
+	}
 }
 
-func TestFetchThrottling(t *testing.T) {
-	vendorListTwo := mockVendorListData(t, 2, map[uint16]*purposes{
-		32: {
-			purposes: []int{1, 2},
-		},
-	})
-	vendorListThree := mockVendorListData(t, 3, map[uint16]*purposes{
-		32: {
-			purposes: []int{1, 2},
+func TestTCF2FetcherDynamicLoadListExists(t *testing.T) {
+	// Loads the first vendor list during initialization by setting the latest vendor list version to 1.
+	// All other vendor lists will be dynamically loaded.
+	// Ensures TCF1 fetch settings have no effect on TCF2.
+
+	server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{
+		vendorListLatestVersion: 1,
+		vendorLists: map[int]string{
+			1: tcf2VendorList1,
+			2: tcf2VendorList2,
 		},
-	})
-	server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{
-		1: "{}",
-		2: vendorListTwo,
-		3: vendorListThree,
 	})))
 	defer server.Close()
 
-	fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), 1)
-	_, err := fetcher(context.Background(), 2)
-	assertNilErr(t, err)
-	_, err = fetcher(context.Background(), 3)
-	assertErr(t, err, false)
+	testCases := []test{
+		{
+			description: "Fetch - No Fallback - Vendor List 2",
+			setup: testSetup{
+				enableTCF1Fetch:    true,
+				enableTCF1Fallback: false,
+				vendorListVersion:  2,
+			},
+			expected: vendorList2Expected,
+		},
+		{
+			description: "Fetch - Fallback - Vendor List 2",
+			setup: testSetup{
+				enableTCF1Fetch:    true,
+				enableTCF1Fallback: true,
+				vendorListVersion:  2,
+			},
+			expected: vendorList2Expected,
+		},
+		{
+			description: "No Fetch - Fallback - Vendor List 2",
+			setup: testSetup{
+				enableTCF1Fetch:    false,
+				enableTCF1Fallback: true,
+				vendorListVersion:  2,
+			},
+			expected: vendorList2Expected,
+		},
+		{
+			description: "No Fetch - No Fallback - Vendor List 2",
+			setup: testSetup{
+				enableTCF1Fetch:    false,
+				enableTCF1Fallback: false,
+				vendorListVersion:  2,
+			},
+			expected: vendorList2Expected,
+		},
+	}
+
+	for _, test := range testCases {
+		runTest(t, test, tcf2SpecVersion, server)
+	}
 }
 
-func TestMalformedVendorlistFetch(t *testing.T) {
-	server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{1: "{}"})))
+func TestTCF1FetcherDynamicLoadListDoesntExist(t *testing.T) {
+	// Loads the first vendor list during initialization by setting the latest vendor list version to 1.
+	// All other vendor list load attempts will be done dynamically.
+
+	server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{
+		vendorListLatestVersion: 1,
+		vendorLists: map[int]string{
+			1: tcf1VendorList1,
+		},
+	})))
 	defer server.Close()
 
-	fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), 1)
-	_, err := fetcher(context.Background(), 1)
-	assertErr(t, err, false)
+	testCases := []test{
+		{
+			description: "Fetch - No Fallback - Vendor Doesn't Exist",
+			setup: testSetup{
+				enableTCF1Fetch:    true,
+				enableTCF1Fallback: false,
+				vendorListVersion:  2,
+			},
+			expected: testExpected{
+				errorMessage: "gdpr vendor list version 2 does not exist, or has not been loaded yet. Try again in a few minutes",
+			},
+		},
+		{
+			description: "Fetch - Fallback - Vendor Doesn't Exist",
+			setup: testSetup{
+				enableTCF1Fetch:    true,
+				enableTCF1Fallback: true,
+				vendorListVersion:  2,
+			},
+			expected: vendorListFallbackExpected,
+		},
+		{
+			description: "No Fetch - Fallback - Vendor Doesn't Exist",
+			setup: testSetup{
+				enableTCF1Fetch:    false,
+				enableTCF1Fallback: true,
+				vendorListVersion:  2,
+			},
+			expected: vendorListFallbackExpected,
+		},
+		{
+			description: "No Fetch - No Fallback - Vendor Doesn't Exist",
+			setup: testSetup{
+				enableTCF1Fetch:    false,
+				enableTCF1Fallback: false,
+				vendorListVersion:  2,
+			},
+			expected: testExpected{
+				errorMessage: "gdpr vendor list version 2 does not exist, or has not been loaded yet. Try again in a few minutes",
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		runTest(t, test, 1, server)
+	}
 }
 
-func TestMissingVendorlistFetch(t *testing.T) {
-	server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{1: "{}"})))
+func TestTCF2FetcherDynamicLoadListDoesntExist(t *testing.T) {
+	// Loads the first vendor list during initialization by setting the latest vendor list version to 1.
+	// All other vendor list load attempts will be done dynamically.
+	// Ensures TCF1 fetch settings have no effect on TCF2.
+
+	server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{
+		vendorListLatestVersion: 1,
+		vendorLists: map[int]string{
+			1: tcf2VendorList1,
+		},
+	})))
 	defer server.Close()
 
-	fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), 1)
-	_, err := fetcher(context.Background(), 2)
-	assertErr(t, err, false)
-}
+	testCases := []test{
+		{
+			description: "Fetch - No Fallback - Vendor Doesn't Exist",
+			setup: testSetup{
+				enableTCF1Fetch:    true,
+				enableTCF1Fallback: false,
+				vendorListVersion:  2,
+			},
+			expected: testExpected{
+				errorMessage: "gdpr vendor list version 2 does not exist, or has not been loaded yet. Try again in a few minutes",
+			},
+		},
+		{
+			description: "Fetch - Fallback - Vendor Doesn't Exist",
+			setup: testSetup{
+				enableTCF1Fetch:    true,
+				enableTCF1Fallback: true,
+				vendorListVersion:  2,
+			},
+			expected: testExpected{
+				errorMessage: "gdpr vendor list version 2 does not exist, or has not been loaded yet. Try again in a few minutes",
+			},
+		},
+		{
+			description: "No Fetch - Fallback - Vendor Doesn't Exist",
+			setup: testSetup{
+				enableTCF1Fetch:    false,
+				enableTCF1Fallback: true,
+				vendorListVersion:  2,
+			},
+			expected: testExpected{
+				errorMessage: "gdpr vendor list version 2 does not exist, or has not been loaded yet. Try again in a few minutes",
+			},
+		},
+		{
+			description: "No Fetch - No Fallback - Vendor Doesn't Exist",
+			setup: testSetup{
+				enableTCF1Fetch:    false,
+				enableTCF1Fallback: false,
+				vendorListVersion:  2,
+			},
+			expected: testExpected{
+				errorMessage: "gdpr vendor list version 2 does not exist, or has not been loaded yet. Try again in a few minutes",
+			},
+		},
+	}
 
-func TestVendorListMaker(t *testing.T) {
-	assertStringsEqual(t, "https://vendorlist.consensu.org/vendorlist.json", vendorListURLMaker(0, 1))
-	assertStringsEqual(t, "https://vendorlist.consensu.org/v-2/vendorlist.json", vendorListURLMaker(2, 1))
-	assertStringsEqual(t, "https://vendorlist.consensu.org/v-12/vendorlist.json", vendorListURLMaker(12, 1))
-	assertStringsEqual(t, "https://vendorlist.consensu.org/v2/vendor-list.json", vendorListURLMaker(0, 2))
-	assertStringsEqual(t, "https://vendorlist.consensu.org/v2/archives/vendor-list-v7.json", vendorListURLMaker(7, 2))
+	for _, test := range testCases {
+		runTest(t, test, tcf2SpecVersion, server)
+	}
 }
 
-func TestDefaultVendorList(t *testing.T) {
-	firstVendorList := mockVendorListData(t, 1, map[uint16]*purposes{
-		32: {
-			purposes: []int{1, 2},
-		},
-	})
-	secondVendorList := mockVendorListData(t, 2, map[uint16]*purposes{
-		12: {
-			purposes: []int{2},
+func TestTCF1FetcherThrottling(t *testing.T) {
+	server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{
+		vendorListLatestVersion: 1,
+		vendorLists: map[int]string{
+			1: tcf1MarshalVendorList(tcf1VendorList{
+				VendorListVersion: 1,
+				Vendors:           []tcf1Vendor{{ID: 12, Purposes: []int{1}}},
+			}),
+			2: tcf1MarshalVendorList(tcf1VendorList{
+				VendorListVersion: 2,
+				Vendors:           []tcf1Vendor{{ID: 12, Purposes: []int{1, 2}}},
+			}),
+			3: tcf1MarshalVendorList(tcf1VendorList{
+				VendorListVersion: 3,
+				Vendors:           []tcf1Vendor{{ID: 12, Purposes: []int{1, 2, 3}}},
+			}),
 		},
-	})
-	server := httptest.NewServer(http.HandlerFunc(mockServer(2, map[int]string{
-		1: firstVendorList,
-		2: secondVendorList,
 	})))
 	defer server.Close()
 
-	testcfg := testConfig()
-	testcfg.TCF1.FetchGVL = true
-	testcfg.TCF1.FallbackGVLPath = "../static/tcf1/fallback_gvl.json"
-	fetcher := newVendorListFetcher(context.Background(), testcfg, server.Client(), testURLMaker(server), 1)
+	fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), tcf1SpecVersion)
 
-	list, err := fetcher(context.Background(), 12)
-	assert.NoError(t, err, "Error with fetching default vendorlist: %v", err)
-	assert.Equal(t, uint16(215), list.Version(), "Expected to fetch default version 215, got %d", list.Version())
+	// Dynamically Load List 2 Successfully
+	_, errList1 := fetcher(context.Background(), 2)
+	assert.NoError(t, errList1)
 
-	// Testing that we got the default vendorlist data, and not the version off the server.
-	vendor := list.Vendor(12)
-	assert.Equal(t, true, vendor.Purpose(1))
-	assert.Equal(t, false, vendor.Purpose(2))
+	// Fail To Load List 3 Due To Rate Limiting
+	// - The request is rate limited after dynamically list 2.
+	_, errList2 := fetcher(context.Background(), 3)
+	assert.EqualError(t, errList2, "gdpr vendor list version 3 does not exist, or has not been loaded yet. Try again in a few minutes")
 }
 
-func TestFallbackVendorListPassthrough(t *testing.T) {
-	firstVendorList := mockVendorListData(t, 1, map[uint16]*purposes{
-		32: {
-			purposes: []int{1, 2},
+func TestTCF2FetcherThrottling(t *testing.T) {
+	server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{
+		vendorListLatestVersion: 1,
+		vendorLists: map[int]string{
+			1: tcf2MarshalVendorList(tcf2VendorList{
+				VendorListVersion: 1,
+				Vendors:           map[string]*tcf2Vendor{"12": {ID: 12, Purposes: []int{1}}},
+			}),
+			2: tcf2MarshalVendorList(tcf2VendorList{
+				VendorListVersion: 2,
+				Vendors:           map[string]*tcf2Vendor{"12": {ID: 12, Purposes: []int{1, 2}}},
+			}),
+			3: tcf2MarshalVendorList(tcf2VendorList{
+				VendorListVersion: 3,
+				Vendors:           map[string]*tcf2Vendor{"12": {ID: 12, Purposes: []int{1, 2, 3}}},
+			}),
 		},
-	})
-	secondVendorList := mockVendorListData(t, 2, map[uint16]*purposes{
-		12: {
-			purposes: []int{2},
+	})))
+	defer server.Close()
+
+	fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), tcf2SpecVersion)
+
+	// Dynamically Load List 2 Successfully
+	_, errList1 := fetcher(context.Background(), 2)
+	assert.NoError(t, errList1)
+
+	// Fail To Load List 3 Due To Rate Limiting
+	// - The request is rate limited after dynamically list 2.
+	_, errList2 := fetcher(context.Background(), 3)
+	assert.EqualError(t, errList2, "gdpr vendor list version 3 does not exist, or has not been loaded yet. Try again in a few minutes")
+}
+
+func TestTCF1MalformedVendorlist(t *testing.T) {
+	server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{
+		vendorListLatestVersion: 1,
+		vendorLists: map[int]string{
+			1: "malformed",
 		},
-	})
-	server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{
-		1: firstVendorList,
-		2: secondVendorList,
 	})))
 	defer server.Close()
 
-	testcfg := testConfig()
-	testcfg.TCF1.FetchGVL = true
-	testcfg.TCF1.FallbackGVLPath = "../static/tcf1/fallback_gvl.json"
-	fetcher := newVendorListFetcher(context.Background(), testcfg, server.Client(), testURLMaker(server), 1)
-	list, err := fetcher(context.Background(), 2)
-	assert.NoError(t, err, "Error with fetching existing vendorlist: %v", err)
-	assert.Equal(t, uint16(2), list.Version(), "Expected to fetch mock list version 2, got version %d", list.Version())
-
-	// Testing that we got the testing vendorlist data, and not the default.
-	vendor := list.Vendor(12)
-	assert.Equal(t, false, vendor.Purpose(1))
-	assert.Equal(t, true, vendor.Purpose(2))
-}
-
-func TestFallbackVendorListNoFetch(t *testing.T) {
-	firstVendorList := mockVendorListData(t, 1, map[uint16]*purposes{
-		32: {
-			purposes: []int{1, 2},
-		},
-	})
-	secondVendorList := mockVendorListData(t, 2, map[uint16]*purposes{
-		12: {
-			purposes: []int{2},
-		},
-	})
-	server := httptest.NewServer(http.HandlerFunc(mockServer(1, map[int]string{
-		1: firstVendorList,
-		2: secondVendorList,
+	fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), tcf1SpecVersion)
+	_, err := fetcher(context.Background(), 1)
+
+	// Fetching should fail since vendor list could not be unmarshalled.
+	assert.Error(t, err)
+}
+
+func TestTCF2MalformedVendorlist(t *testing.T) {
+	server := httptest.NewServer(http.HandlerFunc(mockServer(serverSettings{
+		vendorListLatestVersion: 1,
+		vendorLists: map[int]string{
+			1: "malformed",
+		},
 	})))
 	defer server.Close()
 
-	testcfg := testConfig()
-	testcfg.TCF1.FetchGVL = false
-	testcfg.TCF1.FallbackGVLPath = "../static/tcf1/fallback_gvl.json"
-	fetcher := newVendorListFetcher(context.Background(), testcfg, server.Client(), testURLMaker(server), 1)
-	list, err := fetcher(context.Background(), 2)
-	assert.NoError(t, err, "Error with fetching default vendorlist: %v", err)
-	assert.Equal(t, uint16(215), list.Version(), "Expected to fetch default version 215, got %d", list.Version())
+	fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), tcf2SpecVersion)
+	_, err := fetcher(context.Background(), 1)
+
+	// Fetching should fail since vendor list could not be unmarshalled.
+	assert.Error(t, err)
+}
+
+func TestTCF1ServerUrlInvalid(t *testing.T) {
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
+	server.Close()
+
+	invalidURLGenerator := func(uint16, uint8) string { return " http://invalid-url-has-leading-whitespace" }
 
-	// Testing that we got the default vendorlist data, and not the version off the server.
-	vendor := list.Vendor(12)
-	assert.Equal(t, true, vendor.Purpose(1))
-	assert.Equal(t, false, vendor.Purpose(2))
+	fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), invalidURLGenerator, tcf1SpecVersion)
+	_, err := fetcher(context.Background(), 1)
 
+	assert.EqualError(t, err, "gdpr vendor list version 1 does not exist, or has not been loaded yet. Try again in a few minutes")
+}
+
+func TestTCF2ServerUrlInvalid(t *testing.T) {
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
+	server.Close()
+
+	invalidURLGenerator := func(uint16, uint8) string { return " http://invalid-url-has-leading-whitespace" }
+
+	fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), invalidURLGenerator, tcf2SpecVersion)
+	_, err := fetcher(context.Background(), 1)
+
+	assert.EqualError(t, err, "gdpr vendor list version 1 does not exist, or has not been loaded yet. Try again in a few minutes")
+}
+
+func TestTCF1ServerUnavailable(t *testing.T) {
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
+	server.Close()
+
+	fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), tcf1SpecVersion)
+	_, err := fetcher(context.Background(), 1)
+
+	assert.EqualError(t, err, "gdpr vendor list version 1 does not exist, or has not been loaded yet. Try again in a few minutes")
+}
+
+func TestTCF2ServerUnavailable(t *testing.T) {
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
+	server.Close()
+
+	fetcher := newVendorListFetcher(context.Background(), testConfig(), server.Client(), testURLMaker(server), tcf2SpecVersion)
+	_, err := fetcher(context.Background(), 1)
+
+	assert.EqualError(t, err, "gdpr vendor list version 1 does not exist, or has not been loaded yet. Try again in a few minutes")
+}
+
+func TestVendorListURLMaker(t *testing.T) {
+	testCases := []struct {
+		description       string
+		tcfSpecVersion    uint8
+		vendorListVersion uint16
+		expectedURL       string
+	}{
+		{
+			description:       "TCF1 - Latest",
+			tcfSpecVersion:    1,
+			vendorListVersion: 0, // Forces latest version.
+			expectedURL:       "https://vendorlist.consensu.org/vendorlist.json",
+		},
+		{
+			description:       "TCF1 - Specific",
+			tcfSpecVersion:    1,
+			vendorListVersion: 42,
+			expectedURL:       "https://vendorlist.consensu.org/v-42/vendorlist.json",
+		},
+		{
+			description:       "TCF2 - Latest",
+			tcfSpecVersion:    2,
+			vendorListVersion: 0, // Forces latest version.
+			expectedURL:       "https://vendorlist.consensu.org/v2/vendor-list.json",
+		},
+		{
+			description:       "TCF2 - Specific",
+			tcfSpecVersion:    2,
+			vendorListVersion: 42,
+			expectedURL:       "https://vendorlist.consensu.org/v2/archives/vendor-list-v42.json",
+		},
+	}
+
+	for _, test := range testCases {
+		result := vendorListURLMaker(test.vendorListVersion, test.tcfSpecVersion)
+		assert.Equal(t, test.expectedURL, result)
+	}
+}
+
+var tcf1VendorList1 = tcf1MarshalVendorList(tcf1VendorList{
+	VendorListVersion: 1,
+	Vendors:           []tcf1Vendor{{ID: 12, Purposes: []int{2}}},
+})
+
+var tcf2VendorList1 = tcf2MarshalVendorList(tcf2VendorList{
+	VendorListVersion: 1,
+	Vendors:           map[string]*tcf2Vendor{"12": {ID: 12, Purposes: []int{2}}},
+})
+
+var vendorList1Expected = testExpected{
+	vendorListVersion: 1,
+	vendorID:          12,
+	vendorPurposes:    map[int]bool{1: false, 2: true, 3: false},
+}
+
+var tcf1VendorList2 = tcf1MarshalVendorList(tcf1VendorList{
+	VendorListVersion: 2,
+	Vendors:           []tcf1Vendor{{ID: 12, Purposes: []int{2, 3}}},
+})
+
+var tcf2VendorList2 = tcf2MarshalVendorList(tcf2VendorList{
+	VendorListVersion: 2,
+	Vendors:           map[string]*tcf2Vendor{"12": {ID: 12, Purposes: []int{2, 3}}},
+})
+
+var vendorList2Expected = testExpected{
+	vendorListVersion: 2,
+	vendorID:          12,
+	vendorPurposes:    map[int]bool{1: false, 2: true, 3: true},
+}
+
+var vendorListFallbackExpected = testExpected{
+	vendorListVersion: 215, // Values from hardcoded fallback file.
+	vendorID:          12,
+	vendorPurposes:    map[int]bool{1: true, 2: false, 3: true},
+}
+
+type tcf1VendorList struct {
+	VendorListVersion uint16       `json:"vendorListVersion"`
+	Vendors           []tcf1Vendor `json:"vendors"`
+}
+
+type tcf1Vendor struct {
+	ID       uint16 `json:"id"`
+	Purposes []int  `json:"purposeIds"`
+}
+
+func tcf1MarshalVendorList(vendorList tcf1VendorList) string {
+	json, _ := json.Marshal(vendorList)
+	return string(json)
+}
+
+type tcf2VendorList struct {
+	VendorListVersion uint16                 `json:"vendorListVersion"`
+	Vendors           map[string]*tcf2Vendor `json:"vendors"`
+}
+
+type tcf2Vendor struct {
+	ID               uint16 `json:"id"`
+	Purposes         []int  `json:"purposes"`
+	LegIntPurposes   []int  `json:"legIntPurposes"`
+	FlexiblePurposes []int  `json:"flexiblePurposes"`
+	SpecialPurposes  []int  `json:"specialPurposes"`
+}
+
+func tcf2MarshalVendorList(vendorList tcf2VendorList) string {
+	json, _ := json.Marshal(vendorList)
+	return string(json)
+}
+
+type serverSettings struct {
+	vendorListLatestVersion int
+	vendorLists             map[int]string
 }
 
 // mockServer returns a handler which returns the given response for each global vendor list version.
@@ -247,129 +711,74 @@ func TestFallbackVendorListNoFetch(t *testing.T) {
 //
 // If the "version" query param points to a version which doesn't exist, it returns a 403.
 // Don't ask why... that's just what the official page is doing. See https://vendorlist.consensu.org/v-9999/vendorlist.json
-func mockServer(latestVersion int, responses map[int]string) func(http.ResponseWriter, *http.Request) {
+func mockServer(settings serverSettings) func(http.ResponseWriter, *http.Request) {
 	return func(w http.ResponseWriter, req *http.Request) {
-		version := req.URL.Query().Get("version")
-		versionInt, err := strconv.Atoi(version)
+		vendorListVersion := req.URL.Query().Get("version")
+		vendorListVersionInt, err := strconv.Atoi(vendorListVersion)
 		if err != nil {
 			w.WriteHeader(http.StatusBadRequest)
-			w.Write([]byte("Request had invalid version: " + version))
+			w.Write([]byte("Request had invalid version: " + vendorListVersion))
 			return
 		}
-		if versionInt == 0 {
-			versionInt = latestVersion
+		if vendorListVersionInt == 0 {
+			vendorListVersionInt = settings.vendorListLatestVersion
 		}
-		response, ok := responses[versionInt]
+		response, ok := settings.vendorLists[vendorListVersionInt]
 		if !ok {
 			w.WriteHeader(http.StatusForbidden)
-			w.Write([]byte("Version not found: " + version))
+			w.Write([]byte("Version not found: " + vendorListVersion))
 			return
 		}
 		w.Write([]byte(response))
 	}
 }
 
-func mockVendorListData(t *testing.T, version uint16, vendors map[uint16]*purposes) string {
-	type vendorContract struct {
-		ID       uint16 `json:"id"`
-		Purposes []int  `json:"purposeIds"`
-	}
-
-	type vendorListContract struct {
-		Version uint16           `json:"vendorListVersion"`
-		Vendors []vendorContract `json:"vendors"`
-	}
-
-	buildVendors := func(input map[uint16]*purposes) []vendorContract {
-		vendors := make([]vendorContract, 0, len(input))
-		for id, purpose := range input {
-			vendors = append(vendors, vendorContract{
-				ID:       id,
-				Purposes: purpose.purposes,
-			})
-		}
-		return vendors
-	}
-
-	obj := vendorListContract{
-		Version: version,
-		Vendors: buildVendors(vendors),
-	}
-	data, err := json.Marshal(obj)
-	assertNilErr(t, err)
-	return string(data)
+type test struct {
+	description string
+	setup       testSetup
+	expected    testExpected
 }
 
-type purposeMap map[uint16]*purposes
-
-func mockVendorListDataTCF2(t *testing.T, version uint16, basicPurposes purposeMap, legitInterests purposeMap, flexPurposes purposeMap, specialPurposes purposeMap) string {
-	type vendorContract struct {
-		ID               uint16 `json:"id"`
-		Purposes         []int  `json:"purposes"`
-		LegIntPurposes   []int  `json:"legIntPurposes"`
-		FlexiblePurposes []int  `json:"flexiblePurposes"`
-		SpecialPurposes  []int  `json:"specialPurposes"`
-	}
-
-	type vendorListContract struct {
-		Version uint16                    `json:"vendorListVersion"`
-		Vendors map[string]vendorContract `json:"vendors"`
-	}
-
-	vendors := make(map[string]vendorContract, len(basicPurposes))
-	for id, purpose := range basicPurposes {
-		sid := strconv.Itoa(int(id))
-		vendor, ok := vendors[sid]
-		if !ok {
-			vendor = vendorContract{ID: id}
-		}
-		vendor.Purposes = purpose.purposes
-		vendors[sid] = vendor
-	}
+type testSetup struct {
+	enableTCF1Fetch    bool
+	enableTCF1Fallback bool
+	vendorListVersion  uint16
+}
 
-	for id, purpose := range legitInterests {
-		sid := strconv.Itoa(int(id))
-		vendor, ok := vendors[sid]
-		if !ok {
-			vendor = vendorContract{ID: id}
-		}
-		vendor.LegIntPurposes = purpose.purposes
-		vendors[sid] = vendor
-	}
+type testExpected struct {
+	errorMessage      string
+	vendorListVersion uint16
+	vendorID          uint16
+	vendorPurposes    map[int]bool
+}
 
-	for id, purpose := range flexPurposes {
-		sid := strconv.Itoa(int(id))
-		vendor, ok := vendors[sid]
-		if !ok {
-			vendor = vendorContract{ID: id}
-		}
-		vendor.FlexiblePurposes = purpose.purposes
-		vendors[sid] = vendor
+func runTest(t *testing.T, test test, tcfSpecVersion uint8, server *httptest.Server) {
+	config := testConfig()
+	config.TCF1.FetchGVL = test.setup.enableTCF1Fetch
+	if test.setup.enableTCF1Fallback {
+		config.TCF1.FallbackGVLPath = "../static/tcf1/fallback_gvl.json"
 	}
 
-	for id, purpose := range specialPurposes {
-		sid := strconv.Itoa(int(id))
-		vendor, ok := vendors[sid]
-		if !ok {
-			vendor = vendorContract{ID: id}
+	fetcher := newVendorListFetcher(context.Background(), config, server.Client(), testURLMaker(server), tcfSpecVersion)
+	vendorList, err := fetcher(context.Background(), test.setup.vendorListVersion)
+
+	if test.expected.errorMessage != "" {
+		assert.EqualError(t, err, test.expected.errorMessage, test.description+":error")
+	} else {
+		assert.NoError(t, err, test.description+":vendorlist")
+		assert.Equal(t, test.expected.vendorListVersion, vendorList.Version(), test.description+":vendorlistid")
+		vendor := vendorList.Vendor(test.expected.vendorID)
+		for id, expected := range test.expected.vendorPurposes {
+			result := vendor.Purpose(consentconstants.Purpose(id))
+			assert.Equalf(t, expected, result, "%s:vendor-%d:purpose-%d", test.description, vendorList.Version(), id)
 		}
-		vendor.SpecialPurposes = purpose.purposes
-		vendors[sid] = vendor
 	}
-
-	obj := vendorListContract{
-		Version: version,
-		Vendors: vendors,
-	}
-	data, err := json.Marshal(obj)
-	assertNilErr(t, err)
-	return string(data)
 }
 
 func testURLMaker(server *httptest.Server) func(uint16, uint8) string {
 	url := server.URL
-	return func(version uint16, TCFVer uint8) string {
-		return url + "?version=" + strconv.Itoa(int(version))
+	return func(vendorListVersion uint16, tcfSpecVersion uint8) string {
+		return url + "?version=" + strconv.Itoa(int(vendorListVersion))
 	}
 }
 
@@ -379,9 +788,8 @@ func testConfig() config.GDPR {
 			InitVendorlistFetch:   60 * 1000,
 			ActiveVendorlistFetch: 1000 * 5,
 		},
+		TCF1: config.TCF1{
+			FetchGVL: true,
+		},
 	}
 }
-
-type purposes struct {
-	purposes []int
-}
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)
 }