Repository: prebid/prebid-server
Pull request #2374 (2022-09-12 14:33:38)

GDPR: Basic enforcement
This PR adds basic enforcement. The basic enforcement algorithm may now be set as the enforcement algorithm of choice for any purpose in the host config or in an account config, with the account config setting taking precedence. If the basic algorithm is specified in a config for a given purpose or if the conditions hold to downgrade from full to basic for a particular bidder, we will execute the basic enforcement algorithm instead of the full enforcement algorithm for that bidder and purpose.

As part of this PR I did some refactoring while leaving all of the GDPR integration tests in `impl_test.go` in place. As part of the refactor:
- `allowActivities`, which was previously called from two places in `AuctionActivitiesAllowed`, has been deleted with the logic moved into `AuctionActivitiesAllowed` and reorganized so this related code is consolidated and a bit flatter.
- separate helper functions `allowID`, `allowBidRequest` and `allowGeo` were created
- the full enforcement algorithm was separated out into its own file to improve testability

Notes on the design:
- an interface `PurposeEnforcer` was created which is implemented by both the full and basic enforcement algorithm objects `FullEnforcement` and `BasicEnforcement` and these algorithms compute legal basis for a given purpose and bidder using the `LegalBasis` method
- when the `allowSync`, `allowID` and `allowBidRequest` functions are checking if a corresponding activity is allowed, they use the `purposeEnforcerBuilder` to determine which algorithm object implementing the `PurposeEnforcer` interface should be used to determine legal basis for a purpose.
- The `purposeEnforcerBuilder` minimizes the number of `PurposeEnforcer` objects that are created for a given request by caching any newly created objects for a given purpose for the life of the request. Any subsequent attempt during the life of the request to create a particular type of `PurposeEnforcer` object for a particular purpose will retrieve what is stored in memory instead of constructing a new object.



Patch:
diff --git a/account/account.go b/account/account.go
index 64c7ee3e..7d4abf04 100644
--- a/account/account.go
+++ b/account/account.go
@@ -92,6 +92,13 @@ func GetAccount(ctx context.Context, cfg *config.Configuration, fetcher stored_r
 	return account, nil
 }
 
+// TCF2Enforcements maps enforcement algo string values to their integer representation and is
+// used to limit string compares
+var TCF2Enforcements = map[string]config.TCF2EnforcementAlgo{
+	config.TCF2EnforceAlgoBasic: config.TCF2BasicEnforcement,
+	config.TCF2EnforceAlgoFull:  config.TCF2FullEnforcement,
+}
+
 // setDerivedConfig modifies an account object by setting fields derived from other fields set in the account configuration
 func setDerivedConfig(account *config.Account) {
 	account.GDPR.PurposeConfigs = map[consentconstants.Purpose]*config.AccountGDPRPurpose{
@@ -107,9 +114,16 @@ func setDerivedConfig(account *config.Account) {
 		10: &account.GDPR.Purpose10,
 	}
 
-	// To look for a purpose's vendor exceptions in O(1) time, for each purpose we fill this hash table with bidders
-	// located in the VendorExceptions field of the GDPR.PurposeX struct
 	for _, pc := range account.GDPR.PurposeConfigs {
+		// To minimize the number of string compares per request, we set the integer representation
+		// of the enforcement algorithm on each purpose config
+		pc.EnforceAlgoID = config.TCF2UndefinedEnforcement
+		if algo, exists := TCF2Enforcements[pc.EnforceAlgo]; exists {
+			pc.EnforceAlgoID = algo
+		}
+
+		// To look for a purpose's vendor exceptions in O(1) time, for each purpose we fill this hash table with bidders
+		// located in the VendorExceptions field of the GDPR.PurposeX struct
 		if pc.VendorExceptions == nil {
 			continue
 		}
diff --git a/adapters/appush/appush.go b/adapters/appush/appush.go
new file mode 100644
index 00000000..fa99fafa
--- /dev/null
+++ b/adapters/appush/appush.go
@@ -0,0 +1,154 @@
+package appush
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+
+	"github.com/prebid/openrtb/v17/openrtb2"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+type adapter struct {
+	endpoint string
+}
+
+type reqBodyExt struct {
+	AppushBidderExt reqBodyExtBidder `json:"bidder"`
+}
+
+type reqBodyExtBidder struct {
+	Type        string `json:"type"`
+	PlacementID string `json:"placementId,omitempty"`
+	EndpointID  string `json:"endpointId,omitempty"`
+}
+
+func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
+	bidder := &adapter{
+		endpoint: config.Endpoint,
+	}
+	return bidder, nil
+}
+
+func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	var err error
+	var adapterRequests []*adapters.RequestData
+
+	for _, imp := range request.Imp {
+		reqCopy := *request
+		reqCopy.Imp = []openrtb2.Imp{imp}
+
+		var bidderExt adapters.ExtImpBidder
+		var appushExt openrtb_ext.ImpExtAppush
+
+		if err = json.Unmarshal(reqCopy.Imp[0].Ext, &bidderExt); err != nil {
+			return nil, []error{err}
+		}
+		if err = json.Unmarshal(bidderExt.Bidder, &appushExt); err != nil {
+			return nil, []error{err}
+		}
+
+		temp := reqBodyExt{AppushBidderExt: reqBodyExtBidder{}}
+
+		if appushExt.PlacementID != "" {
+			temp.AppushBidderExt.PlacementID = appushExt.PlacementID
+			temp.AppushBidderExt.Type = "publisher"
+		} else if appushExt.EndpointID != "" {
+			temp.AppushBidderExt.EndpointID = appushExt.EndpointID
+			temp.AppushBidderExt.Type = "network"
+		}
+
+		finalyImpExt, err := json.Marshal(temp)
+		if err != nil {
+			return nil, []error{err}
+		}
+
+		reqCopy.Imp[0].Ext = finalyImpExt
+
+		adapterReq, err := a.makeRequest(&reqCopy)
+		if err != nil {
+			return nil, []error{err}
+		}
+
+		if adapterReq != nil {
+			adapterRequests = append(adapterRequests, adapterReq)
+		}
+	}
+	return adapterRequests, nil
+}
+
+func (a *adapter) makeRequest(request *openrtb2.BidRequest) (*adapters.RequestData, error) {
+	reqJSON, err := json.Marshal(request)
+	if err != nil {
+		return nil, err
+	}
+
+	headers := http.Header{}
+	headers.Add("Content-Type", "application/json;charset=utf-8")
+	headers.Add("Accept", "application/json")
+	return &adapters.RequestData{
+		Method:  "POST",
+		Uri:     a.endpoint,
+		Body:    reqJSON,
+		Headers: headers,
+	}, err
+}
+
+func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	if responseData.StatusCode == http.StatusNoContent {
+		return nil, nil
+	}
+
+	if responseData.StatusCode != http.StatusOK {
+		err := &errortypes.BadServerResponse{
+			Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info.", responseData.StatusCode),
+		}
+		return nil, []error{err}
+	}
+
+	var response openrtb2.BidResponse
+	if err := json.Unmarshal(responseData.Body, &response); err != nil {
+		return nil, []error{err}
+	}
+
+	bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
+	bidResponse.Currency = response.Cur
+	for _, seatBid := range response.SeatBid {
+		for i := range seatBid.Bid {
+			bidType, err := getMediaTypeForImp(seatBid.Bid[i].ImpID, request.Imp)
+			if err != nil {
+				return nil, []error{err}
+			}
+
+			b := &adapters.TypedBid{
+				Bid:     &seatBid.Bid[i],
+				BidType: bidType,
+			}
+			bidResponse.Bids = append(bidResponse.Bids, b)
+		}
+	}
+	return bidResponse, nil
+}
+
+func getMediaTypeForImp(impID string, imps []openrtb2.Imp) (openrtb_ext.BidType, error) {
+	for _, imp := range imps {
+		if imp.ID == impID {
+			if imp.Banner != nil {
+				return openrtb_ext.BidTypeBanner, nil
+			}
+			if imp.Video != nil {
+				return openrtb_ext.BidTypeVideo, nil
+			}
+			if imp.Native != nil {
+				return openrtb_ext.BidTypeNative, nil
+			}
+		}
+	}
+
+	return "", &errortypes.BadInput{
+		Message: fmt.Sprintf("Failed to find impression \"%s\"", impID),
+	}
+}
diff --git a/adapters/ccx/ccx.go b/adapters/ccx/ccx.go
new file mode 100644
index 00000000..cc28e60c
--- /dev/null
+++ b/adapters/ccx/ccx.go
@@ -0,0 +1,80 @@
+package ccx
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+
+	"github.com/prebid/openrtb/v17/openrtb2"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/openrtb_ext"
+
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/errortypes"
+)
+
+type adapter struct {
+	endpoint string
+}
+
+// Builder builds a new instance of the Foo adapter for the given bidder with the given config.
+func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
+	bidder := &adapter{
+		endpoint: config.Endpoint,
+	}
+	return bidder, nil
+}
+
+func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+
+	requestJSON, err := json.Marshal(request)
+	if err != nil {
+		return nil, []error{err}
+	}
+
+	requestData := &adapters.RequestData{
+		Method: "POST",
+		Uri:    a.endpoint,
+		Body:   requestJSON,
+	}
+
+	return []*adapters.RequestData{requestData}, nil
+}
+
+func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	if responseData.StatusCode == http.StatusNoContent {
+		return nil, nil
+	}
+
+	if responseData.StatusCode == http.StatusBadRequest {
+		err := &errortypes.BadInput{
+			Message: "Unexpected status code: 400. Bad request from publisher. Run with request.debug = 1 for more info.",
+		}
+		return nil, []error{err}
+	}
+
+	if responseData.StatusCode != http.StatusOK {
+		err := &errortypes.BadServerResponse{
+			Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info.", responseData.StatusCode),
+		}
+		return nil, []error{err}
+	}
+
+	var response openrtb2.BidResponse
+	if err := json.Unmarshal(responseData.Body, &response); err != nil {
+		return nil, []error{err}
+	}
+
+	bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
+	bidResponse.Currency = response.Cur
+	for _, seatBid := range response.SeatBid {
+		for i := range seatBid.Bid {
+			b := &adapters.TypedBid{
+				Bid:     &seatBid.Bid[i],
+				BidType: openrtb_ext.BidTypeBanner,
+			}
+			bidResponse.Bids = append(bidResponse.Bids, b)
+		}
+	}
+	return bidResponse, nil
+}
diff --git a/adapters/pubmatic/pubmatic.go b/adapters/pubmatic/pubmatic.go
index f00d0d24..5040622d 100644
--- a/adapters/pubmatic/pubmatic.go
+++ b/adapters/pubmatic/pubmatic.go
@@ -41,12 +41,7 @@ type pubmaticBidExtVideo struct {
 
 type ExtImpBidderPubmatic struct {
 	adapters.ExtImpBidder
-	Data *ExtData `json:"data,omitempty"`
-}
-
-type ExtData struct {
-	AdServer *ExtAdServer `json:"adserver"`
-	PBAdSlot string       `json:"pbadslot"`
+	Data json.RawMessage `json:"data,omitempty"`
 }
 
 type ExtAdServer struct {
@@ -71,6 +66,8 @@ const (
 	pmZoneIDKeyNameOld = "pmZoneID"
 	ImpExtAdUnitKey    = "dfp_ad_unit_code"
 	AdServerGAM        = "gam"
+	AdServerKey        = "adserver"
+	PBAdslotKey        = "pbadslot"
 )
 
 func (a *PubmaticAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
@@ -303,12 +300,8 @@ func parseImpressionObject(imp *openrtb2.Imp, extractWrapperExtFromImp, extractP
 		extMap[pmZoneIDKeyName] = pubmaticExt.PmZoneID
 	}
 
-	if bidderExt.Data != nil {
-		if bidderExt.Data.AdServer != nil && bidderExt.Data.AdServer.Name == AdServerGAM && bidderExt.Data.AdServer.AdSlot != "" {
-			extMap[ImpExtAdUnitKey] = bidderExt.Data.AdServer.AdSlot
-		} else if bidderExt.Data.PBAdSlot != "" {
-			extMap[ImpExtAdUnitKey] = bidderExt.Data.PBAdSlot
-		}
+	if len(bidderExt.Data) > 0 {
+		populateFirstPartyDataImpAttributes(bidderExt.Data, extMap)
 	}
 
 	imp.Ext = nil
@@ -497,6 +490,109 @@ func getNativeAdm(adm string) (string, error) {
 	return adm, nil
 }
 
+// getMapFromJSON converts JSON to map
+func getMapFromJSON(source json.RawMessage) map[string]interface{} {
+	if source != nil {
+		dataMap := make(map[string]interface{})
+		err := json.Unmarshal(source, &dataMap)
+		if err == nil {
+			return dataMap
+		}
+	}
+	return nil
+}
+
+// populateFirstPartyDataImpAttributes will parse imp.ext.data and populate imp extMap
+func populateFirstPartyDataImpAttributes(data json.RawMessage, extMap map[string]interface{}) {
+
+	dataMap := getMapFromJSON(data)
+
+	if dataMap == nil {
+		return
+	}
+
+	populateAdUnitKey(data, dataMap, extMap)
+	populateDctrKey(dataMap, extMap)
+}
+
+// populateAdUnitKey parses data object to read and populate DFP adunit key
+func populateAdUnitKey(data json.RawMessage, dataMap, extMap map[string]interface{}) {
+
+	if name, err := jsonparser.GetString(data, "adserver", "name"); err == nil && name == AdServerGAM {
+		if adslot, err := jsonparser.GetString(data, "adserver", "adslot"); err == nil && adslot != "" {
+			extMap[ImpExtAdUnitKey] = adslot
+		}
+	}
+
+	//imp.ext.dfp_ad_unit_code is not set, then check pbadslot in imp.ext.data
+	if extMap[ImpExtAdUnitKey] == nil && dataMap[PBAdslotKey] != nil {
+		extMap[ImpExtAdUnitKey] = dataMap[PBAdslotKey].(string)
+	}
+}
+
+// populateDctrKey reads key-val pairs from imp.ext.data and add it in imp.ext.key_val
+func populateDctrKey(dataMap, extMap map[string]interface{}) {
+	var dctr strings.Builder
+
+	//append dctr key if already present in extMap
+	if extMap[dctrKeyName] != nil {
+		dctr.WriteString(extMap[dctrKeyName].(string))
+	}
+
+	for key, val := range dataMap {
+
+		//ignore 'pbaslot' and 'adserver' key as they are not targeting keys
+		if key == PBAdslotKey || key == AdServerKey {
+			continue
+		}
+
+		//separate key-val pairs in dctr string by pipe(|)
+		if dctr.Len() > 0 {
+			dctr.WriteString("|")
+		}
+
+		//trimming spaces from key
+		key = strings.TrimSpace(key)
+
+		switch typedValue := val.(type) {
+		case string:
+			if _, err := fmt.Fprintf(&dctr, "%s=%s", key, strings.TrimSpace(typedValue)); err != nil {
+				continue
+			}
+
+		case float64, bool:
+			if _, err := fmt.Fprintf(&dctr, "%s=%v", key, typedValue); err != nil {
+				continue
+			}
+
+		case []interface{}:
+			if valStrArr := getStringArray(typedValue); len(valStrArr) > 0 {
+				valStr := strings.Join(valStrArr[:], ",")
+				if _, err := fmt.Fprintf(&dctr, "%s=%s", key, valStr); err != nil {
+					continue
+				}
+			}
+		}
+	}
+
+	if dctrStr := dctr.String(); dctrStr != "" {
+		extMap[dctrKeyName] = strings.TrimSuffix(dctrStr, "|")
+	}
+}
+
+// getStringArray converts interface of type string array to string array
+func getStringArray(array []interface{}) []string {
+	aString := make([]string, len(array))
+	for i, v := range array {
+		if str, ok := v.(string); ok {
+			aString[i] = strings.TrimSpace(str)
+		} else {
+			return nil
+		}
+	}
+	return aString
+}
+
 // getBidType returns the bid type specified in the response bid.ext
 func getBidType(bidExt *pubmaticBidExt) openrtb_ext.BidType {
 	// setting "banner" as the default bid type
diff --git a/adapters/tappx/tappx.go b/adapters/tappx/tappx.go
index 05582802..06059267 100644
--- a/adapters/tappx/tappx.go
+++ b/adapters/tappx/tappx.go
@@ -18,7 +18,7 @@ import (
 	"github.com/prebid/prebid-server/openrtb_ext"
 )
 
-const TAPPX_BIDDER_VERSION = "1.4"
+const TAPPX_BIDDER_VERSION = "1.5"
 const TYPE_CNN = "prebid"
 
 type TappxAdapter struct {
@@ -132,7 +132,7 @@ func (a *TappxAdapter) buildEndpointURL(params *openrtb_ext.ExtImpTappx, test in
 	}
 
 	tappxHost := "tappx.com"
-	isNewEndpoint, err := regexp.Match(`^(zz|vz)[0-9]{3,}([a-z]{2}|test)$`, []byte(params.Endpoint))
+	isNewEndpoint, err := regexp.Match(`^(zz|vz)[0-9]{3,}([a-z]{2,3}|test)$`, []byte(params.Endpoint))
 	if isNewEndpoint {
 		tappxHost = params.Endpoint + ".pub." + tappxHost + "/rtb/"
 	} else {
diff --git a/config/accounts.go b/config/accounts.go
index a0785a8e..9d5918a4 100644
--- a/config/accounts.go
+++ b/config/accounts.go
@@ -81,17 +81,6 @@ type AccountGDPR struct {
 	SpecialFeature1     AccountGDPRSpecialFeature      `mapstructure:"special_feature1" json:"special_feature1"`
 }
 
-// BasicEnforcementVendor checks if the given bidder is considered a basic enforcement vendor which indicates whether
-// weak vendor enforcement applies to that bidder.
-func (a *AccountGDPR) BasicEnforcementVendor(bidder openrtb_ext.BidderName) (value, exists bool) {
-	if a.BasicEnforcementVendorsMap == nil {
-		return false, false
-	}
-	_, found := a.BasicEnforcementVendorsMap[string(bidder)]
-
-	return found, true
-}
-
 // EnabledForChannelType indicates whether GDPR is turned on at the account level for the specified channel type
 // by using the channel type setting if defined or the general GDPR setting if defined; otherwise it returns nil.
 func (a *AccountGDPR) EnabledForChannelType(channelType ChannelType) *bool {
@@ -135,6 +124,17 @@ func (a *AccountGDPR) PurposeEnforced(purpose consentconstants.Purpose) (value,
 	return *a.PurposeConfigs[purpose].EnforcePurpose, true
 }
 
+// PurposeEnforcementAlgo checks the purpose enforcement algo for a given purpose by first
+// looking at the account settings, and if not set there, defaulting to the host configuration.
+func (a *AccountGDPR) PurposeEnforcementAlgo(purpose consentconstants.Purpose) (value TCF2EnforcementAlgo, exists bool) {
+	c, exists := a.PurposeConfigs[purpose]
+
+	if exists && (c.EnforceAlgoID == TCF2BasicEnforcement || c.EnforceAlgoID == TCF2FullEnforcement) {
+		return c.EnforceAlgoID, true
+	}
+	return TCF2UndefinedEnforcement, false
+}
+
 // PurposeEnforcingVendors gets the account level enforce vendors setting for a given purpose returning the value and
 // whether or not it is set. If not set, a default value of true is returned matching host default behavior.
 func (a *AccountGDPR) PurposeEnforcingVendors(purpose consentconstants.Purpose) (value, exists bool) {
@@ -147,17 +147,14 @@ func (a *AccountGDPR) PurposeEnforcingVendors(purpose consentconstants.Purpose)
 	return *a.PurposeConfigs[purpose].EnforceVendors, true
 }
 
-// PurposeVendorException checks if the given bidder is a vendor exception for a given purpose.
-func (a *AccountGDPR) PurposeVendorException(purpose consentconstants.Purpose, bidder openrtb_ext.BidderName) (value, exists bool) {
-	if a.PurposeConfigs[purpose] == nil {
-		return false, false
-	}
-	if a.PurposeConfigs[purpose].VendorExceptionMap == nil {
-		return false, false
-	}
-	_, found := a.PurposeConfigs[purpose].VendorExceptionMap[bidder]
+// PurposeVendorExceptions returns the vendor exception map for a given purpose.
+func (a *AccountGDPR) PurposeVendorExceptions(purpose consentconstants.Purpose) (value map[openrtb_ext.BidderName]struct{}, exists bool) {
+	c, exists := a.PurposeConfigs[purpose]
 
-	return found, true
+	if exists && c.VendorExceptionMap != nil {
+		return c.VendorExceptionMap, true
+	}
+	return nil, false
 }
 
 // PurposeOneTreatmentEnabled gets the account level purpose one treatment enabled setting returning the value and
@@ -180,9 +177,11 @@ func (a *AccountGDPR) PurposeOneTreatmentAccessAllowed() (value, exists bool) {
 
 // AccountGDPRPurpose represents account-specific GDPR purpose configuration
 type AccountGDPRPurpose struct {
-	EnforceAlgo    string `mapstructure:"enforce_algo" json:"enforce_algo,omitempty"`
-	EnforcePurpose *bool  `mapstructure:"enforce_purpose" json:"enforce_purpose,omitempty"`
-	EnforceVendors *bool  `mapstructure:"enforce_vendors" json:"enforce_vendors,omitempty"`
+	EnforceAlgo string `mapstructure:"enforce_algo" json:"enforce_algo,omitempty"`
+	// Integer representation of enforcement algo for performance improvement on compares
+	EnforceAlgoID  TCF2EnforcementAlgo
+	EnforcePurpose *bool `mapstructure:"enforce_purpose" json:"enforce_purpose,omitempty"`
+	EnforceVendors *bool `mapstructure:"enforce_vendors" json:"enforce_vendors,omitempty"`
 	// Array of vendor exceptions that is used to create the hash table VendorExceptionMap so vendor names can be instantly accessed
 	VendorExceptions   []openrtb_ext.BidderName `mapstructure:"vendor_exceptions" json:"vendor_exceptions"`
 	VendorExceptionMap map[openrtb_ext.BidderName]struct{}
diff --git a/config/config.go b/config/config.go
index b560bce3..46b0ce66 100644
--- a/config/config.go
+++ b/config/config.go
@@ -264,8 +264,8 @@ func (cfg *GDPR) validatePurposes(errs []error) []error {
 		enforceAlgoValue := purposeConfigs[i].EnforceAlgo
 		enforceAlgoField := fmt.Sprintf("gdpr.tcf2.purpose%d.enforce_algo", (i + 1))
 
-		if enforceAlgoValue != TCF2FullEnforcement {
-			errs = append(errs, fmt.Errorf("%s must be \"full\". Got %s", enforceAlgoField, enforceAlgoValue))
+		if enforceAlgoValue != TCF2EnforceAlgoFull && enforceAlgoValue != TCF2EnforceAlgoBasic {
+			errs = append(errs, fmt.Errorf("%s must be \"basic\" or \"full\". Got %s", enforceAlgoField, enforceAlgoValue))
 		}
 	}
 	return errs
@@ -285,7 +285,16 @@ func (t *GDPRTimeouts) ActiveTimeout() time.Duration {
 }
 
 const (
-	TCF2FullEnforcement = "full"
+	TCF2EnforceAlgoBasic = "basic"
+	TCF2EnforceAlgoFull  = "full"
+)
+
+type TCF2EnforcementAlgo int
+
+const (
+	TCF2UndefinedEnforcement TCF2EnforcementAlgo = iota
+	TCF2BasicEnforcement
+	TCF2FullEnforcement
 )
 
 // TCF2 defines the TCF2 specific configurations for GDPR
@@ -307,15 +316,6 @@ type TCF2 struct {
 	PurposeOneTreatment TCF2PurposeOneTreatment `mapstructure:"purpose_one_treatment"`
 }
 
-// BasicEnforcementVendor checks if the given bidder is considered a basic enforcement vendor which indicates whether
-// weak vendor enforcement applies to that bidder. If set, the legal basis calculation for the bidder only considers
-// consent to the purpose, not the vendor. The idea is that the publisher trusts this vendor to enforce the
-// appropriate rules on their own. This only comes into play when enforceVendors is true as it lists those vendors that
-// are exempt for vendor enforcement.
-func (t *TCF2) BasicEnforcementVendor(openrtb_ext.BidderName) bool {
-	return false
-}
-
 // ChannelEnabled checks if a given channel type is enabled. All channel types are considered either
 // enabled or disabled based on the Enabled flag.
 func (t *TCF2) ChannelEnabled(channelType ChannelType) bool {
@@ -336,6 +336,14 @@ func (t *TCF2) PurposeEnforced(purpose consentconstants.Purpose) (value bool) {
 	return t.PurposeConfigs[purpose].EnforcePurpose
 }
 
+// PurposeEnforcementAlgo returns the default enforcement algorithm for a given purpose
+func (t *TCF2) PurposeEnforcementAlgo(purpose consentconstants.Purpose) (value TCF2EnforcementAlgo) {
+	if c, exists := t.PurposeConfigs[purpose]; exists {
+		return c.EnforceAlgoID
+	}
+	return TCF2FullEnforcement
+}
+
 // PurposeEnforcingVendors checks if enforcing vendors is turned on for a given purpose. With enforcing vendors
 // enabled, the GDPR full enforcement algorithm considers the GVL when determining legal basis; otherwise it's skipped.
 func (t *TCF2) PurposeEnforcingVendors(purpose consentconstants.Purpose) (value bool) {
@@ -345,17 +353,15 @@ func (t *TCF2) PurposeEnforcingVendors(purpose consentconstants.Purpose) (value
 	return t.PurposeConfigs[purpose].EnforceVendors
 }
 
-// PurposeVendorException checks if the specified bidder is considered a vendor exception for a given purpose. If a
-// bidder is a vendor exception, the GDPR full enforcement algorithm will bypass the legal basis calculation assuming
-// the request is valid and there isn't a "deny all" publisher restriction
-func (t *TCF2) PurposeVendorException(purpose consentconstants.Purpose, bidder openrtb_ext.BidderName) (value bool) {
-	if t.PurposeConfigs[purpose] == nil {
-		return false
-	}
-	if _, ok := t.PurposeConfigs[purpose].VendorExceptionMap[bidder]; ok {
-		return true
+// PurposeVendorExceptions returns the vendor exception map for a given purpose if it exists, otherwise it returns
+// an empty map of vendor exceptions
+func (t *TCF2) PurposeVendorExceptions(purpose consentconstants.Purpose) (value map[openrtb_ext.BidderName]struct{}) {
+	c, exists := t.PurposeConfigs[purpose]
+
+	if exists && c.VendorExceptionMap != nil {
+		return c.VendorExceptionMap
 	}
-	return false
+	return make(map[openrtb_ext.BidderName]struct{}, 0)
 }
 
 // FeatureOneEnforced checks if special feature one is enforced. If it is enforced, PBS will determine whether geo
@@ -385,10 +391,12 @@ func (t *TCF2) PurposeOneTreatmentAccessAllowed() (value bool) {
 
 // Making a purpose struct so purpose specific details can be added later.
 type TCF2Purpose struct {
-	Enabled        bool   `mapstructure:"enabled"` // Deprecated: Use enforce_purpose instead
-	EnforceAlgo    string `mapstructure:"enforce_algo"`
-	EnforcePurpose bool   `mapstructure:"enforce_purpose"`
-	EnforceVendors bool   `mapstructure:"enforce_vendors"`
+	Enabled     bool   `mapstructure:"enabled"` // Deprecated: Use enforce_purpose instead
+	EnforceAlgo string `mapstructure:"enforce_algo"`
+	// Integer representation of enforcement algo for performance improvement on compares
+	EnforceAlgoID  TCF2EnforcementAlgo
+	EnforcePurpose bool `mapstructure:"enforce_purpose"`
+	EnforceVendors bool `mapstructure:"enforce_vendors"`
 	// Array of vendor exceptions that is used to create the hash table VendorExceptionMap so vendor names can be instantly accessed
 	VendorExceptions   []openrtb_ext.BidderName `mapstructure:"vendor_exceptions"`
 	VendorExceptionMap map[openrtb_ext.BidderName]struct{}
@@ -675,6 +683,16 @@ func New(v *viper.Viper, bidderInfos BidderInfos, normalizeBidderName func(strin
 		10: &c.GDPR.TCF2.Purpose10,
 	}
 
+	// As an alternative to performing several string compares per request, we set the integer representation of
+	// the enforcement algorithm on each purpose config
+	for _, pc := range c.GDPR.TCF2.PurposeConfigs {
+		if pc.EnforceAlgo == TCF2EnforceAlgoBasic {
+			pc.EnforceAlgoID = TCF2BasicEnforcement
+		} else {
+			pc.EnforceAlgoID = TCF2FullEnforcement
+		}
+	}
+
 	// To look for a purpose's vendor exceptions in O(1) time, for each purpose we fill this hash table with bidders
 	// located in the VendorExceptions field of the GDPR.TCF2.PurposeX struct defined in this file
 	for _, pc := range c.GDPR.TCF2.PurposeConfigs {
@@ -1033,16 +1051,16 @@ func SetupViper(v *viper.Viper, filename string, bidderInfos BidderInfos) {
 	v.SetDefault("gdpr.tcf2.purpose8.enabled", true)
 	v.SetDefault("gdpr.tcf2.purpose9.enabled", true)
 	v.SetDefault("gdpr.tcf2.purpose10.enabled", true)
-	v.SetDefault("gdpr.tcf2.purpose1.enforce_algo", TCF2FullEnforcement)
-	v.SetDefault("gdpr.tcf2.purpose2.enforce_algo", TCF2FullEnforcement)
-	v.SetDefault("gdpr.tcf2.purpose3.enforce_algo", TCF2FullEnforcement)
-	v.SetDefault("gdpr.tcf2.purpose4.enforce_algo", TCF2FullEnforcement)
-	v.SetDefault("gdpr.tcf2.purpose5.enforce_algo", TCF2FullEnforcement)
-	v.SetDefault("gdpr.tcf2.purpose6.enforce_algo", TCF2FullEnforcement)
-	v.SetDefault("gdpr.tcf2.purpose7.enforce_algo", TCF2FullEnforcement)
-	v.SetDefault("gdpr.tcf2.purpose8.enforce_algo", TCF2FullEnforcement)
-	v.SetDefault("gdpr.tcf2.purpose9.enforce_algo", TCF2FullEnforcement)
-	v.SetDefault("gdpr.tcf2.purpose10.enforce_algo", TCF2FullEnforcement)
+	v.SetDefault("gdpr.tcf2.purpose1.enforce_algo", TCF2EnforceAlgoFull)
+	v.SetDefault("gdpr.tcf2.purpose2.enforce_algo", TCF2EnforceAlgoFull)
+	v.SetDefault("gdpr.tcf2.purpose3.enforce_algo", TCF2EnforceAlgoFull)
+	v.SetDefault("gdpr.tcf2.purpose4.enforce_algo", TCF2EnforceAlgoFull)
+	v.SetDefault("gdpr.tcf2.purpose5.enforce_algo", TCF2EnforceAlgoFull)
+	v.SetDefault("gdpr.tcf2.purpose6.enforce_algo", TCF2EnforceAlgoFull)
+	v.SetDefault("gdpr.tcf2.purpose7.enforce_algo", TCF2EnforceAlgoFull)
+	v.SetDefault("gdpr.tcf2.purpose8.enforce_algo", TCF2EnforceAlgoFull)
+	v.SetDefault("gdpr.tcf2.purpose9.enforce_algo", TCF2EnforceAlgoFull)
+	v.SetDefault("gdpr.tcf2.purpose10.enforce_algo", TCF2EnforceAlgoFull)
 	v.SetDefault("gdpr.tcf2.purpose1.enforce_purpose", true)
 	v.SetDefault("gdpr.tcf2.purpose2.enforce_purpose", true)
 	v.SetDefault("gdpr.tcf2.purpose3.enforce_purpose", true)
@@ -1131,14 +1149,14 @@ func migrateConfigTCF2EnforcePurposeFlags(v *viper.Viper) {
 		if v.IsSet(algoField) {
 			glog.Warningf("using %s and ignoring deprecated %s string type", algoField, purposeField)
 		} else {
-			v.Set(algoField, TCF2FullEnforcement)
+			v.Set(algoField, TCF2EnforceAlgoFull)
 
-			glog.Warningf("setting %s to \"%s\" based on deprecated %s string type \"%s\"", algoField, TCF2FullEnforcement, purposeField, v.GetString(purposeField))
+			glog.Warningf("setting %s to \"%s\" based on deprecated %s string type \"%s\"", algoField, TCF2EnforceAlgoFull, purposeField, v.GetString(purposeField))
 		}
 
 		oldPurposeFieldValue := v.GetString(purposeField)
 		newPurposeFieldValue := "false"
-		if oldPurposeFieldValue == TCF2FullEnforcement {
+		if oldPurposeFieldValue == TCF2EnforceAlgoFull {
 			newPurposeFieldValue = "true"
 		}
 
diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go
index b8c0d056..26abc217 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -33,6 +33,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/apacdex"
 	"github.com/prebid/prebid-server/adapters/applogy"
 	"github.com/prebid/prebid-server/adapters/appnexus"
+	"github.com/prebid/prebid-server/adapters/appush"
 	"github.com/prebid/prebid-server/adapters/audienceNetwork"
 	"github.com/prebid/prebid-server/adapters/automatad"
 	"github.com/prebid/prebid-server/adapters/avocet"
@@ -51,6 +52,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/bmtm"
 	"github.com/prebid/prebid-server/adapters/boldwin"
 	"github.com/prebid/prebid-server/adapters/brightroll"
+	"github.com/prebid/prebid-server/adapters/ccx"
 	"github.com/prebid/prebid-server/adapters/coinzilla"
 	"github.com/prebid/prebid-server/adapters/colossus"
 	"github.com/prebid/prebid-server/adapters/compass"
@@ -190,6 +192,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderApacdex:           apacdex.Builder,
 		openrtb_ext.BidderApplogy:           applogy.Builder,
 		openrtb_ext.BidderAppnexus:          appnexus.Builder,
+		openrtb_ext.BidderAppush:            appush.Builder,
 		openrtb_ext.BidderAudienceNetwork:   audienceNetwork.Builder,
 		openrtb_ext.BidderAutomatad:         automatad.Builder,
 		openrtb_ext.BidderAvocet:            avocet.Builder,
@@ -208,6 +211,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderBmtm:              bmtm.Builder,
 		openrtb_ext.BidderBoldwin:           boldwin.Builder,
 		openrtb_ext.BidderBrightroll:        brightroll.Builder,
+		openrtb_ext.BidderCcx:               ccx.Builder,
 		openrtb_ext.BidderCoinzilla:         coinzilla.Builder,
 		openrtb_ext.BidderColossus:          colossus.Builder,
 		openrtb_ext.BidderCompass:           compass.Builder,
diff --git a/gdpr/aggregated_config.go b/gdpr/aggregated_config.go
index 60e1f071..bbfb5032 100644
--- a/gdpr/aggregated_config.go
+++ b/gdpr/aggregated_config.go
@@ -9,14 +9,15 @@ import (
 
 // TCF2ConfigReader is an interface to access TCF2 configurations
 type TCF2ConfigReader interface {
-	BasicEnforcementVendor(openrtb_ext.BidderName) bool
+	BasicEnforcementVendors() map[string]struct{}
 	FeatureOneEnforced() bool
 	FeatureOneVendorException(openrtb_ext.BidderName) bool
 	ChannelEnabled(config.ChannelType) bool
 	IsEnabled() bool
 	PurposeEnforced(consentconstants.Purpose) bool
+	PurposeEnforcementAlgo(consentconstants.Purpose) config.TCF2EnforcementAlgo
 	PurposeEnforcingVendors(consentconstants.Purpose) bool
-	PurposeVendorException(consentconstants.Purpose, openrtb_ext.BidderName) bool
+	PurposeVendorExceptions(consentconstants.Purpose) map[openrtb_ext.BidderName]struct{}
 	PurposeOneTreatmentEnabled() bool
 	PurposeOneTreatmentAccessAllowed() bool
 }
@@ -62,6 +63,15 @@ func (tc *tcf2Config) PurposeEnforced(purpose consentconstants.Purpose) bool {
 	return value
 }
 
+// PurposeEnforcementAlgo checks the purpose enforcement algo for a given purpose by first
+// looking at the account settings, and if not set there, defaulting to the host configuration.
+func (tc *tcf2Config) PurposeEnforcementAlgo(purpose consentconstants.Purpose) config.TCF2EnforcementAlgo {
+	if value, exists := tc.AccountConfig.PurposeEnforcementAlgo(purpose); exists {
+		return value
+	}
+	return tc.HostConfig.PurposeEnforcementAlgo(purpose)
+}
+
 // PurposeEnforcingVendors checks if enforcing vendors is turned on for a given purpose by first looking at the
 // account settings, and if not set there, defaulting to the host configuration. With enforcing vendors enabled,
 // the GDPR full enforcement algorithm considers the GVL when determining legal basis; otherwise it's skipped.
@@ -74,16 +84,14 @@ func (tc *tcf2Config) PurposeEnforcingVendors(purpose consentconstants.Purpose)
 	return value
 }
 
-// PurposeVendorException checks if the specified bidder is considered a vendor exception for a given purpose by first
-// looking at the account settings, and if not set there, defaulting to the host configuration. If a bidder is a vendor
-// exception, the GDPR full enforcement algorithm will bypass the legal basis calculation assuming the request is valid
-// and there isn't a "deny all" publisher restriction
-func (tc *tcf2Config) PurposeVendorException(purpose consentconstants.Purpose, bidder openrtb_ext.BidderName) bool {
-	if value, exists := tc.AccountConfig.PurposeVendorException(purpose, bidder); exists {
+// PurposeVendorExceptions returns the vendor exception map for the specified purpose if it exists for the account;
+// otherwise it returns a nil map. If a bidder is a vendor exception, the GDPR full enforcement algorithm will
+// bypass the legal basis calculation assuming the request is valid and there isn't a "deny all" publisher restriction
+func (tc *tcf2Config) PurposeVendorExceptions(purpose consentconstants.Purpose) map[openrtb_ext.BidderName]struct{} {
+	if value, exists := tc.AccountConfig.PurposeVendorExceptions(purpose); exists {
 		return value
 	}
-	value := tc.HostConfig.PurposeVendorException(purpose, bidder)
-	return value
+	return tc.HostConfig.PurposeVendorExceptions(purpose)
 }
 
 // FeatureOneEnforced checks if special feature one is enforced by first looking at the account settings, and if not
@@ -128,14 +136,14 @@ func (tc *tcf2Config) PurposeOneTreatmentAccessAllowed() bool {
 	return value
 }
 
-// BasicEnforcementVendor checks if the given bidder is considered a basic enforcement vendor by looking at the account
-// settings, and if not set there, defaulting to false. If set, the legal basis calculation for the bidder only considers
-// consent to the purpose, not the vendor. The idea is that the publisher trusts this vendor to enforce the
-// appropriate rules on their own. This only comes into play when enforceVendors is true as it lists those vendors that
-// are exempt for vendor enforcement.
-func (tc *tcf2Config) BasicEnforcementVendor(bidder openrtb_ext.BidderName) bool {
-	if value, exists := tc.AccountConfig.BasicEnforcementVendor(bidder); exists {
-		return value
+// BasicEnforcementVendors returns the basic enforcement map if it exists for the account; otherwise it returns
+// an empty map. If a bidder is considered a basic enforcement vendor, the legal basis calculation for the bidder
+// only considers consent to the purpose, not the vendor. The idea is that the publisher trusts this vendor to
+// enforce the appropriate rules on their own. This only comes into play when enforceVendors is true as it lists
+// those vendors that are exempt for vendor enforcement.
+func (tc *tcf2Config) BasicEnforcementVendors() map[string]struct{} {
+	if tc.AccountConfig.BasicEnforcementVendorsMap != nil {
+		return tc.AccountConfig.BasicEnforcementVendorsMap
 	}
-	return false
+	return make(map[string]struct{}, 0)
 }
diff --git a/gdpr/basic_enforcement.go b/gdpr/basic_enforcement.go
new file mode 100644
index 00000000..f4559c46
--- /dev/null
+++ b/gdpr/basic_enforcement.go
@@ -0,0 +1,58 @@
+package gdpr
+
+import (
+	tcf2 "github.com/prebid/go-gdpr/vendorconsent/tcf2"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+// BasicEnforcement determines if legal basis is satisfied for a given purpose and bidder using
+// the TCF2 basic enforcement algorithm. The algorithm is a high-level mode of consent confirmation
+// that looks for a good-faith indication that the user has provided consent or legal basis signals
+// necessary to perform a privacy-protected activity. The algorithm does not involve the GVL.
+// BasicEnforcement implements the PurposeEnforcer interface
+type BasicEnforcement struct {
+	cfg purposeConfig
+}
+
+// LegalBasis determines if legal basis is satisfied for a given purpose and bidder based on user consent
+// and legal basis signals.
+func (be *BasicEnforcement) LegalBasis(vendorInfo VendorInfo, bidder openrtb_ext.BidderName, consent tcf2.ConsentMetadata, overrides Overrides) bool {
+	enforcePurpose, enforceVendors := be.applyEnforceOverrides(overrides)
+
+	if !enforcePurpose && !enforceVendors {
+		return true
+	}
+	if be.cfg.vendorException(bidder) && !overrides.blockVendorExceptions {
+		return true
+	}
+	if !enforcePurpose && be.cfg.basicEnforcementVendor(bidder) {
+		return true
+	}
+	if enforcePurpose && consent.PurposeAllowed(be.cfg.PurposeID) && be.cfg.basicEnforcementVendor(bidder) {
+		return true
+	}
+	if enforcePurpose && consent.PurposeLITransparency(be.cfg.PurposeID) && overrides.allowLITransparency {
+		return true
+	}
+	if enforcePurpose && !consent.PurposeAllowed(be.cfg.PurposeID) {
+		return false
+	}
+	if !enforceVendors {
+		return true
+	}
+	return consent.VendorConsent(vendorInfo.vendorID)
+}
+
+// applyEnforceOverrides returns the enforce purpose and enforce vendor configuration values unless
+// those values have been overridden, in which case they return true
+func (be *BasicEnforcement) applyEnforceOverrides(overrides Overrides) (enforcePurpose, enforceVendors bool) {
+	enforcePurpose = be.cfg.EnforcePurpose
+	if overrides.enforcePurpose {
+		enforcePurpose = true
+	}
+	enforceVendors = be.cfg.EnforceVendors
+	if overrides.enforceVendors {
+		enforceVendors = true
+	}
+	return
+}
diff --git a/gdpr/full_enforcement.go b/gdpr/full_enforcement.go
new file mode 100644
index 00000000..b86f7a3f
--- /dev/null
+++ b/gdpr/full_enforcement.go
@@ -0,0 +1,97 @@
+package gdpr
+
+import (
+	tcf2 "github.com/prebid/go-gdpr/vendorconsent/tcf2"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+const (
+	pubRestrictNotAllowed           = 0
+	pubRestrictRequireConsent       = 1
+	pubRestrictRequireLegitInterest = 2
+)
+
+// FullEnforcement determines if legal basis is satisfied for a given purpose and bidder using
+// the TCF2 full enforcement algorithm. The algorithm is a detailed confirmation that reads the
+// GVL, interprets the consent string and performs legal basis analysis necessary to perform a
+// privacy-protected activity.
+// FullEnforcement implements the PurposeEnforcer interface
+type FullEnforcement struct {
+	cfg purposeConfig
+}
+
+// LegalBasis determines if legal basis is satisfied for a given purpose and bidder based on the
+// vendor claims in the GVL, publisher restrictions and user consent.
+func (fe *FullEnforcement) LegalBasis(vendorInfo VendorInfo, bidder openrtb_ext.BidderName, consent tcf2.ConsentMetadata, overrides Overrides) bool {
+	enforcePurpose, enforceVendors := fe.applyEnforceOverrides(overrides)
+
+	if consent.CheckPubRestriction(uint8(fe.cfg.PurposeID), pubRestrictNotAllowed, vendorInfo.vendorID) {
+		return false
+	}
+	if !enforcePurpose && !enforceVendors {
+		return true
+	}
+	if fe.cfg.vendorException(bidder) && !overrides.blockVendorExceptions {
+		return true
+	}
+
+	purposeAllowed := fe.consentEstablished(consent, vendorInfo, enforcePurpose, enforceVendors)
+	legitInterest := fe.legitInterestEstablished(consent, vendorInfo, enforcePurpose, enforceVendors)
+
+	if consent.CheckPubRestriction(uint8(fe.cfg.PurposeID), pubRestrictRequireConsent, vendorInfo.vendorID) {
+		return purposeAllowed
+	}
+	if consent.CheckPubRestriction(uint8(fe.cfg.PurposeID), pubRestrictRequireLegitInterest, vendorInfo.vendorID) {
+		return legitInterest
+	}
+
+	return purposeAllowed || legitInterest
+}
+
+// applyEnforceOverrides returns the enforce purpose and enforce vendor configuration values unless
+// those values have been overridden, in which case they return true
+func (fe *FullEnforcement) applyEnforceOverrides(overrides Overrides) (enforcePurpose, enforceVendors bool) {
+	enforcePurpose = fe.cfg.EnforcePurpose
+	if overrides.enforcePurpose {
+		enforcePurpose = true
+	}
+	enforceVendors = fe.cfg.EnforceVendors
+	if overrides.enforceVendors {
+		enforceVendors = true
+	}
+	return
+}
+
+// consentEstablished determines if consent has been established for a given purpose and bidder
+// based on the purpose config, user consent and the GVL. For consent to be established, the vendor
+// must declare the purpose as either consent or flex and the user must consent in accordance with
+// the purpose configs.
+func (fe *FullEnforcement) consentEstablished(consent tcf2.ConsentMetadata, vi VendorInfo, enforcePurpose bool, enforceVendors bool) bool {
+	if !vi.vendor.Purpose(fe.cfg.PurposeID) {
+		return false
+	}
+	if enforcePurpose && !consent.PurposeAllowed(fe.cfg.PurposeID) {
+		return false
+	}
+	if enforceVendors && !consent.VendorConsent(vi.vendorID) {
+		return false
+	}
+	return true
+}
+
+// legitInterestEstablished determines if legitimate interest has been established for a given
+// purpose and bidder based on the purpose config, user consent and the GVL. For consent to be
+// established, the vendor must declare the purpose as either legit interest or flex and the user
+// must have been provided notice for the legit interest basis in accordance with the purpose configs.
+func (fe *FullEnforcement) legitInterestEstablished(consent tcf2.ConsentMetadata, vi VendorInfo, enforcePurpose bool, enforceVendors bool) bool {
+	if !vi.vendor.LegitimateInterest(fe.cfg.PurposeID) {
+		return false
+	}
+	if enforcePurpose && !consent.PurposeLITransparency(fe.cfg.PurposeID) {
+		return false
+	}
+	if enforceVendors && !consent.VendorLegitInterest(vi.vendorID) {
+		return false
+	}
+	return true
+}
diff --git a/gdpr/gdpr.go b/gdpr/gdpr.go
index efad6a81..db6aa125 100644
--- a/gdpr/gdpr.go
+++ b/gdpr/gdpr.go
@@ -33,31 +33,33 @@ type RequestInfo struct {
 	PublisherID string
 }
 
-// NewPermissionsBuilder is of type PermissionsBuilderMaker
-// It takes host config data used to configure the builder function it returns
+// NewPermissionsBuilder takes host config data used to configure the builder function it returns
 func NewPermissionsBuilder(cfg config.GDPR, gvlVendorIDs map[openrtb_ext.BidderName]uint16, vendorListFetcher VendorListFetcher) PermissionsBuilder {
 	return func(tcf2Cfg TCF2ConfigReader, requestInfo RequestInfo) Permissions {
-		return NewPermissions(cfg, tcf2Cfg, gvlVendorIDs, vendorListFetcher, requestInfo)
+		purposeEnforcerBuilder := NewPurposeEnforcerBuilder(tcf2Cfg)
+
+		return NewPermissions(cfg, tcf2Cfg, gvlVendorIDs, vendorListFetcher, purposeEnforcerBuilder, requestInfo)
 	}
 }
 
 // NewPermissions gets a per-request Permissions object that can then be used to check GDPR permissions for a given bidder.
-func NewPermissions(cfg config.GDPR, tcf2Config TCF2ConfigReader, vendorIDs map[openrtb_ext.BidderName]uint16, fetcher VendorListFetcher, requestInfo RequestInfo) Permissions {
+func NewPermissions(cfg config.GDPR, tcf2Config TCF2ConfigReader, vendorIDs map[openrtb_ext.BidderName]uint16, fetcher VendorListFetcher, purposeEnforcerBuilder PurposeEnforcerBuilder, requestInfo RequestInfo) Permissions {
 	if !cfg.Enabled {
 		return &AlwaysAllow{}
 	}
 
 	permissionsImpl := &permissionsImpl{
-		fetchVendorList:       fetcher,
-		gdprDefaultValue:      cfg.DefaultValue,
-		hostVendorID:          cfg.HostVendorID,
-		nonStandardPublishers: cfg.NonStandardPublisherMap,
-		cfg:                   tcf2Config,
-		vendorIDs:             vendorIDs,
-		publisherID:           requestInfo.PublisherID,
-		gdprSignal:            SignalNormalize(requestInfo.GDPRSignal, cfg.DefaultValue),
-		consent:               requestInfo.Consent,
-		aliasGVLIDs:           requestInfo.AliasGVLIDs,
+		fetchVendorList:        fetcher,
+		gdprDefaultValue:       cfg.DefaultValue,
+		hostVendorID:           cfg.HostVendorID,
+		nonStandardPublishers:  cfg.NonStandardPublisherMap,
+		cfg:                    tcf2Config,
+		vendorIDs:              vendorIDs,
+		publisherID:            requestInfo.PublisherID,
+		gdprSignal:             SignalNormalize(requestInfo.GDPRSignal, cfg.DefaultValue),
+		consent:                requestInfo.Consent,
+		aliasGVLIDs:            requestInfo.AliasGVLIDs,
+		purposeEnforcerBuilder: purposeEnforcerBuilder,
 	}
 
 	if cfg.HostVendorID == 0 {
diff --git a/gdpr/impl.go b/gdpr/impl.go
index ebc8869a..9f9b47cd 100644
--- a/gdpr/impl.go
+++ b/gdpr/impl.go
@@ -7,33 +7,42 @@ import (
 
 	"github.com/prebid/go-gdpr/api"
 	"github.com/prebid/go-gdpr/consentconstants"
-	tcf2ConsentConstants "github.com/prebid/go-gdpr/consentconstants/tcf2"
 	"github.com/prebid/go-gdpr/vendorconsent"
 	tcf2 "github.com/prebid/go-gdpr/vendorconsent/tcf2"
 	"github.com/prebid/prebid-server/openrtb_ext"
 )
 
+const noBidder openrtb_ext.BidderName = ""
+
+// permissionsImpl contains global and request-specific GDPR config data and is used to determine
+// whether various cookie sync and auction activities are permitted for a request
+// permissionsImpl implements the Permissions interface
 type permissionsImpl struct {
-	fetchVendorList       VendorListFetcher
-	gdprDefaultValue      string
-	hostVendorID          int
-	nonStandardPublishers map[string]struct{}
-	cfg                   TCF2ConfigReader
-	vendorIDs             map[openrtb_ext.BidderName]uint16
-	consent               string
-	gdprSignal            Signal
-	publisherID           string
-	aliasGVLIDs           map[string]uint16
+	// global
+	fetchVendorList        VendorListFetcher
+	gdprDefaultValue       string
+	hostVendorID           int
+	nonStandardPublishers  map[string]struct{}
+	purposeEnforcerBuilder PurposeEnforcerBuilder
+	vendorIDs              map[openrtb_ext.BidderName]uint16
+	// request-specific
+	aliasGVLIDs map[string]uint16
+	cfg         TCF2ConfigReader
+	consent     string
+	gdprSignal  Signal
+	publisherID string
 }
 
+// HostCookiesAllowed determines whether the host is allowed to set cookies on the user's device
 func (p *permissionsImpl) HostCookiesAllowed(ctx context.Context) (bool, error) {
 	if p.gdprSignal != SignalYes {
 		return true, nil
 	}
 
-	return p.allowSync(ctx, uint16(p.hostVendorID), false)
+	return p.allowSync(ctx, uint16(p.hostVendorID), noBidder, false)
 }
 
+// BidderSyncAllowed determines whether a given bidder is allowed to perform a cookie sync
 func (p *permissionsImpl) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.BidderName) (bool, error) {
 	if p.gdprSignal != SignalYes {
 		return true, nil
@@ -41,13 +50,15 @@ func (p *permissionsImpl) BidderSyncAllowed(ctx context.Context, bidder openrtb_
 
 	id, ok := p.vendorIDs[bidder]
 	if ok {
-		vendorException := p.cfg.PurposeVendorException(consentconstants.Purpose(1), bidder)
-		return p.allowSync(ctx, id, vendorException)
+		vendorExceptions := p.cfg.PurposeVendorExceptions(consentconstants.Purpose(1))
+		_, vendorException := vendorExceptions[bidder]
+		return p.allowSync(ctx, id, bidder, vendorException)
 	}
 
 	return false, nil
 }
 
+// AuctionActivitiesAllowed determines whether auction activities are permitted for a given bidder
 func (p *permissionsImpl) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions AuctionPermissions, err error) {
 	if _, ok := p.nonStandardPublishers[p.publisherID]; ok {
 		return AllowAll, nil
@@ -61,15 +72,46 @@ func (p *permissionsImpl) AuctionActivitiesAllowed(ctx context.Context, bidderCo
 		return p.defaultPermissions(), nil
 	}
 
-	weakVendorEnforcement := p.cfg.BasicEnforcementVendor(bidder)
+	// note the bidder here is guaranteed to be enabled
+	vendorID, vendorFound := p.resolveVendorID(bidderCoreName, bidder)
+	basicEnforcementVendors := p.cfg.BasicEnforcementVendors()
+	_, weakVendorEnforcement := basicEnforcementVendors[string(bidder)]
+
+	if !vendorFound && !weakVendorEnforcement {
+		return DenyAll, nil
+	}
+
+	parsedConsent, vendor, err := p.parseVendor(ctx, vendorID, p.consent)
+	if err != nil {
+		return p.defaultPermissions(), err
+	}
+
+	// vendor will be nil if not a valid TCF2 consent string
+	if vendor == nil {
+		if weakVendorEnforcement && parsedConsent.Version() == 2 {
+			vendor = vendorTrue{}
+		} else {
+			return p.defaultPermissions(), nil
+		}
+	}
+
+	if !p.cfg.IsEnabled() {
+		return AllowBidRequestOnly, nil
+	}
 
-	if id, ok := p.resolveVendorId(bidderCoreName, bidder); ok {
-		return p.allowActivities(ctx, id, bidderCoreName, weakVendorEnforcement)
-	} else if weakVendorEnforcement {
-		return p.allowActivities(ctx, 0, bidderCoreName, weakVendorEnforcement)
+	consentMeta, ok := parsedConsent.(tcf2.ConsentMetadata)
+	if !ok {
+		err = fmt.Errorf("Unable to access TCF2 parsed consent")
+		return p.defaultPermissions(), err
 	}
 
-	return DenyAll, nil
+	vendorInfo := VendorInfo{vendorID: vendorID, vendor: vendor}
+	permissions = AuctionPermissions{}
+	permissions.AllowBidRequest = p.allowBidRequest(bidderCoreName, consentMeta, vendorInfo)
+	permissions.PassGeo = p.allowGeo(bidderCoreName, consentMeta, vendor)
+	permissions.PassID = p.allowID(bidderCoreName, consentMeta, vendorInfo)
+
+	return permissions, nil
 }
 
 // defaultPermissions returns a permissions object that denies passing user IDs while
@@ -88,7 +130,9 @@ func (p *permissionsImpl) defaultPermissions() AuctionPermissions {
 	return perms
 }
 
-func (p *permissionsImpl) resolveVendorId(bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (id uint16, ok bool) {
+// resolveVendorID gets the vendor ID for the specified bidder from either the alias GVL IDs
+// provided in the request or from the bidder configs loaded at startup
+func (p *permissionsImpl) resolveVendorID(bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (id uint16, ok bool) {
 	if id, ok = p.aliasGVLIDs[string(bidder)]; ok {
 		return id, ok
 	}
@@ -98,7 +142,9 @@ func (p *permissionsImpl) resolveVendorId(bidderCoreName openrtb_ext.BidderName,
 	return id, ok
 }
 
-func (p *permissionsImpl) allowSync(ctx context.Context, vendorID uint16, vendorException bool) (bool, error) {
+// allowSync computes cookie sync activity legal basis for a given bidder using the enforcement
+// algorithms selected by the purpose enforcer builder
+func (p *permissionsImpl) allowSync(ctx context.Context, vendorID uint16, bidder openrtb_ext.BidderName, vendorException bool) (bool, error) {
 	if p.consent == "" {
 		return false, nil
 	}
@@ -125,120 +171,65 @@ func (p *permissionsImpl) allowSync(ctx context.Context, vendorID uint16, vendor
 		return p.cfg.PurposeOneTreatmentAccessAllowed(), nil
 	}
 
-	enforceVendors := p.cfg.PurposeEnforcingVendors(tcf2ConsentConstants.InfoStorageAccess)
-	return p.checkPurpose(consentMeta, vendor, vendorID, tcf2ConsentConstants.InfoStorageAccess, enforceVendors, vendorException, false), nil
-}
-
-func (p *permissionsImpl) allowActivities(ctx context.Context, vendorID uint16, bidder openrtb_ext.BidderName, weakVendorEnforcement bool) (AuctionPermissions, error) {
-	parsedConsent, vendor, err := p.parseVendor(ctx, vendorID, p.consent)
-	if err != nil {
-		return p.defaultPermissions(), err
-	}
-
-	// vendor will be nil if not a valid TCF2 consent string
-	if vendor == nil {
-		if weakVendorEnforcement && parsedConsent.Version() == 2 {
-			vendor = vendorTrue{}
-		} else {
-			return p.defaultPermissions(), nil
-		}
-	}
+	purpose := consentconstants.Purpose(1)
+	enforcer := p.purposeEnforcerBuilder(purpose, bidder)
 
-	if !p.cfg.IsEnabled() {
-		return AllowBidRequestOnly, nil
-	}
-
-	consentMeta, ok := parsedConsent.(tcf2.ConsentMetadata)
-	if !ok {
-		err = fmt.Errorf("Unable to access TCF2 parsed consent")
-		return p.defaultPermissions(), err
-	}
-
-	permissions := AuctionPermissions{}
-	if p.cfg.FeatureOneEnforced() {
-		vendorException := p.cfg.FeatureOneVendorException(bidder)
-		permissions.PassGeo = vendorException || (consentMeta.SpecialFeatureOptIn(1) && (vendor.SpecialFeature(1) || weakVendorEnforcement))
-	} else {
-		permissions.PassGeo = true
-	}
-	if p.cfg.PurposeEnforced(consentconstants.Purpose(2)) {
-		enforceVendors := p.cfg.PurposeEnforcingVendors(consentconstants.Purpose(2))
-		vendorException := p.cfg.PurposeVendorException(consentconstants.Purpose(2), bidder)
-		permissions.AllowBidRequest = p.checkPurpose(consentMeta, vendor, vendorID, consentconstants.Purpose(2), enforceVendors, vendorException, weakVendorEnforcement)
-	} else {
-		permissions.AllowBidRequest = true
-	}
-	for i := 2; i <= 10; i++ {
-		enforceVendors := p.cfg.PurposeEnforcingVendors(consentconstants.Purpose(i))
-		vendorException := p.cfg.PurposeVendorException(consentconstants.Purpose(i), bidder)
-		if p.checkPurpose(consentMeta, vendor, vendorID, consentconstants.Purpose(i), enforceVendors, vendorException, weakVendorEnforcement) {
-			permissions.PassID = true
-			break
-		}
+	vendorInfo := VendorInfo{vendorID: vendorID, vendor: vendor}
+	if enforcer.LegalBasis(vendorInfo, bidder, consentMeta, Overrides{blockVendorExceptions: !vendorException}) {
+		return true, nil
 	}
-
-	return permissions, nil
+	return false, nil
 }
 
-const pubRestrictNotAllowed = 0
-const pubRestrictRequireConsent = 1
-const pubRestrictRequireLegitInterest = 2
-
-func (p *permissionsImpl) checkPurpose(consent tcf2.ConsentMetadata, vendor api.Vendor, vendorID uint16, purpose consentconstants.Purpose, enforceVendors, vendorException, weakVendorEnforcement bool) bool {
-	if consent.CheckPubRestriction(uint8(purpose), pubRestrictNotAllowed, vendorID) {
-		return false
-	}
-
-	if vendorException {
-		return true
-	}
-
-	purposeAllowed := p.consentEstablished(consent, vendor, vendorID, purpose, enforceVendors, weakVendorEnforcement)
-	legitInterest := p.legitInterestEstablished(consent, vendor, vendorID, purpose, enforceVendors, weakVendorEnforcement)
+// allowBidRequest computes legal basis for a given bidder using the enforcement algorithms selected
+// by the purpose enforcer builder
+func (p *permissionsImpl) allowBidRequest(bidder openrtb_ext.BidderName, consentMeta tcf2.ConsentMetadata, vendorInfo VendorInfo) bool {
+	enforcer := p.purposeEnforcerBuilder(consentconstants.Purpose(2), bidder)
 
-	if consent.CheckPubRestriction(uint8(purpose), pubRestrictRequireConsent, vendorID) {
-		return purposeAllowed
+	overrides := Overrides{}
+	if _, ok := enforcer.(*BasicEnforcement); ok {
+		overrides.allowLITransparency = true
 	}
-	if consent.CheckPubRestriction(uint8(purpose), pubRestrictRequireLegitInterest, vendorID) {
-		// Need LITransparency here
-		return legitInterest
-	}
-
-	return purposeAllowed || legitInterest
+	return enforcer.LegalBasis(vendorInfo, bidder, consentMeta, overrides)
 }
 
-func (p *permissionsImpl) consentEstablished(consent tcf2.ConsentMetadata, vendor api.Vendor, vendorID uint16, purpose consentconstants.Purpose, enforceVendors, weakVendorEnforcement bool) bool {
-	if !consent.PurposeAllowed(purpose) {
-		return false
-	}
-	if weakVendorEnforcement {
-		return true
-	}
-	if !enforceVendors {
+// allowGeo computes legal basis for a given bidder using the configs, consent and GVL pertaining to
+// feature one
+func (p *permissionsImpl) allowGeo(bidder openrtb_ext.BidderName, consentMeta tcf2.ConsentMetadata, vendor api.Vendor) bool {
+	if !p.cfg.FeatureOneEnforced() {
 		return true
 	}
-	if vendor.Purpose(purpose) && consent.VendorConsent(vendorID) {
+	if p.cfg.FeatureOneVendorException(bidder) {
 		return true
 	}
-	return false
+
+	basicEnforcementVendors := p.cfg.BasicEnforcementVendors()
+	_, weakVendorEnforcement := basicEnforcementVendors[string(bidder)]
+	return consentMeta.SpecialFeatureOptIn(1) && (vendor.SpecialFeature(1) || weakVendorEnforcement)
 }
 
-func (p *permissionsImpl) legitInterestEstablished(consent tcf2.ConsentMetadata, vendor api.Vendor, vendorID uint16, purpose consentconstants.Purpose, enforceVendors, weakVendorEnforcement bool) bool {
-	if !consent.PurposeLITransparency(purpose) {
-		return false
-	}
-	if weakVendorEnforcement {
-		return true
-	}
-	if !enforceVendors {
-		return true
-	}
-	if vendor.LegitimateInterest(purpose) && consent.VendorLegitInterest(vendorID) {
-		return true
+// allowID computes the pass user ID activity legal basis for a given bidder using the enforcement algorithms
+// selected by the purpose enforcer builder. For the user ID activity, the selected enforcement algorithm must
+// always assume we are enforcing the purpose.
+// If the purpose for which we are computing legal basis is purpose 2, the algorithm should allow LI transparency.
+func (p *permissionsImpl) allowID(bidder openrtb_ext.BidderName, consentMeta tcf2.ConsentMetadata, vendorInfo VendorInfo) bool {
+	for i := 2; i <= 10; i++ {
+		purpose := consentconstants.Purpose(i)
+		enforcer := p.purposeEnforcerBuilder(purpose, bidder)
+
+		overrides := Overrides{enforcePurpose: true, enforceVendors: true}
+		if _, ok := enforcer.(*BasicEnforcement); ok && purpose == consentconstants.Purpose(2) {
+			overrides.allowLITransparency = true
+		}
+		if enforcer.LegalBasis(vendorInfo, bidder, consentMeta, overrides) {
+			return true
+		}
 	}
+
 	return false
 }
 
+// parseVendor parses the consent string and fetches the specified vendor's information from the GVL
 func (p *permissionsImpl) parseVendor(ctx context.Context, vendorID uint16, consent string) (parsedConsent api.VendorConsents, vendor api.Vendor, err error) {
 	parsedConsent, err = vendorconsent.ParseString(consent)
 	if err != nil {
@@ -279,11 +270,9 @@ type AlwaysAllow struct{}
 func (a AlwaysAllow) HostCookiesAllowed(ctx context.Context) (bool, error) {
 	return true, nil
 }
-
 func (a AlwaysAllow) BidderSyncAllowed(ctx context.Context, bidder openrtb_ext.BidderName) (bool, error) {
 	return true, nil
 }
-
 func (a AlwaysAllow) AuctionActivitiesAllowed(ctx context.Context, bidderCoreName openrtb_ext.BidderName, bidder openrtb_ext.BidderName) (permissions AuctionPermissions, err error) {
 	return AllowAll, nil
 }
diff --git a/gdpr/purpose_config.go b/gdpr/purpose_config.go
new file mode 100644
index 00000000..015f2326
--- /dev/null
+++ b/gdpr/purpose_config.go
@@ -0,0 +1,38 @@
+package gdpr
+
+import (
+	"github.com/prebid/go-gdpr/consentconstants"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+// purposeConfig represents all of the config info selected from the host and account configs for
+// a particular purpose needed to determine legal basis using one of the GDPR enforcement algorithms
+type purposeConfig struct {
+	PurposeID                  consentconstants.Purpose
+	EnforceAlgo                config.TCF2EnforcementAlgo
+	EnforcePurpose             bool
+	EnforceVendors             bool
+	VendorExceptionMap         map[openrtb_ext.BidderName]struct{}
+	BasicEnforcementVendorsMap map[string]struct{}
+}
+
+// basicEnforcementVendor returns true if a given bidder is configured as a basic enforcement vendor
+// for the purpose
+func (pc *purposeConfig) basicEnforcementVendor(bidder openrtb_ext.BidderName) bool {
+	if pc.BasicEnforcementVendorsMap == nil {
+		return false
+	}
+	_, found := pc.BasicEnforcementVendorsMap[string(bidder)]
+	return found
+}
+
+// vendorException returns true if a given bidder is configured as a vendor exception
+// for the purpose
+func (pc *purposeConfig) vendorException(bidder openrtb_ext.BidderName) bool {
+	if pc.VendorExceptionMap == nil {
+		return false
+	}
+	_, found := pc.VendorExceptionMap[bidder]
+	return found
+}
diff --git a/gdpr/purpose_enforcer.go b/gdpr/purpose_enforcer.go
new file mode 100644
index 00000000..c8e76f98
--- /dev/null
+++ b/gdpr/purpose_enforcer.go
@@ -0,0 +1,111 @@
+package gdpr
+
+import (
+	"github.com/prebid/go-gdpr/api"
+	"github.com/prebid/go-gdpr/consentconstants"
+	tcf2 "github.com/prebid/go-gdpr/vendorconsent/tcf2"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+// PurposeEnforcer represents the enforcement strategy for determining if legal basis is achieved for a purpose
+type PurposeEnforcer interface {
+	LegalBasis(vendorInfo VendorInfo, bidder openrtb_ext.BidderName, consent tcf2.ConsentMetadata, overrides Overrides) bool
+}
+
+// PurposeEnforcerBuilder generates an instance of PurposeEnforcer for a given purpose and bidder
+type PurposeEnforcerBuilder func(p consentconstants.Purpose, bidder openrtb_ext.BidderName) PurposeEnforcer
+
+// Overrides specifies enforcement algorithm rule adjustments
+type Overrides struct {
+	allowLITransparency   bool
+	blockVendorExceptions bool
+	enforcePurpose        bool
+	enforceVendors        bool
+}
+
+type BidderInfo struct {
+	bidderCoreName openrtb_ext.BidderName
+	bidder         openrtb_ext.BidderName
+}
+type VendorInfo struct {
+	vendorID uint16
+	vendor   api.Vendor
+}
+
+// PurposeEnforcers holds the full and basic enforcers for a purpose
+type PurposeEnforcers struct {
+	Full  PurposeEnforcer
+	Basic PurposeEnforcer
+}
+
+// NewPurposeEnforcerBuilder creates a new instance of PurposeEnforcerBuilder. This function uses
+// closures so that any enforcer generated by the returned builder may use the config and also be
+// cached and reused within a request context
+func NewPurposeEnforcerBuilder(cfg TCF2ConfigReader) PurposeEnforcerBuilder {
+	cachedEnforcers := make([]PurposeEnforcers, 10)
+
+	return func(purpose consentconstants.Purpose, bidder openrtb_ext.BidderName) PurposeEnforcer {
+		index := purpose - 1
+
+		var basicEnforcementVendor bool
+		if purpose == consentconstants.Purpose(1) {
+			basicEnforcementVendor = false
+		} else {
+			basicEnforcementVendors := cfg.BasicEnforcementVendors()
+			_, basicEnforcementVendor = basicEnforcementVendors[string(bidder)]
+		}
+
+		enforceAlgo := cfg.PurposeEnforcementAlgo(purpose)
+		downgraded := isDowngraded(enforceAlgo, basicEnforcementVendor)
+
+		if enforceAlgo == config.TCF2BasicEnforcement || downgraded {
+			if cachedEnforcers[index].Basic != nil {
+				return cachedEnforcers[index].Basic
+			}
+
+			purposeCfg := purposeConfig{
+				PurposeID:                  purpose,
+				EnforceAlgo:                enforceAlgo,
+				EnforcePurpose:             cfg.PurposeEnforced(purpose),
+				EnforceVendors:             cfg.PurposeEnforcingVendors(purpose),
+				VendorExceptionMap:         cfg.PurposeVendorExceptions(purpose),
+				BasicEnforcementVendorsMap: cfg.BasicEnforcementVendors(),
+			}
+
+			enforcer := &BasicEnforcement{
+				cfg: purposeCfg,
+			}
+			cachedEnforcers[index].Basic = enforcer
+			return enforcer
+		} else {
+			if cachedEnforcers[index].Full != nil {
+				return cachedEnforcers[index].Full
+			}
+
+			purposeCfg := purposeConfig{
+				PurposeID:                  purpose,
+				EnforceAlgo:                enforceAlgo,
+				EnforcePurpose:             cfg.PurposeEnforced(purpose),
+				EnforceVendors:             cfg.PurposeEnforcingVendors(purpose),
+				VendorExceptionMap:         cfg.PurposeVendorExceptions(purpose),
+				BasicEnforcementVendorsMap: cfg.BasicEnforcementVendors(),
+			}
+
+			enforcer := &FullEnforcement{
+				cfg: purposeCfg,
+			}
+			cachedEnforcers[index].Full = enforcer
+			return enforcer
+		}
+	}
+}
+
+// isDowngraded determines if the enforcement algorithm used to determine legal basis for a
+// purpose should be downgraded from full enforcement to basic
+func isDowngraded(enforceAlgo config.TCF2EnforcementAlgo, basicEnforcementVendor bool) bool {
+	if enforceAlgo == config.TCF2FullEnforcement && basicEnforcementVendor {
+		return true
+	}
+	return false
+}
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 1af9aa93..e4c68fe6 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -113,6 +113,7 @@ const (
 	BidderApacdex           BidderName = "apacdex"
 	BidderApplogy           BidderName = "applogy"
 	BidderAppnexus          BidderName = "appnexus"
+	BidderAppush            BidderName = "appush"
 	BidderAudienceNetwork   BidderName = "audienceNetwork"
 	BidderAutomatad         BidderName = "automatad"
 	BidderAvocet            BidderName = "avocet"
@@ -131,6 +132,7 @@ const (
 	BidderBmtm              BidderName = "bmtm"
 	BidderBoldwin           BidderName = "boldwin"
 	BidderBrightroll        BidderName = "brightroll"
+	BidderCcx               BidderName = "ccx"
 	BidderCoinzilla         BidderName = "coinzilla"
 	BidderColossus          BidderName = "colossus"
 	BidderCompass           BidderName = "compass"
@@ -278,6 +280,7 @@ func CoreBidderNames() []BidderName {
 		BidderApacdex,
 		BidderApplogy,
 		BidderAppnexus,
+		BidderAppush,
 		BidderAudienceNetwork,
 		BidderAutomatad,
 		BidderAvocet,
@@ -296,6 +299,7 @@ func CoreBidderNames() []BidderName {
 		BidderBmtm,
 		BidderBoldwin,
 		BidderBrightroll,
+		BidderCcx,
 		BidderCoinzilla,
 		BidderColossus,
 		BidderCompass,
diff --git a/openrtb_ext/imp_appush.go b/openrtb_ext/imp_appush.go
new file mode 100644
index 00000000..6a383603
--- /dev/null
+++ b/openrtb_ext/imp_appush.go
@@ -0,0 +1,6 @@
+package openrtb_ext
+
+type ImpExtAppush struct {
+	PlacementID string `json:"placementId"`
+	EndpointID  string `json:"endpointId"`
+}
diff --git a/openrtb_ext/imp_ccx.go b/openrtb_ext/imp_ccx.go
new file mode 100644
index 00000000..bff400e1
--- /dev/null
+++ b/openrtb_ext/imp_ccx.go
@@ -0,0 +1,5 @@
+package openrtb_ext
+
+type ExtImpCcx struct {
+	PlacementID string `json:"placementId"`
+}
diff --git a/static/bidder-info/appush.yaml b/static/bidder-info/appush.yaml
new file mode 100644
index 00000000..68bdad48
--- /dev/null
+++ b/static/bidder-info/appush.yaml
@@ -0,0 +1,16 @@
+endpoint: "http://hb.appush.com/pserver"
+maintainer:
+  email: "support@appush.com"
+gvlVendorID: 879
+capabilities:
+  site:
+    mediaTypes:
+    - banner
+    - video
+    - native
+    
+  app:
+    mediaTypes:
+    - banner
+    - video
+    - native
\ No newline at end of file
diff --git a/static/bidder-info/ccx.yaml b/static/bidder-info/ccx.yaml
new file mode 100644
index 00000000..132a9d19
--- /dev/null
+++ b/static/bidder-info/ccx.yaml
@@ -0,0 +1,14 @@
+endpoint: "https://delivery.clickonometrics.pl/ortb/prebid/bid"
+maintainer:
+  email: "it@clickonometrics.pl"
+gvlVendorID: 773
+capabilities:
+  site:
+    mediaTypes:
+      - banner
+      - video
+userSync:
+  # Clickonometrics supports user syncing, but requires configuration by the host. contact this
+  # bidder directly at the email address in this file to ask about enabling user sync.
+  supports:
+    - redirect
diff --git a/static/bidder-params/appush.json b/static/bidder-params/appush.json
new file mode 100644
index 00000000..0e06cccf
--- /dev/null
+++ b/static/bidder-params/appush.json
@@ -0,0 +1,23 @@
+{
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "title": "Appush Adapter Params",
+    "description": "A schema which validates params accepted by the Appush adapter",
+    "type": "object",
+  
+    "properties": {
+      "placementId": {
+        "type": "string",
+        "minLength": 1,
+        "description": "Placement ID"
+      },
+      "endpointId": {
+        "type": "string",
+        "minLength": 1,
+        "description": "Endpoint ID"
+      }
+    },
+    "oneOf": [
+      { "required": ["placementId"] },
+      { "required": ["endpointId"] }
+    ]
+  }
\ No newline at end of file
diff --git a/static/bidder-params/ccx.json b/static/bidder-params/ccx.json
new file mode 100644
index 00000000..037af3db
--- /dev/null
+++ b/static/bidder-params/ccx.json
@@ -0,0 +1,13 @@
+{
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "title": "Clickonometrics Adapter Params",
+    "description": "A schema which validates params accepted by the Clickonometrics adapter",
+    "type": "object",
+    "properties": {
+      "placementId": {
+        "type": "integer",
+        "description": "Placement ID"
+      }
+    },
+    "required": ["placementId"]
+  }
\ No newline at end of file


Test patch:
diff --git a/account/account_test.go b/account/account_test.go
index 244c4318..de905043 100644
--- a/account/account_test.go
+++ b/account/account_test.go
@@ -121,6 +121,8 @@ func TestSetDerivedConfig(t *testing.T) {
 		purpose1VendorExceptions []openrtb_ext.BidderName
 		feature1VendorExceptions []openrtb_ext.BidderName
 		basicEnforcementVendors  []string
+		enforceAlgo              string
+		wantEnforceAlgoID        config.TCF2EnforcementAlgo
 	}{
 		{
 			description:              "Nil purpose 1 vendor exceptions",
@@ -158,6 +160,16 @@ func TestSetDerivedConfig(t *testing.T) {
 			description:             "Multiple basic enforcement vendors",
 			basicEnforcementVendors: []string{"appnexus", "rubicon"},
 		},
+		{
+			description:       "Basic Enforcement algorithm",
+			enforceAlgo:       config.TCF2EnforceAlgoBasic,
+			wantEnforceAlgoID: config.TCF2BasicEnforcement,
+		},
+		{
+			description:       "Full Enforcement algorithm",
+			enforceAlgo:       config.TCF2EnforceAlgoFull,
+			wantEnforceAlgoID: config.TCF2FullEnforcement,
+		},
 	}
 
 	for _, tt := range tests {
@@ -165,6 +177,7 @@ func TestSetDerivedConfig(t *testing.T) {
 			GDPR: config.AccountGDPR{
 				Purpose1: config.AccountGDPRPurpose{
 					VendorExceptions: tt.purpose1VendorExceptions,
+					EnforceAlgo:      tt.enforceAlgo,
 				},
 				SpecialFeature1: config.AccountGDPRSpecialFeature{
 					VendorExceptions: tt.feature1VendorExceptions,
@@ -193,6 +206,8 @@ func TestSetDerivedConfig(t *testing.T) {
 		assert.ElementsMatch(t, purpose1ExceptionMapKeys, tt.purpose1VendorExceptions, tt.description)
 		assert.ElementsMatch(t, feature1ExceptionMapKeys, tt.feature1VendorExceptions, tt.description)
 		assert.ElementsMatch(t, basicEnforcementMapKeys, tt.basicEnforcementVendors, tt.description)
+
+		assert.Equal(t, account.GDPR.Purpose1.EnforceAlgoID, tt.wantEnforceAlgoID, tt.description)
 	}
 }
 
diff --git a/adapters/appush/appush_test.go b/adapters/appush/appush_test.go
new file mode 100644
index 00000000..65be1c32
--- /dev/null
+++ b/adapters/appush/appush_test.go
@@ -0,0 +1,20 @@
+package appush
+
+import (
+	"testing"
+
+	"github.com/prebid/prebid-server/adapters/adapterstest"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+func TestJsonSamples(t *testing.T) {
+	bidder, buildErr := Builder(openrtb_ext.BidderAppush, config.Adapter{
+		Endpoint: "http://example.com/pserver"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})
+
+	if buildErr != nil {
+		t.Fatalf("Builder returned unexpected error %v", buildErr)
+	}
+
+	adapterstest.RunJSONBidderTest(t, "appushtest", bidder)
+}
diff --git a/adapters/appush/appushtest/exemplary/endpointId.json b/adapters/appush/appushtest/exemplary/endpointId.json
new file mode 100644
index 00000000..691e1bbc
--- /dev/null
+++ b/adapters/appush/appushtest/exemplary/endpointId.json
@@ -0,0 +1,133 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "device": {
+            "ip": "123.123.123.123",
+            "ua": "iPad"
+        },
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "endpointId": "test"
+                    }
+                }
+            }
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://example.com/pserver",
+                "body": {
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "tagid": "test",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 300,
+                                        "h": 250
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "endpointId": "test",
+                                    "type": "network"
+                                }
+                            }
+                        }
+                    ],
+                    "app": {
+                        "id": "1",
+                        "bundle": "com.wls.testwlsapplication"
+                    },
+                    "device": {
+                        "ip": "123.123.123.123",
+                        "ua": "iPad"
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "bid": [
+                                {
+                                    "id": "test_bid_id",
+                                    "impid": "test-imp-id",
+                                    "price": 0.27543,
+                                    "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://example.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                                    "cid": "test_cid",
+                                    "crid": "test_crid",
+                                    "dealid": "test_dealid",
+                                    "w": 300,
+                                    "h": 250,
+                                    "ext": {
+                                        "prebid": {
+                                            "type": "banner"
+                                        }
+                                    }
+                                }
+                            ],
+                            "seat": "appush"
+                        }
+                    ],
+                    "cur": "USD"
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "bids": [
+                {
+                    "bid": {
+                        "id": "test_bid_id",
+                        "impid": "test-imp-id",
+                        "price": 0.27543,
+                        "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://example.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                        "cid": "test_cid",
+                        "crid": "test_crid",
+                        "dealid": "test_dealid",
+                        "w": 300,
+                        "h": 250,
+                        "ext": {
+                            "prebid": {
+                                "type": "banner"
+                            }
+                        }
+                    },
+                    "type": "banner"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/appush/appushtest/exemplary/simple-banner.json b/adapters/appush/appushtest/exemplary/simple-banner.json
new file mode 100644
index 00000000..5804d6bf
--- /dev/null
+++ b/adapters/appush/appushtest/exemplary/simple-banner.json
@@ -0,0 +1,133 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "device": {
+            "ip": "123.123.123.123",
+            "ua": "iPad"
+        },
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "placementId": "test"
+                    }
+                }
+            }
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://example.com/pserver",
+                "body": {
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "tagid": "test",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 300,
+                                        "h": 250
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "placementId": "test",
+                                    "type": "publisher"
+                                }
+                            }
+                        }
+                    ],
+                    "app": {
+                        "id": "1",
+                        "bundle": "com.wls.testwlsapplication"
+                    },
+                    "device": {
+                        "ip": "123.123.123.123",
+                        "ua": "iPad"
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "bid": [
+                                {
+                                    "id": "test_bid_id",
+                                    "impid": "test-imp-id",
+                                    "price": 0.27543,
+                                    "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://example.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                                    "cid": "test_cid",
+                                    "crid": "test_crid",
+                                    "dealid": "test_dealid",
+                                    "w": 300,
+                                    "h": 250,
+                                    "ext": {
+                                        "prebid": {
+                                            "type": "banner"
+                                        }
+                                    }
+                                }
+                            ],
+                            "seat": "appush"
+                        }
+                    ],
+                    "cur": "USD"
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "bids": [
+                {
+                    "bid": {
+                        "id": "test_bid_id",
+                        "impid": "test-imp-id",
+                        "price": 0.27543,
+                        "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://example.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                        "cid": "test_cid",
+                        "crid": "test_crid",
+                        "dealid": "test_dealid",
+                        "w": 300,
+                        "h": 250,
+                        "ext": {
+                            "prebid": {
+                                "type": "banner"
+                            }
+                        }
+                    },
+                    "type": "banner"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/appush/appushtest/exemplary/simple-native.json b/adapters/appush/appushtest/exemplary/simple-native.json
new file mode 100644
index 00000000..633216b2
--- /dev/null
+++ b/adapters/appush/appushtest/exemplary/simple-native.json
@@ -0,0 +1,117 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "device": {
+            "ip": "123.123.123.123",
+            "ua": "iPad"
+        },
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "native": {
+                    "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}",
+                    "ver": "1.1"
+                },
+                "ext": {
+                    "bidder": {
+                        "placementId": "test"
+                    }
+                }
+            }
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://example.com/pserver",
+                "body": {
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "tagid": "test",
+                            "native": {
+                                "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}",
+                                "ver": "1.1"
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "placementId": "test",
+                                    "type": "publisher"
+                                }
+                            }
+                        }
+                    ],
+                    "app": {
+                        "id": "1",
+                        "bundle": "com.wls.testwlsapplication"
+                    },
+                    "device": {
+                        "ip": "123.123.123.123",
+                        "ua": "iPad"
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "bid": [
+                                {
+                                    "id": "test_bid_id",
+                                    "impid": "test-imp-id",
+                                    "price": 0.27543,
+                                    "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://example.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                                    "cid": "test_cid",
+                                    "crid": "test_crid",
+                                    "dealid": "test_dealid",
+                                    "w": 300,
+                                    "h": 250,
+                                    "ext": {
+                                        "prebid": {
+                                            "type": "native"
+                                        }
+                                    }
+                                }
+                            ],
+                            "seat": "appush"
+                        }
+                    ],
+                    "cur": "USD"
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "bids": [
+                {
+                    "bid": {
+                        "id": "test_bid_id",
+                        "impid": "test-imp-id",
+                        "price": 0.27543,
+                        "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://example.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                        "cid": "test_cid",
+                        "crid": "test_crid",
+                        "dealid": "test_dealid",
+                        "w": 300,
+                        "h": 250,
+                        "ext": {
+                            "prebid": {
+                                "type": "native"
+                            }
+                        }
+                    },
+                    "type": "native"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/appush/appushtest/exemplary/simple-video.json b/adapters/appush/appushtest/exemplary/simple-video.json
new file mode 100644
index 00000000..21a0a03d
--- /dev/null
+++ b/adapters/appush/appushtest/exemplary/simple-video.json
@@ -0,0 +1,128 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "device": {
+            "ip": "123.123.123.123",
+            "ua": "iPad"
+        },
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "video": {
+                    "mimes": [
+                        "video/mp4"
+                    ],
+                    "protocols": [
+                        2,
+                        5
+                    ],
+                    "w": 1024,
+                    "h": 576
+                },
+                "ext": {
+                    "bidder": {
+                        "placementId": "test"
+                    }
+                }
+            }
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://example.com/pserver",
+                "body": {
+                    "id": "test-request-id",
+                    "device": {
+                        "ip": "123.123.123.123",
+                        "ua": "iPad"
+                    },
+                    "app": {
+                        "id": "1",
+                        "bundle": "com.wls.testwlsapplication"
+                    },
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "tagid": "test",
+                            "video": {
+                                "mimes": [
+                                    "video/mp4"
+                                ],
+                                "protocols": [
+                                    2,
+                                    5
+                                ],
+                                "w": 1024,
+                                "h": 576
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "placementId": "test",
+                                    "type": "publisher"
+                                }
+                            }
+                        }
+                    ]
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "bid": [
+                                {
+                                    "id": "test_bid_id",
+                                    "impid": "test-imp-id",
+                                    "price": 0.27543,
+                                    "adm": "<VAST version=\"3.0\"><Ad id=\"20001\" sequence=\"1\"><InLine><AdSystem version=\"4.0\"><![CDATA[iabtechlab]]></AdSystem><AdTitle><![CDATA[Inline Simple Ad]]></AdTitle><Impression><![CDATA[]]></Impression><Creatives><Creative id=\"5480\" sequence=\"1\"><Linear><Duration>00:01:00</Duration><MediaFiles><MediaFile id=\"5246\" delivery=\"progressive\" type=\"video/mp4\" bitrate=\"600\" width=\"640\" height=\"360\" minBitrate=\"500\" maxBitrate=\"700\" scalable=\"1\" maintainAspectRatio=\"1\" codec=\"0\"><![CDATA[https://s0.2mdn.net/4253510/google_ddm_animation_480P.mp4]]></MediaFile></MediaFiles><VideoClicks><ClickThrough id=\"blog\"><![CDATA[https://example.com]]></ClickThrough></VideoClicks></Linear></Creative></Creatives></InLine></Ad></VAST>",
+                                    "cid": "test_cid",
+                                    "crid": "test_crid",
+                                    "dealid": "test_dealid",
+                                    "ext": {
+                                        "prebid": {
+                                            "type": "video"
+                                        }
+                                    }
+                                }
+                            ],
+                            "seat": "appush"
+                        }
+                    ],
+                    "cur": "USD"
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "currency": "USD",
+            "bids": [
+                {
+                    "bid": {
+                        "id": "test_bid_id",
+                        "impid": "test-imp-id",
+                        "price": 0.27543,
+                        "adm": "<VAST version=\"3.0\"><Ad id=\"20001\" sequence=\"1\"><InLine><AdSystem version=\"4.0\"><![CDATA[iabtechlab]]></AdSystem><AdTitle><![CDATA[Inline Simple Ad]]></AdTitle><Impression><![CDATA[]]></Impression><Creatives><Creative id=\"5480\" sequence=\"1\"><Linear><Duration>00:01:00</Duration><MediaFiles><MediaFile id=\"5246\" delivery=\"progressive\" type=\"video/mp4\" bitrate=\"600\" width=\"640\" height=\"360\" minBitrate=\"500\" maxBitrate=\"700\" scalable=\"1\" maintainAspectRatio=\"1\" codec=\"0\"><![CDATA[https://s0.2mdn.net/4253510/google_ddm_animation_480P.mp4]]></MediaFile></MediaFiles><VideoClicks><ClickThrough id=\"blog\"><![CDATA[https://example.com]]></ClickThrough></VideoClicks></Linear></Creative></Creatives></InLine></Ad></VAST>",
+                        "cid": "test_cid",
+                        "crid": "test_crid",
+                        "dealid": "test_dealid",
+                        "ext": {
+                            "prebid": {
+                                "type": "video"
+                            }
+                        }
+                    },
+                    "type": "video"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/appush/appushtest/exemplary/simple-web-banner.json b/adapters/appush/appushtest/exemplary/simple-web-banner.json
new file mode 100644
index 00000000..431170dc
--- /dev/null
+++ b/adapters/appush/appushtest/exemplary/simple-web-banner.json
@@ -0,0 +1,133 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "placementId": "test"
+                    }
+                }
+            }
+        ],
+        "site": {
+            "id": "1",
+            "domain": "test.com"
+        },
+        "device": {
+            "ip": "123.123.123.123",
+            "ua": "Ubuntu"
+        }
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://example.com/pserver",
+                "body": {
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "tagid": "test",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 300,
+                                        "h": 250
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "placementId": "test",
+                                    "type": "publisher"
+                                }
+                            }
+                        }
+                    ],
+                    "site": {
+                        "id": "1",
+                        "domain": "test.com"
+                    },
+                    "device": {
+                        "ip": "123.123.123.123",
+                        "ua": "Ubuntu"
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "bid": [
+                                {
+                                    "id": "test_bid_id",
+                                    "impid": "test-imp-id",
+                                    "price": 0.27543,
+                                    "adm": "<iframe id=\"adm-banner-1\" width=\"468\" height=\"60\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://example.com/pserver&k=bc2d316f39931a07d9a8dd249bf85fc0\"></iframe>",
+                                    "cid": "test_cid",
+                                    "crid": "test_crid",
+                                    "dealid": "test_dealid",
+                                    "w": 468,
+                                    "h": 60,
+                                    "ext": {
+                                        "prebid": {
+                                            "type": "banner"
+                                        }
+                                    }
+                                }
+                            ],
+                            "seat": "appush"
+                        }
+                    ],
+                    "cur": "USD"
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "bids": [
+                {
+                    "bid": {
+                        "id": "test_bid_id",
+                        "impid": "test-imp-id",
+                        "price": 0.27543,
+                        "adm": "<iframe id=\"adm-banner-1\" width=\"468\" height=\"60\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://example.com/pserver&k=bc2d316f39931a07d9a8dd249bf85fc0\"></iframe>",
+                        "cid": "test_cid",
+                        "crid": "test_crid",
+                        "dealid": "test_dealid",
+                        "w": 468,
+                        "h": 60,
+                        "ext": {
+                            "prebid": {
+                                "type": "banner"
+                            }
+                        }
+                    },
+                    "type": "banner"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/appush/appushtest/supplemental/bad_media_type.json b/adapters/appush/appushtest/supplemental/bad_media_type.json
new file mode 100644
index 00000000..6489c38a
--- /dev/null
+++ b/adapters/appush/appushtest/supplemental/bad_media_type.json
@@ -0,0 +1,86 @@
+{
+	"mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "ext": {
+                    "bidder": {
+                        "placementId": "test"
+                    }
+                }
+            }
+        ],
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "device": {
+            "ip": "123.123.123.123",
+            "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+        }
+    },
+	"httpCalls": [{
+		"expectedRequest": {
+			"uri": "http://example.com/pserver",
+			"body": {
+                "id": "test-request-id",
+                "imp": [
+                    {
+                        "id": "test-imp-id",
+                        "ext": {
+                            "bidder": {
+                                "placementId": "test",
+                                "type": "publisher"
+                            }
+                        }
+                    }
+                ],
+                "app": {
+                    "id": "1",
+                    "bundle": "com.wls.testwlsapplication"
+                },
+                "device": {
+                    "ip": "123.123.123.123",
+                    "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+                }
+            }
+		},
+		"mockResponse": {
+			"status": 200,
+			"body": {
+                "id": "test-request-id",
+                "seatbid": [
+                    {
+                        "bid": [
+                            {
+                                "id": "test_bid_id",
+                                "impid": "test-imp-id",
+                                "price": 0.27543,
+                                "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://example.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                                "cid": "test_cid",
+                                "crid": "test_crid",
+                                "dealid": "test_dealid",
+                                "w": 300,
+                                "h": 250,
+                                "ext": {
+                                    "prebid": {
+                                        "type": "banner"
+                                    }
+                                }
+                            }
+                        ],
+                        "seat": "appush"
+                    }
+                ],
+                "cur": "USD"
+            }
+        }
+    }],
+    "expectedMakeBidsErrors": [
+        {
+          "value": "Failed to find impression \"test-imp-id\"",
+          "comparison": "literal"
+        }
+    ]
+}
diff --git a/adapters/appush/appushtest/supplemental/bad_response.json b/adapters/appush/appushtest/supplemental/bad_response.json
new file mode 100644
index 00000000..8c1cc375
--- /dev/null
+++ b/adapters/appush/appushtest/supplemental/bad_response.json
@@ -0,0 +1,84 @@
+{
+	"mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "placementId": "test"
+                    }
+                }
+            }
+        ],
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "device": {
+            "ip": "123.123.123.123",
+            "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+        }
+    },
+	"httpCalls": [{
+		"expectedRequest": {
+			"uri": "http://example.com/pserver",
+			"body": {
+                "id": "test-request-id",
+                "imp": [
+                    {
+                        "id": "test-imp-id",
+                        "banner": {
+                            "format": [
+                                {
+                                    "w": 300,
+                                    "h": 250
+                                },
+                                {
+                                    "w": 300,
+                                    "h": 600
+                                }
+                            ]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "placementId": "test",
+                                "type": "publisher"
+                            }
+                        }
+                    }
+                ],
+                "app": {
+                    "id": "1",
+                    "bundle": "com.wls.testwlsapplication"
+                },
+                "device": {
+                    "ip": "123.123.123.123",
+                    "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+                }
+            }
+		},
+		"mockResponse": {
+			"status": 200,
+			"body": ""
+        }
+    }],
+    "expectedMakeBidsErrors": [
+        {
+          "value": "json: cannot unmarshal string into Go value of type openrtb2.BidResponse",
+          "comparison": "literal"
+        }
+    ]
+}
diff --git a/adapters/appush/appushtest/supplemental/status-204.json b/adapters/appush/appushtest/supplemental/status-204.json
new file mode 100644
index 00000000..3e536c25
--- /dev/null
+++ b/adapters/appush/appushtest/supplemental/status-204.json
@@ -0,0 +1,79 @@
+{
+	"mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "placementId": "test"
+                    }
+                }
+            }
+        ],
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "device": {
+            "ip": "123.123.123.123",
+            "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+        }
+    },
+	"httpCalls": [{
+		"expectedRequest": {
+			"uri": "http://example.com/pserver",
+			"body": {
+                "id": "test-request-id",
+                "imp": [
+                    {
+                        "id": "test-imp-id",
+                        "banner": {
+                            "format": [
+                                {
+                                    "w": 300,
+                                    "h": 250
+                                },
+                                {
+                                    "w": 300,
+                                    "h": 600
+                                }
+                            ]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "placementId": "test",
+                                "type": "publisher"
+                            }
+                        }
+                    }
+                ],
+                "app": {
+                    "id": "1",
+                    "bundle": "com.wls.testwlsapplication"
+                },
+                "device": {
+                    "ip": "123.123.123.123",
+                    "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+                }
+            }
+		},
+		"mockResponse": {
+			"status": 204,
+			"body": {}
+		}
+    }],
+    "expectedBidResponses": []
+}
diff --git a/adapters/appush/appushtest/supplemental/status-not-200.json b/adapters/appush/appushtest/supplemental/status-not-200.json
new file mode 100644
index 00000000..b60ca2cc
--- /dev/null
+++ b/adapters/appush/appushtest/supplemental/status-not-200.json
@@ -0,0 +1,84 @@
+{
+	"mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "placementId": "test"
+                    }
+                }
+            }
+        ],
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "device": {
+            "ip": "123.123.123.123",
+            "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+        }
+    },
+	"httpCalls": [{
+		"expectedRequest": {
+			"uri": "http://example.com/pserver",
+			"body": {
+                "id": "test-request-id",
+                "imp": [
+                    {
+                        "id": "test-imp-id",
+                        "banner": {
+                            "format": [
+                                {
+                                    "w": 300,
+                                    "h": 250
+                                },
+                                {
+                                    "w": 300,
+                                    "h": 600
+                                }
+                            ]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "placementId": "test",
+                                "type": "publisher"
+                            }
+                        }
+                    }
+                ],
+                "app": {
+                    "id": "1",
+                    "bundle": "com.wls.testwlsapplication"
+                },
+                "device": {
+                    "ip": "123.123.123.123",
+                    "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+                }
+            }
+		},
+		"mockResponse": {
+			"status": 404,
+			"body": {}
+		}
+    }],
+    "expectedMakeBidsErrors": [
+        {
+          "value": "Unexpected status code: 404. Run with request.debug = 1 for more info.",
+          "comparison": "literal"
+        }
+    ]
+}
diff --git a/adapters/appush/params_test.go b/adapters/appush/params_test.go
new file mode 100644
index 00000000..a471c3f0
--- /dev/null
+++ b/adapters/appush/params_test.go
@@ -0,0 +1,47 @@
+package appush
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+func TestValidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json schema. %v", err)
+	}
+
+	for _, p := range validParams {
+		if err := validator.Validate(openrtb_ext.BidderAppush, json.RawMessage(p)); err != nil {
+			t.Errorf("Schema rejected valid params: %s", p)
+		}
+	}
+}
+
+func TestInvalidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json schema. %v", err)
+	}
+
+	for _, p := range invalidParams {
+		if err := validator.Validate(openrtb_ext.BidderAppush, json.RawMessage(p)); err == nil {
+			t.Errorf("Schema allowed invalid params: %s", p)
+		}
+	}
+}
+
+var validParams = []string{
+	`{"placementId": "test"}`,
+	`{"placementId": "1"}`,
+	`{"endpointId": "test"}`,
+	`{"endpointId": "1"}`,
+}
+
+var invalidParams = []string{
+	`{"placementId": 42}`,
+	`{"endpointId": 42}`,
+	`{"placementId": "1", "endpointId": "1"}`,
+}
diff --git a/adapters/ccx/ccx_test.go b/adapters/ccx/ccx_test.go
new file mode 100644
index 00000000..4d02c984
--- /dev/null
+++ b/adapters/ccx/ccx_test.go
@@ -0,0 +1,20 @@
+package ccx
+
+import (
+	"testing"
+
+	"github.com/prebid/prebid-server/adapters/adapterstest"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+func TestJsonSamples(t *testing.T) {
+	bidder, buildErr := Builder(openrtb_ext.BidderCcx, config.Adapter{
+		Endpoint: "https://delivery.clickonometrics.pl/ortb/prebid/bid"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})
+
+	if buildErr != nil {
+		t.Fatalf("Builder returned unexpected error %v", buildErr)
+	}
+
+	adapterstest.RunJSONBidderTest(t, "ccxtest", bidder)
+}
diff --git a/adapters/ccx/ccxtest/exemplary/multi-banner.json b/adapters/ccx/ccxtest/exemplary/multi-banner.json
new file mode 100644
index 00000000..0d6fb225
--- /dev/null
+++ b/adapters/ccx/ccxtest/exemplary/multi-banner.json
@@ -0,0 +1,181 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-multi-id",
+    "imp": [
+      {
+        "id": "test-imp-id-1",
+        "banner": {
+          "format": [{"w": 300, "h": 250}]
+        },
+        "ext": {
+          "bidder": {
+            "placementId": 123456789
+          }
+        }
+      },
+      {
+        "id": "test-imp-id-2",
+        "banner": {
+          "format": [{"w": 320, "h": 480}]
+        },
+        "ext": {
+          "bidder": {
+            "placementId": 123454321
+          }
+        }
+      }
+    ],
+    "site": {
+      "page": "somepage.com"
+    },
+    "user": {
+      "buyeruid": "someuid",
+      "ext": {
+        "consent" : "dummy"
+      }
+    },
+    "regs": {
+      "ext": {
+       "gdpr":1
+      }
+    }
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://delivery.clickonometrics.pl/ortb/prebid/bid",
+        "body": {
+          "id": "test-request-multi-id",
+          "imp": [
+            {
+              "id": "test-imp-id-1",
+              "banner": {
+                "format": [{"w": 300, "h": 250}]
+              },
+              "ext": {
+                "bidder": {
+                  "placementId": 123456789
+                }
+              }
+            },
+            {
+              "id": "test-imp-id-2",
+              "banner": {
+                "format": [{"w": 320, "h": 480}]
+              },
+              "ext": {
+                "bidder": {
+                  "placementId": 123454321
+                }
+              }
+            }
+          ],
+          "site": {
+              "page": "somepage.com"
+          },
+          "user": {
+            "buyeruid": "someuid",
+            "ext": {
+              "consent" : "dummy"
+            }
+          },
+          "regs": {
+            "ext": {
+             "gdpr":1
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-multi-id",
+          "seatbid": [
+            {
+              "bid": [{
+                "id": "11111111-2222-33333333",
+                "impid": "test-imp-id-1",
+                "price": 0.500000,
+                "adm": "some-test-ad-1",
+                "adomain":[
+                  "somedomain.com"
+                 ],
+                "crid": "1234",
+                "w": 300,
+                "h": 250,
+                "ext": {
+                  "type": "standard"
+                }
+              },
+              {
+                "id": "11111111-2222-44444444",
+                "impid": "test-imp-id-2",
+                "price": 0.500000,
+                "adm": "some-test-ad-2",
+                "adomain":[
+                  "somedomain.com"
+                 ],
+                "crid": "4321",
+                "w": 320,
+                "h": 480,
+                "ext": {
+                  "type": "standard"
+                }
+              }]
+            }
+          ],
+          "cur": "PLN",
+          "ext": {
+            "ttl": 10,
+            "usersync":[]
+          }
+        }
+      }
+    }
+  ],
+
+  "expectedBidResponses": [
+    {
+      "currency": "PLN",
+      "bids": [
+        {
+          "bid": {
+            "id": "11111111-2222-33333333",
+            "impid": "test-imp-id-1",
+            "price": 0.5,
+            "adm": "some-test-ad-1",
+            "adomain":[
+              "somedomain.com"
+             ],
+            "crid": "1234",
+            "w": 300,
+            "h": 250,
+            "ext": {
+              "type": "standard"
+            }
+          },
+          "type": "banner"
+        },
+        {
+          "bid": {
+            "id": "11111111-2222-44444444",
+            "impid": "test-imp-id-2",
+            "price": 0.5,
+            "adm": "some-test-ad-2",
+            "adomain":[
+              "somedomain.com"
+             ],
+            "crid": "4321",
+            "w": 320,
+            "h": 480,
+            "ext": {
+              "type": "standard"
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/ccx/ccxtest/exemplary/simple-banner.json b/adapters/ccx/ccxtest/exemplary/simple-banner.json
new file mode 100644
index 00000000..a9ea10c6
--- /dev/null
+++ b/adapters/ccx/ccxtest/exemplary/simple-banner.json
@@ -0,0 +1,126 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [{"w": 300, "h": 250}]
+        },
+        "ext": {
+          "bidder": {
+            "placementId": 123456789
+          }
+        }
+      }
+    ],
+    "site": {
+      "page": "somepage.com"
+    },
+    "user": {
+      "buyeruid": "someuid",
+      "ext": {
+        "consent" : "dummy"
+      }
+    },
+    "regs": {
+      "ext": {
+       "gdpr":1
+      }
+    }
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://delivery.clickonometrics.pl/ortb/prebid/bid",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "banner": {
+                "format": [{"w": 300, "h": 250}]
+              },
+              "ext": {
+                "bidder": {
+                  "placementId": 123456789
+                }
+              }
+            }
+          ],
+          "site": {
+              "page": "somepage.com"
+          },
+          "user": {
+            "buyeruid": "someuid",
+            "ext": {
+              "consent" : "dummy"
+            }
+          },
+          "regs": {
+            "ext": {
+             "gdpr":1
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "bid": [{
+                "id": "11111111-2222-33333333",
+                "impid": "test-imp-id",
+                "price": 0.500000,
+                "adm": "some-test-ad",
+                "adomain":[
+                  "somedomain.com"
+                 ],
+                "crid": "1234",
+                "w": 300,
+                "h": 250,
+                "ext": {
+                  "type": "standard"
+                }
+              }]
+            }
+          ],
+          "cur": "PLN",
+          "ext": {
+            "ttl": 10,
+            "usersync":[]
+          }
+        }
+      }
+    }
+  ],
+
+  "expectedBidResponses": [
+    {
+      "currency": "PLN",
+      "bids": [
+        {
+          "bid": {
+            "id": "11111111-2222-33333333",
+            "impid": "test-imp-id",
+            "price": 0.5,
+            "adm": "some-test-ad",
+            "adomain":[
+              "somedomain.com"
+             ],
+            "crid": "1234",
+            "w": 300,
+            "h": 250,
+            "ext": {
+              "type": "standard"
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/ccx/ccxtest/supplemental/204-response-from-target.json b/adapters/ccx/ccxtest/supplemental/204-response-from-target.json
new file mode 100644
index 00000000..75b4dfd1
--- /dev/null
+++ b/adapters/ccx/ccxtest/supplemental/204-response-from-target.json
@@ -0,0 +1,53 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [{"w": 300, "h": 250}]
+        },
+        "ext": {
+          "bidder": {
+            "placementId": 123456789
+          }
+        }
+      }
+    ],
+    "site": {
+      "page": "somepage.com"
+    }
+  },
+  
+    "httpCalls": [
+      {
+        "expectedRequest": {
+          "uri": "https://delivery.clickonometrics.pl/ortb/prebid/bid",
+          "body": {
+            "id": "test-request-id",
+            "imp": [
+              {
+                "id": "test-imp-id",
+                "banner": {
+                  "format": [{"w": 300, "h": 250}]
+                },
+                "ext": {
+                  "bidder": {
+                    "placementId": 123456789
+                  }
+                }
+              }
+            ],
+            "site": {
+                "page": "somepage.com"
+            }
+          }
+        },
+        "mockResponse": {
+          "status": 204
+        }
+      }
+    ],
+    "expectedMakeBidsErrors": []
+  }
+  
\ No newline at end of file
diff --git a/adapters/ccx/ccxtest/supplemental/400-response-from-target.json b/adapters/ccx/ccxtest/supplemental/400-response-from-target.json
new file mode 100644
index 00000000..a57394cd
--- /dev/null
+++ b/adapters/ccx/ccxtest/supplemental/400-response-from-target.json
@@ -0,0 +1,58 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [{"w": 300, "h": 250}]
+        },
+        "ext": {
+          "bidder": {
+            "placementId": 123456789
+          }
+        }
+      }
+    ],
+    "site": {
+      "page": "somepage.com"
+    }
+  },
+  
+    "httpCalls": [
+      {
+        "expectedRequest": {
+          "uri": "https://delivery.clickonometrics.pl/ortb/prebid/bid",
+          "body": {
+            "id": "test-request-id",
+            "imp": [
+              {
+                "id": "test-imp-id",
+                "banner": {
+                  "format": [{"w": 300, "h": 250}]
+                },
+                "ext": {
+                  "bidder": {
+                    "placementId": 123456789
+                  }
+                }
+              }
+            ],
+            "site": {
+                "page": "somepage.com"
+            }
+          }
+        },
+        "mockResponse": {
+          "status": 400
+        }
+      }
+    ],
+    "expectedMakeBidsErrors": [
+        {
+            "value": "Unexpected status code: 400. Bad request from publisher. Run with request.debug = 1 for more info.",
+            "comparison": "literal"
+        }
+    ]
+  }
+  
\ No newline at end of file
diff --git a/adapters/ccx/ccxtest/supplemental/500-response-from-target.json b/adapters/ccx/ccxtest/supplemental/500-response-from-target.json
new file mode 100644
index 00000000..2ddd51f9
--- /dev/null
+++ b/adapters/ccx/ccxtest/supplemental/500-response-from-target.json
@@ -0,0 +1,58 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [{"w": 300, "h": 250}]
+        },
+        "ext": {
+          "bidder": {
+            "placementId": 123456789
+          }
+        }
+      }
+    ],
+    "site": {
+      "page": "somepage.com"
+    }
+  },
+  
+    "httpCalls": [
+      {
+        "expectedRequest": {
+          "uri": "https://delivery.clickonometrics.pl/ortb/prebid/bid",
+          "body": {
+            "id": "test-request-id",
+            "imp": [
+              {
+                "id": "test-imp-id",
+                "banner": {
+                  "format": [{"w": 300, "h": 250}]
+                },
+                "ext": {
+                  "bidder": {
+                    "placementId": 123456789
+                  }
+                }
+              }
+            ],
+            "site": {
+                "page": "somepage.com"
+            }
+          }
+        },
+        "mockResponse": {
+          "status": 500
+        }
+      }
+    ],
+    "expectedMakeBidsErrors": [
+        {
+            "value": "Unexpected status code: 500. Run with request.debug = 1 for more info.",
+            "comparison": "literal"
+        }
+    ]
+  }
+  
\ No newline at end of file
diff --git a/adapters/ccx/params_test.go b/adapters/ccx/params_test.go
new file mode 100644
index 00000000..ecd94213
--- /dev/null
+++ b/adapters/ccx/params_test.go
@@ -0,0 +1,48 @@
+package ccx
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+func TestValidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json schema. %v", err)
+	}
+
+	for _, p := range validParams {
+		if err := validator.Validate(openrtb_ext.BidderCcx, json.RawMessage(p)); err != nil {
+			t.Errorf("Schema rejected valid params: %s", p)
+		}
+	}
+}
+
+func TestInvalidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json schema. %v", err)
+	}
+
+	for _, p := range invalidParams {
+		if err := validator.Validate(openrtb_ext.BidderCcx, json.RawMessage(p)); err == nil {
+			t.Errorf("Schema allowed invalid params: %s", p)
+		}
+	}
+}
+
+var validParams = []string{
+	`{"placementId": 123456789}`,
+}
+
+var invalidParams = []string{
+	``,
+	`null`,
+	`true`,
+	`[]`,
+	`{}`,
+	`{"placementId": ""}`,
+	`{"placementId": "123456789"}`,
+}
diff --git a/adapters/pubmatic/pubmatic_test.go b/adapters/pubmatic/pubmatic_test.go
index 96aef7a9..9e7a97d2 100644
--- a/adapters/pubmatic/pubmatic_test.go
+++ b/adapters/pubmatic/pubmatic_test.go
@@ -3,6 +3,8 @@ package pubmatic
 import (
 	"encoding/json"
 	"net/http"
+	"sort"
+	"strings"
 	"testing"
 
 	"github.com/prebid/openrtb/v17/openrtb2"
@@ -495,3 +497,211 @@ func Test_getAlternateBidderCodesFromRequest(t *testing.T) {
 		})
 	}
 }
+
+func TestPopulateFirstPartyDataImpAttributes(t *testing.T) {
+	type args struct {
+		data      json.RawMessage
+		impExtMap map[string]interface{}
+	}
+	tests := []struct {
+		name           string
+		args           args
+		expectedImpExt map[string]interface{}
+	}{
+		{
+			name: "Only Targeting present in imp.ext.data",
+			args: args{
+				data:      json.RawMessage(`{"sport":["rugby","cricket"]}`),
+				impExtMap: map[string]interface{}{},
+			},
+			expectedImpExt: map[string]interface{}{
+				"key_val": "sport=rugby,cricket",
+			},
+		},
+		{
+			name: "Targeting and adserver object present in imp.ext.data",
+			args: args{
+				data:      json.RawMessage(`{"adserver": {"name": "gam","adslot": "/1111/home"},"pbadslot": "/2222/home","sport":["rugby","cricket"]}`),
+				impExtMap: map[string]interface{}{},
+			},
+			expectedImpExt: map[string]interface{}{
+				"dfp_ad_unit_code": "/1111/home",
+				"key_val":          "sport=rugby,cricket",
+			},
+		},
+		{
+			name: "Targeting and pbadslot key present in imp.ext.data ",
+			args: args{
+				data:      json.RawMessage(`{"pbadslot": "/2222/home","sport":["rugby","cricket"]}`),
+				impExtMap: map[string]interface{}{},
+			},
+			expectedImpExt: map[string]interface{}{
+				"dfp_ad_unit_code": "/2222/home",
+				"key_val":          "sport=rugby,cricket",
+			},
+		},
+		{
+			name: "Targeting and Invalid Adserver object in imp.ext.data",
+			args: args{
+				data:      json.RawMessage(`{"adserver": "invalid","sport":["rugby","cricket"]}`),
+				impExtMap: map[string]interface{}{},
+			},
+			expectedImpExt: map[string]interface{}{
+				"key_val": "sport=rugby,cricket",
+			},
+		},
+		{
+			name: "key_val already present in imp.ext.data",
+			args: args{
+				data: json.RawMessage(`{"sport":["rugby","cricket"]}`),
+				impExtMap: map[string]interface{}{
+					"key_val": "k1=v1|k2=v2",
+				},
+			},
+			expectedImpExt: map[string]interface{}{
+				"key_val": "k1=v1|k2=v2|sport=rugby,cricket",
+			},
+		},
+		{
+			name: "int data present in imp.ext.data",
+			args: args{
+				data:      json.RawMessage(`{"age": 25}`),
+				impExtMap: map[string]interface{}{},
+			},
+			expectedImpExt: map[string]interface{}{
+				"key_val": "age=25",
+			},
+		},
+		{
+			name: "float data present in imp.ext.data",
+			args: args{
+				data:      json.RawMessage(`{"floor": 0.15}`),
+				impExtMap: map[string]interface{}{},
+			},
+			expectedImpExt: map[string]interface{}{
+				"key_val": "floor=0.15",
+			},
+		},
+		{
+			name: "bool data present in imp.ext.data",
+			args: args{
+				data:      json.RawMessage(`{"k1": true}`),
+				impExtMap: map[string]interface{}{},
+			},
+			expectedImpExt: map[string]interface{}{
+				"key_val": "k1=true",
+			},
+		},
+		{
+			name: "imp.ext.data is not present",
+			args: args{
+				data:      nil,
+				impExtMap: map[string]interface{}{},
+			},
+			expectedImpExt: map[string]interface{}{},
+		},
+		{
+			name: "string with spaces present in imp.ext.data",
+			args: args{
+				data:      json.RawMessage(`{"  category  ": "   cinema  "}`),
+				impExtMap: map[string]interface{}{},
+			},
+			expectedImpExt: map[string]interface{}{
+				"key_val": "category=cinema",
+			},
+		},
+		{
+			name: "string array with spaces present in imp.ext.data",
+			args: args{
+				data:      json.RawMessage(`{"  country\t": ["  India", "\tChina  "]}`),
+				impExtMap: map[string]interface{}{},
+			},
+			expectedImpExt: map[string]interface{}{
+				"key_val": "country=India,China",
+			},
+		},
+		{
+			name: "Invalid data present in imp.ext.data",
+			args: args{
+				data:      json.RawMessage(`{"country": [1, "India"],"category":"movies"}`),
+				impExtMap: map[string]interface{}{},
+			},
+			expectedImpExt: map[string]interface{}{
+				"key_val": "category=movies",
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			populateFirstPartyDataImpAttributes(tt.args.data, tt.args.impExtMap)
+			assert.Equal(t, tt.expectedImpExt, tt.args.impExtMap)
+		})
+	}
+}
+
+func TestPopulateFirstPartyDataImpAttributesForMultipleAttributes(t *testing.T) {
+	impExtMap := map[string]interface{}{
+		"key_val": "k1=v1|k2=v2",
+	}
+	data := json.RawMessage(`{"sport":["rugby","cricket"],"pageType":"article","age":30,"floor":1.25}`)
+	expectedKeyValArr := []string{"age=30", "floor=1.25", "k1=v1", "k2=v2", "pageType=article", "sport=rugby,cricket"}
+
+	populateFirstPartyDataImpAttributes(data, impExtMap)
+
+	//read dctr value and split on "|" for comparison
+	actualKeyValArr := strings.Split(impExtMap[dctrKeyName].(string), "|")
+	sort.Strings(actualKeyValArr)
+	assert.Equal(t, expectedKeyValArr, actualKeyValArr)
+}
+
+func TestGetStringArray(t *testing.T) {
+	tests := []struct {
+		name   string
+		input  []interface{}
+		output []string
+	}{
+		{
+			name:   "Valid String Array",
+			input:  append(make([]interface{}, 0), "hello", "world"),
+			output: []string{"hello", "world"},
+		},
+		{
+			name:   "Invalid String Array",
+			input:  append(make([]interface{}, 0), 1, 2),
+			output: nil,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := getStringArray(tt.input)
+			assert.Equal(t, tt.output, got)
+		})
+	}
+}
+
+func TestGetMapFromJSON(t *testing.T) {
+	tests := []struct {
+		name   string
+		input  json.RawMessage
+		output map[string]interface{}
+	}{
+		{
+			name:  "Valid JSON",
+			input: json.RawMessage(`{"buyid":"testBuyId"}`),
+			output: map[string]interface{}{
+				"buyid": "testBuyId",
+			},
+		},
+		{
+			name:   "Invalid JSON",
+			input:  json.RawMessage(`{"buyid":}`),
+			output: nil,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := getMapFromJSON(tt.input)
+			assert.Equal(t, tt.output, got)
+		})
+	}
+}
diff --git a/adapters/pubmatic/pubmatictest/supplemental/impExtData.json b/adapters/pubmatic/pubmatictest/supplemental/impExtData.json
new file mode 100644
index 00000000..de346fc6
--- /dev/null
+++ b/adapters/pubmatic/pubmatictest/supplemental/impExtData.json
@@ -0,0 +1,156 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "adSlot": "AdTag_Div1@300x250",
+                        "publisherId": "999",
+                        "wrapper": {
+                            "version": 1,
+                            "profile": 5123
+                        },
+                        "dctr": "k1=v1|k2=v2"
+                    },
+                    "data": {
+                        "adserver": {
+                            "name": "gam",
+                            "adslot": "/1111/home"
+                        },
+                        "pbadslot": "/2222/home",
+                        "sport": [
+                            "rugby",
+                            "cricket"
+                        ]
+                    }
+                }
+            }
+        ],
+        "device": {
+            "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
+        },
+        "site": {
+            "id": "siteID",
+            "publisher": {
+                "id": "1234"
+            }
+        }
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "https://hbopenbid.pubmatic.com/translator?source=prebid-server",
+                "body": {
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "tagid": "AdTag_Div1",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 300,
+                                        "h": 250
+                                    }
+                                ],
+                                "h": 250,
+                                "w": 300
+                            },
+                            "ext": {
+                                "dfp_ad_unit_code": "/1111/home",
+                                "key_val": "k1=v1|k2=v2|sport=rugby,cricket"
+                            }
+                        }
+                    ],
+                    "device": {
+                        "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
+                    },
+                    "site": {
+                        "id": "siteID",
+                        "publisher": {
+                            "id": "999"
+                        }
+                    },
+                    "ext": {
+                        "prebid": {},
+                        "wrapper": {
+                            "profile": 5123,
+                            "version": 1
+                        }
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "seat": "958",
+                            "bid": [
+                                {
+                                    "id": "7706636740145184841",
+                                    "impid": "test-imp-id",
+                                    "price": 0.500000,
+                                    "adid": "29681110",
+                                    "adm": "some-test-ad",
+                                    "adomain": [
+                                        "pubmatic.com"
+                                    ],
+                                    "crid": "29681110",
+                                    "h": 250,
+                                    "w": 300,
+                                    "dealid": "test deal",
+                                    "ext": {
+                                        "dspid": 6,
+                                        "deal_channel": 1
+                                    }
+                                }
+                            ]
+                        }
+                    ],
+                    "bidid": "5778926625248726496",
+                    "cur": "USD"
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "currency": "USD",
+            "bids": [
+                {
+                    "bid": {
+                        "id": "7706636740145184841",
+                        "impid": "test-imp-id",
+                        "price": 0.5,
+                        "adid": "29681110",
+                        "adm": "some-test-ad",
+                        "adomain": [
+                            "pubmatic.com"
+                        ],
+                        "crid": "29681110",
+                        "w": 300,
+                        "h": 250,
+                        "dealid": "test deal",
+                        "ext": {
+                            "dspid": 6,
+                            "deal_channel": 1
+                        }
+                    },
+                    "type": "banner"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/tappx/tappx_test.go b/adapters/tappx/tappx_test.go
index 16587d03..c1b71142 100644
--- a/adapters/tappx/tappx_test.go
+++ b/adapters/tappx/tappx_test.go
@@ -46,7 +46,7 @@ func TestTsValue(t *testing.T) {
 
 	url, err := bidderTappx.buildEndpointURL(&tappxExt, test)
 
-	match, err := regexp.MatchString(`http://ssp\.api\.tappx\.com/rtb/v2/DUMMYENDPOINT\?tappxkey=dummy-tappx-key&ts=[0-9]{13}&type_cnn=prebid&v=1\.4`, url)
+	match, err := regexp.MatchString(`http://ssp\.api\.tappx\.com/rtb/v2/DUMMYENDPOINT\?tappxkey=dummy-tappx-key&ts=[0-9]{13}&type_cnn=prebid&v=1\.5`, url)
 	if err != nil {
 		t.Errorf("Error while running regex validation: %s", err.Error())
 		return
diff --git a/adapters/tappx/tappxtest/exemplary/single-banner-impression-extra.json b/adapters/tappx/tappxtest/exemplary/single-banner-impression-extra.json
index 15d19f94..ef8e5e39 100644
--- a/adapters/tappx/tappxtest/exemplary/single-banner-impression-extra.json
+++ b/adapters/tappx/tappxtest/exemplary/single-banner-impression-extra.json
@@ -35,7 +35,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://ssp.api.tappx.com/rtb/v2/ZZ123456PS?tappxkey=pub-12345-android-9876&type_cnn=prebid&v=1.4",
+        "uri": "http://ssp.api.tappx.com/rtb/v2/ZZ123456PS?tappxkey=pub-12345-android-9876&type_cnn=prebid&v=1.5",
         "body": {
           "id": "0000000000001",
           "test": 1,
diff --git a/adapters/tappx/tappxtest/exemplary/single-banner-impression-future-feature.json b/adapters/tappx/tappxtest/exemplary/single-banner-impression-future-feature.json
index 8a78493e..5af06ad5 100644
--- a/adapters/tappx/tappxtest/exemplary/single-banner-impression-future-feature.json
+++ b/adapters/tappx/tappxtest/exemplary/single-banner-impression-future-feature.json
@@ -32,7 +32,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://zz123456ps.pub.tappx.com/rtb/?tappxkey=pub-12345-android-9876&type_cnn=prebid&v=1.4",
+        "uri": "http://zz123456ps.pub.tappx.com/rtb/?tappxkey=pub-12345-android-9876&type_cnn=prebid&v=1.5",
         "body": {
           "id": "0000000000001",
           "test": 1,
diff --git a/adapters/tappx/tappxtest/exemplary/single-banner-impression.json b/adapters/tappx/tappxtest/exemplary/single-banner-impression.json
index da27cd97..dbd342b3 100644
--- a/adapters/tappx/tappxtest/exemplary/single-banner-impression.json
+++ b/adapters/tappx/tappxtest/exemplary/single-banner-impression.json
@@ -32,7 +32,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://ssp.api.tappx.com/rtb/v2/ZZ123456PS?tappxkey=pub-12345-android-9876&type_cnn=prebid&v=1.4",
+        "uri": "http://ssp.api.tappx.com/rtb/v2/ZZ123456PS?tappxkey=pub-12345-android-9876&type_cnn=prebid&v=1.5",
         "body": {
           "id": "0000000000001",
           "test": 1,
diff --git a/adapters/tappx/tappxtest/exemplary/single-banner-site.json b/adapters/tappx/tappxtest/exemplary/single-banner-site.json
index 2a75af67..dde889dd 100644
--- a/adapters/tappx/tappxtest/exemplary/single-banner-site.json
+++ b/adapters/tappx/tappxtest/exemplary/single-banner-site.json
@@ -36,7 +36,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://ssp.api.tappx.com/rtb/v2/ZZ123456PS?tappxkey=pub-12345-site-9876&type_cnn=prebid&v=1.4",
+        "uri": "http://ssp.api.tappx.com/rtb/v2/ZZ123456PS?tappxkey=pub-12345-site-9876&type_cnn=prebid&v=1.5",
         "body": {
           "id": "0000000000001",
           "test": 1,
diff --git a/adapters/tappx/tappxtest/exemplary/single-video-impression.json b/adapters/tappx/tappxtest/exemplary/single-video-impression.json
index 407a0565..a9507df5 100644
--- a/adapters/tappx/tappxtest/exemplary/single-video-impression.json
+++ b/adapters/tappx/tappxtest/exemplary/single-video-impression.json
@@ -34,7 +34,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://ssp.api.tappx.com/rtb/v2/VZ123456PS?tappxkey=pub-12345-android-9876&type_cnn=prebid&v=1.4",
+        "uri": "http://ssp.api.tappx.com/rtb/v2/VZ123456PS?tappxkey=pub-12345-android-9876&type_cnn=prebid&v=1.5",
         "body": {
           "id": "0000000000001",
           "test": 1,
diff --git a/adapters/tappx/tappxtest/exemplary/single-video-site.json b/adapters/tappx/tappxtest/exemplary/single-video-site.json
index 4dec5931..5e2ba58b 100644
--- a/adapters/tappx/tappxtest/exemplary/single-video-site.json
+++ b/adapters/tappx/tappxtest/exemplary/single-video-site.json
@@ -38,7 +38,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://ssp.api.tappx.com/rtb/v2/VZ123456PS?tappxkey=pub-12345-site-9876&type_cnn=prebid&v=1.4",
+        "uri": "http://ssp.api.tappx.com/rtb/v2/VZ123456PS?tappxkey=pub-12345-site-9876&type_cnn=prebid&v=1.5",
         "body": {
           "id": "0000000000001",
           "test": 1,
diff --git a/adapters/tappx/tappxtest/supplemental/204status.json b/adapters/tappx/tappxtest/supplemental/204status.json
index a0141616..8e3bb4b5 100644
--- a/adapters/tappx/tappxtest/supplemental/204status.json
+++ b/adapters/tappx/tappxtest/supplemental/204status.json
@@ -29,7 +29,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://ssp.api.tappx.com/rtb/v2/ZZ123456PS?tappxkey=pub-12345-android-9876&type_cnn=prebid&v=1.4",
+        "uri": "http://ssp.api.tappx.com/rtb/v2/ZZ123456PS?tappxkey=pub-12345-android-9876&type_cnn=prebid&v=1.5",
         "body": {
           "id": "0000000000001",
           "test": 1,
diff --git a/adapters/tappx/tappxtest/supplemental/bidfloor.json b/adapters/tappx/tappxtest/supplemental/bidfloor.json
index cc451d65..7c2c5306 100644
--- a/adapters/tappx/tappxtest/supplemental/bidfloor.json
+++ b/adapters/tappx/tappxtest/supplemental/bidfloor.json
@@ -33,7 +33,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://ssp.api.tappx.com/rtb/v2/ZZ123456PS?tappxkey=pub-12345-android-9876&type_cnn=prebid&v=1.4",
+        "uri": "http://ssp.api.tappx.com/rtb/v2/ZZ123456PS?tappxkey=pub-12345-android-9876&type_cnn=prebid&v=1.5",
         "body": {
           "id": "0000000000001",
           "test": 1,
diff --git a/adapters/tappx/tappxtest/supplemental/http-err-status.json b/adapters/tappx/tappxtest/supplemental/http-err-status.json
index 1798868b..22d0afe3 100644
--- a/adapters/tappx/tappxtest/supplemental/http-err-status.json
+++ b/adapters/tappx/tappxtest/supplemental/http-err-status.json
@@ -29,7 +29,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://ssp.api.tappx.com/rtb/v2/ZZ123456PS?tappxkey=pub-12345-android-9876&type_cnn=prebid&v=1.4",
+        "uri": "http://ssp.api.tappx.com/rtb/v2/ZZ123456PS?tappxkey=pub-12345-android-9876&type_cnn=prebid&v=1.5",
         "body": {
           "id": "0000000000001",
           "test": 1,
diff --git a/adapters/tappx/tappxtest/supplemental/http-err-status2.json b/adapters/tappx/tappxtest/supplemental/http-err-status2.json
index 2d64cfe0..011f80a5 100644
--- a/adapters/tappx/tappxtest/supplemental/http-err-status2.json
+++ b/adapters/tappx/tappxtest/supplemental/http-err-status2.json
@@ -29,7 +29,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://ssp.api.tappx.com/rtb/v2/ZZ123456PS?tappxkey=pub-12345-android-9876&type_cnn=prebid&v=1.4",
+        "uri": "http://ssp.api.tappx.com/rtb/v2/ZZ123456PS?tappxkey=pub-12345-android-9876&type_cnn=prebid&v=1.5",
         "body": {
           "id": "0000000000001",
           "test": 1,
diff --git a/config/accounts_test.go b/config/accounts_test.go
index dbe10698..4a147368 100644
--- a/config/accounts_test.go
+++ b/config/accounts_test.go
@@ -365,6 +365,74 @@ func TestPurposeEnforced(t *testing.T) {
 	}
 }
 
+func TestPurposeEnforcementAlgo(t *testing.T) {
+	tests := []struct {
+		description          string
+		givePurposeConfigNil bool
+		givePurpose1Algo     TCF2EnforcementAlgo
+		givePurpose2Algo     TCF2EnforcementAlgo
+		givePurpose          consentconstants.Purpose
+		wantAlgo             TCF2EnforcementAlgo
+		wantAlgoSet          bool
+	}{
+		{
+			description:          "Purpose config is nil",
+			givePurposeConfigNil: true,
+			givePurpose:          1,
+			wantAlgo:             TCF2UndefinedEnforcement,
+			wantAlgoSet:          false,
+		},
+		{
+			description:      "Purpose 1 enforcement algo is undefined",
+			givePurpose1Algo: TCF2UndefinedEnforcement,
+			givePurpose:      1,
+			wantAlgo:         TCF2UndefinedEnforcement,
+			wantAlgoSet:      false,
+		},
+		{
+			description:      "Purpose 1 enforcement algo set to basic",
+			givePurpose1Algo: TCF2BasicEnforcement,
+			givePurpose:      1,
+			wantAlgo:         TCF2BasicEnforcement,
+			wantAlgoSet:      true,
+		},
+		{
+			description:      "Purpose 1 enforcement algo set to full",
+			givePurpose1Algo: TCF2FullEnforcement,
+			givePurpose:      1,
+			wantAlgo:         TCF2FullEnforcement,
+			wantAlgoSet:      true,
+		},
+		{
+			description:      "Purpose 2 Enforcement algo set to basic",
+			givePurpose2Algo: TCF2BasicEnforcement,
+			givePurpose:      2,
+			wantAlgo:         TCF2BasicEnforcement,
+			wantAlgoSet:      true,
+		},
+	}
+
+	for _, tt := range tests {
+		accountGDPR := AccountGDPR{}
+
+		if !tt.givePurposeConfigNil {
+			accountGDPR.PurposeConfigs = map[consentconstants.Purpose]*AccountGDPRPurpose{
+				1: {
+					EnforceAlgoID: tt.givePurpose1Algo,
+				},
+				2: {
+					EnforceAlgoID: tt.givePurpose2Algo,
+				},
+			}
+		}
+
+		value, present := accountGDPR.PurposeEnforcementAlgo(tt.givePurpose)
+
+		assert.Equal(t, tt.wantAlgo, value, tt.description)
+		assert.Equal(t, tt.wantAlgoSet, present, tt.description)
+	}
+}
+
 func TestPurposeEnforcingVendors(t *testing.T) {
 	tests := []struct {
 		description           string
@@ -433,73 +501,52 @@ func TestPurposeEnforcingVendors(t *testing.T) {
 	}
 }
 
-func TestPurposeVendorException(t *testing.T) {
+func TestPurposeVendorExceptions(t *testing.T) {
 	tests := []struct {
 		description              string
 		givePurposeConfigNil     bool
 		givePurpose1ExceptionMap map[openrtb_ext.BidderName]struct{}
 		givePurpose2ExceptionMap map[openrtb_ext.BidderName]struct{}
 		givePurpose              consentconstants.Purpose
-		giveBidder               openrtb_ext.BidderName
-		wantIsVendorException    bool
-		wantVendorExceptionSet   bool
+		wantExceptionMap         map[openrtb_ext.BidderName]struct{}
+		wantExceptionMapSet      bool
 	}{
 		{
-			description:            "Purpose config is nil",
-			givePurposeConfigNil:   true,
-			givePurpose:            1,
-			giveBidder:             "appnexus",
-			wantIsVendorException:  false,
-			wantVendorExceptionSet: false,
+			description:          "Purpose config is nil",
+			givePurposeConfigNil: true,
+			givePurpose:          1,
+			// wantExceptionMap:     map[openrtb_ext.BidderName]struct{}{},
+			wantExceptionMap:    nil,
+			wantExceptionMapSet: false,
 		},
 		{
-			description:            "Nil - exception map not defined for purpose",
-			givePurpose:            1,
-			giveBidder:             "appnexus",
-			wantIsVendorException:  false,
-			wantVendorExceptionSet: false,
+			description: "Nil - exception map not defined for purpose",
+			givePurpose: 1,
+			// wantExceptionMap:    map[openrtb_ext.BidderName]struct{}{},
+			wantExceptionMap:    nil,
+			wantExceptionMapSet: false,
 		},
 		{
 			description:              "Empty - exception map empty for purpose",
 			givePurpose:              1,
 			givePurpose1ExceptionMap: map[openrtb_ext.BidderName]struct{}{},
-			giveBidder:               "appnexus",
-			wantIsVendorException:    false,
-			wantVendorExceptionSet:   true,
+			wantExceptionMap:         map[openrtb_ext.BidderName]struct{}{},
+			wantExceptionMapSet:      true,
 		},
 		{
-			description:              "One - bidder found in purpose exception map containing one entry",
-			givePurpose:              1,
-			givePurpose1ExceptionMap: map[openrtb_ext.BidderName]struct{}{"appnexus": {}},
-			giveBidder:               "appnexus",
-			wantIsVendorException:    true,
-			wantVendorExceptionSet:   true,
-		},
-		{
-			description:              "Many - bidder found in purpose exception map containing multiple entries",
+			description:              "Nonempty - exception map with multiple entries for purpose",
 			givePurpose:              1,
 			givePurpose1ExceptionMap: map[openrtb_ext.BidderName]struct{}{"rubicon": {}, "appnexus": {}, "index": {}},
-			giveBidder:               "appnexus",
-			wantIsVendorException:    true,
-			wantVendorExceptionSet:   true,
+			wantExceptionMap:         map[openrtb_ext.BidderName]struct{}{"rubicon": {}, "appnexus": {}, "index": {}},
+			wantExceptionMapSet:      true,
 		},
 		{
-			description:              "Many - bidder not found in purpose exception map containing multiple entries",
-			givePurpose:              1,
-			givePurpose1ExceptionMap: map[openrtb_ext.BidderName]struct{}{"rubicon": {}, "appnexus": {}, "index": {}},
-			givePurpose2ExceptionMap: map[openrtb_ext.BidderName]struct{}{"rubicon": {}, "appnexus": {}, "openx": {}},
-			giveBidder:               "openx",
-			wantIsVendorException:    false,
-			wantVendorExceptionSet:   true,
-		},
-		{
-			description:              "Many - bidder found in different purpose exception map containing multiple entries",
+			description:              "Nonempty - exception map with multiple entries for different purpose",
 			givePurpose:              2,
 			givePurpose1ExceptionMap: map[openrtb_ext.BidderName]struct{}{"rubicon": {}, "appnexus": {}, "index": {}},
 			givePurpose2ExceptionMap: map[openrtb_ext.BidderName]struct{}{"rubicon": {}, "appnexus": {}, "openx": {}},
-			giveBidder:               "openx",
-			wantIsVendorException:    true,
-			wantVendorExceptionSet:   true,
+			wantExceptionMap:         map[openrtb_ext.BidderName]struct{}{"rubicon": {}, "appnexus": {}, "openx": {}},
+			wantExceptionMapSet:      true,
 		},
 	}
 
@@ -517,10 +564,10 @@ func TestPurposeVendorException(t *testing.T) {
 			}
 		}
 
-		value, present := accountGDPR.PurposeVendorException(tt.givePurpose, tt.giveBidder)
+		value, present := accountGDPR.PurposeVendorExceptions(tt.givePurpose)
 
-		assert.Equal(t, tt.wantIsVendorException, value, tt.description)
-		assert.Equal(t, tt.wantVendorExceptionSet, present, tt.description)
+		assert.Equal(t, tt.wantExceptionMap, value, tt.description)
+		assert.Equal(t, tt.wantExceptionMapSet, present, tt.description)
 	}
 }
 
@@ -704,59 +751,3 @@ func TestPurposeOneTreatmentAccessAllowed(t *testing.T) {
 		assert.Equal(t, tt.wantAllowedSet, present, tt.description)
 	}
 }
-
-func TestBasicEnforcementVendor(t *testing.T) {
-	tests := []struct {
-		description        string
-		giveBasicVendorMap map[string]struct{}
-		giveBidder         openrtb_ext.BidderName
-		wantBasicVendorSet bool
-		wantIsBasicVendor  bool
-	}{
-		{
-			description:        "Nil - basic vendor map not defined",
-			giveBidder:         "appnexus",
-			wantBasicVendorSet: false,
-			wantIsBasicVendor:  false,
-		},
-		{
-			description:        "Empty - basic vendor map empty",
-			giveBasicVendorMap: map[string]struct{}{},
-			giveBidder:         "appnexus",
-			wantBasicVendorSet: true,
-			wantIsBasicVendor:  false,
-		},
-		{
-			description:        "One - bidder found in basic vendor map containing one entry",
-			giveBasicVendorMap: map[string]struct{}{"appnexus": {}},
-			giveBidder:         "appnexus",
-			wantBasicVendorSet: true,
-			wantIsBasicVendor:  true,
-		},
-		{
-			description:        "Many - bidder found in basic vendor map containing multiple entries",
-			giveBasicVendorMap: map[string]struct{}{"rubicon": {}, "appnexus": {}, "index": {}},
-			giveBidder:         "appnexus",
-			wantBasicVendorSet: true,
-			wantIsBasicVendor:  true,
-		},
-		{
-			description:        "Many - bidder not found in basic vendor map containing multiple entries",
-			giveBasicVendorMap: map[string]struct{}{"rubicon": {}, "appnexus": {}, "index": {}},
-			giveBidder:         "openx",
-			wantBasicVendorSet: true,
-			wantIsBasicVendor:  false,
-		},
-	}
-
-	for _, tt := range tests {
-		accountGDPR := AccountGDPR{
-			BasicEnforcementVendorsMap: tt.giveBasicVendorMap,
-		}
-
-		value, present := accountGDPR.BasicEnforcementVendor(tt.giveBidder)
-
-		assert.Equal(t, tt.wantIsBasicVendor, value, tt.description)
-		assert.Equal(t, tt.wantBasicVendorSet, present, tt.description)
-	}
-}
diff --git a/config/config_test.go b/config/config_test.go
index ace2804c..bf5716b9 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -178,7 +178,8 @@ func TestDefaults(t *testing.T) {
 		Enabled: true,
 		Purpose1: TCF2Purpose{
 			Enabled:            true,
-			EnforceAlgo:        TCF2FullEnforcement,
+			EnforceAlgo:        TCF2EnforceAlgoFull,
+			EnforceAlgoID:      TCF2FullEnforcement,
 			EnforcePurpose:     true,
 			EnforceVendors:     true,
 			VendorExceptions:   []openrtb_ext.BidderName{},
@@ -186,7 +187,8 @@ func TestDefaults(t *testing.T) {
 		},
 		Purpose2: TCF2Purpose{
 			Enabled:            true,
-			EnforceAlgo:        TCF2FullEnforcement,
+			EnforceAlgo:        TCF2EnforceAlgoFull,
+			EnforceAlgoID:      TCF2FullEnforcement,
 			EnforcePurpose:     true,
 			EnforceVendors:     true,
 			VendorExceptions:   []openrtb_ext.BidderName{},
@@ -194,7 +196,8 @@ func TestDefaults(t *testing.T) {
 		},
 		Purpose3: TCF2Purpose{
 			Enabled:            true,
-			EnforceAlgo:        TCF2FullEnforcement,
+			EnforceAlgo:        TCF2EnforceAlgoFull,
+			EnforceAlgoID:      TCF2FullEnforcement,
 			EnforcePurpose:     true,
 			EnforceVendors:     true,
 			VendorExceptions:   []openrtb_ext.BidderName{},
@@ -202,7 +205,8 @@ func TestDefaults(t *testing.T) {
 		},
 		Purpose4: TCF2Purpose{
 			Enabled:            true,
-			EnforceAlgo:        TCF2FullEnforcement,
+			EnforceAlgo:        TCF2EnforceAlgoFull,
+			EnforceAlgoID:      TCF2FullEnforcement,
 			EnforcePurpose:     true,
 			EnforceVendors:     true,
 			VendorExceptions:   []openrtb_ext.BidderName{},
@@ -210,7 +214,8 @@ func TestDefaults(t *testing.T) {
 		},
 		Purpose5: TCF2Purpose{
 			Enabled:            true,
-			EnforceAlgo:        TCF2FullEnforcement,
+			EnforceAlgo:        TCF2EnforceAlgoFull,
+			EnforceAlgoID:      TCF2FullEnforcement,
 			EnforcePurpose:     true,
 			EnforceVendors:     true,
 			VendorExceptions:   []openrtb_ext.BidderName{},
@@ -218,7 +223,8 @@ func TestDefaults(t *testing.T) {
 		},
 		Purpose6: TCF2Purpose{
 			Enabled:            true,
-			EnforceAlgo:        TCF2FullEnforcement,
+			EnforceAlgo:        TCF2EnforceAlgoFull,
+			EnforceAlgoID:      TCF2FullEnforcement,
 			EnforcePurpose:     true,
 			EnforceVendors:     true,
 			VendorExceptions:   []openrtb_ext.BidderName{},
@@ -226,7 +232,8 @@ func TestDefaults(t *testing.T) {
 		},
 		Purpose7: TCF2Purpose{
 			Enabled:            true,
-			EnforceAlgo:        TCF2FullEnforcement,
+			EnforceAlgo:        TCF2EnforceAlgoFull,
+			EnforceAlgoID:      TCF2FullEnforcement,
 			EnforcePurpose:     true,
 			EnforceVendors:     true,
 			VendorExceptions:   []openrtb_ext.BidderName{},
@@ -234,7 +241,8 @@ func TestDefaults(t *testing.T) {
 		},
 		Purpose8: TCF2Purpose{
 			Enabled:            true,
-			EnforceAlgo:        TCF2FullEnforcement,
+			EnforceAlgo:        TCF2EnforceAlgoFull,
+			EnforceAlgoID:      TCF2FullEnforcement,
 			EnforcePurpose:     true,
 			EnforceVendors:     true,
 			VendorExceptions:   []openrtb_ext.BidderName{},
@@ -242,7 +250,8 @@ func TestDefaults(t *testing.T) {
 		},
 		Purpose9: TCF2Purpose{
 			Enabled:            true,
-			EnforceAlgo:        TCF2FullEnforcement,
+			EnforceAlgo:        TCF2EnforceAlgoFull,
+			EnforceAlgoID:      TCF2FullEnforcement,
 			EnforcePurpose:     true,
 			EnforceVendors:     true,
 			VendorExceptions:   []openrtb_ext.BidderName{},
@@ -250,7 +259,8 @@ func TestDefaults(t *testing.T) {
 		},
 		Purpose10: TCF2Purpose{
 			Enabled:            true,
-			EnforceAlgo:        TCF2FullEnforcement,
+			EnforceAlgo:        TCF2EnforceAlgoFull,
+			EnforceAlgoID:      TCF2FullEnforcement,
 			EnforcePurpose:     true,
 			EnforceVendors:     true,
 			VendorExceptions:   []openrtb_ext.BidderName{},
@@ -298,6 +308,7 @@ gdpr:
       enforce_vendors: false
       vendor_exceptions: ["foo2"]
     purpose3:
+      enforce_algo: "basic"
       enforce_vendors: false
       vendor_exceptions: ["foo3"]
     purpose4:
@@ -505,7 +516,8 @@ func TestFullConfig(t *testing.T) {
 		Enabled: true,
 		Purpose1: TCF2Purpose{
 			Enabled:            true, // true by default
-			EnforceAlgo:        TCF2FullEnforcement,
+			EnforceAlgo:        TCF2EnforceAlgoFull,
+			EnforceAlgoID:      TCF2FullEnforcement,
 			EnforcePurpose:     true,
 			EnforceVendors:     false,
 			VendorExceptions:   []openrtb_ext.BidderName{openrtb_ext.BidderName("foo1a"), openrtb_ext.BidderName("foo1b")},
@@ -513,7 +525,8 @@ func TestFullConfig(t *testing.T) {
 		},
 		Purpose2: TCF2Purpose{
 			Enabled:            false,
-			EnforceAlgo:        TCF2FullEnforcement,
+			EnforceAlgo:        TCF2EnforceAlgoFull,
+			EnforceAlgoID:      TCF2FullEnforcement,
 			EnforcePurpose:     false,
 			EnforceVendors:     false,
 			VendorExceptions:   []openrtb_ext.BidderName{openrtb_ext.BidderName("foo2")},
@@ -521,7 +534,8 @@ func TestFullConfig(t *testing.T) {
 		},
 		Purpose3: TCF2Purpose{
 			Enabled:            true, // true by default
-			EnforceAlgo:        TCF2FullEnforcement,
+			EnforceAlgo:        TCF2EnforceAlgoBasic,
+			EnforceAlgoID:      TCF2BasicEnforcement,
 			EnforcePurpose:     true,
 			EnforceVendors:     false,
 			VendorExceptions:   []openrtb_ext.BidderName{openrtb_ext.BidderName("foo3")},
@@ -529,7 +543,8 @@ func TestFullConfig(t *testing.T) {
 		},
 		Purpose4: TCF2Purpose{
 			Enabled:            true, // true by default
-			EnforceAlgo:        TCF2FullEnforcement,
+			EnforceAlgo:        TCF2EnforceAlgoFull,
+			EnforceAlgoID:      TCF2FullEnforcement,
 			EnforcePurpose:     true,
 			EnforceVendors:     false,
 			VendorExceptions:   []openrtb_ext.BidderName{openrtb_ext.BidderName("foo4")},
@@ -537,7 +552,8 @@ func TestFullConfig(t *testing.T) {
 		},
 		Purpose5: TCF2Purpose{
 			Enabled:            true, // true by default
-			EnforceAlgo:        TCF2FullEnforcement,
+			EnforceAlgo:        TCF2EnforceAlgoFull,
+			EnforceAlgoID:      TCF2FullEnforcement,
 			EnforcePurpose:     true,
 			EnforceVendors:     false,
 			VendorExceptions:   []openrtb_ext.BidderName{openrtb_ext.BidderName("foo5")},
@@ -545,7 +561,8 @@ func TestFullConfig(t *testing.T) {
 		},
 		Purpose6: TCF2Purpose{
 			Enabled:            true, // true by default
-			EnforceAlgo:        TCF2FullEnforcement,
+			EnforceAlgo:        TCF2EnforceAlgoFull,
+			EnforceAlgoID:      TCF2FullEnforcement,
 			EnforcePurpose:     true,
 			EnforceVendors:     false,
 			VendorExceptions:   []openrtb_ext.BidderName{openrtb_ext.BidderName("foo6")},
@@ -553,7 +570,8 @@ func TestFullConfig(t *testing.T) {
 		},
 		Purpose7: TCF2Purpose{
 			Enabled:            true, // true by default
-			EnforceAlgo:        TCF2FullEnforcement,
+			EnforceAlgo:        TCF2EnforceAlgoFull,
+			EnforceAlgoID:      TCF2FullEnforcement,
 			EnforcePurpose:     true,
 			EnforceVendors:     false,
 			VendorExceptions:   []openrtb_ext.BidderName{openrtb_ext.BidderName("foo7")},
@@ -561,7 +579,8 @@ func TestFullConfig(t *testing.T) {
 		},
 		Purpose8: TCF2Purpose{
 			Enabled:            true, // true by default
-			EnforceAlgo:        TCF2FullEnforcement,
+			EnforceAlgo:        TCF2EnforceAlgoFull,
+			EnforceAlgoID:      TCF2FullEnforcement,
 			EnforcePurpose:     true,
 			EnforceVendors:     false,
 			VendorExceptions:   []openrtb_ext.BidderName{openrtb_ext.BidderName("foo8")},
@@ -569,7 +588,8 @@ func TestFullConfig(t *testing.T) {
 		},
 		Purpose9: TCF2Purpose{
 			Enabled:            true, // true by default
-			EnforceAlgo:        TCF2FullEnforcement,
+			EnforceAlgo:        TCF2EnforceAlgoFull,
+			EnforceAlgoID:      TCF2FullEnforcement,
 			EnforcePurpose:     true,
 			EnforceVendors:     false,
 			VendorExceptions:   []openrtb_ext.BidderName{openrtb_ext.BidderName("foo9")},
@@ -577,7 +597,8 @@ func TestFullConfig(t *testing.T) {
 		},
 		Purpose10: TCF2Purpose{
 			Enabled:            true, // true by default
-			EnforceAlgo:        TCF2FullEnforcement,
+			EnforceAlgo:        TCF2EnforceAlgoFull,
+			EnforceAlgoID:      TCF2FullEnforcement,
 			EnforcePurpose:     true,
 			EnforceVendors:     false,
 			VendorExceptions:   []openrtb_ext.BidderName{openrtb_ext.BidderName("foo10")},
@@ -646,16 +667,16 @@ func TestValidateConfig(t *testing.T) {
 		GDPR: GDPR{
 			DefaultValue: "1",
 			TCF2: TCF2{
-				Purpose1:  TCF2Purpose{EnforceAlgo: TCF2FullEnforcement},
-				Purpose2:  TCF2Purpose{EnforceAlgo: TCF2FullEnforcement},
-				Purpose3:  TCF2Purpose{EnforceAlgo: TCF2FullEnforcement},
-				Purpose4:  TCF2Purpose{EnforceAlgo: TCF2FullEnforcement},
-				Purpose5:  TCF2Purpose{EnforceAlgo: TCF2FullEnforcement},
-				Purpose6:  TCF2Purpose{EnforceAlgo: TCF2FullEnforcement},
-				Purpose7:  TCF2Purpose{EnforceAlgo: TCF2FullEnforcement},
-				Purpose8:  TCF2Purpose{EnforceAlgo: TCF2FullEnforcement},
-				Purpose9:  TCF2Purpose{EnforceAlgo: TCF2FullEnforcement},
-				Purpose10: TCF2Purpose{EnforceAlgo: TCF2FullEnforcement},
+				Purpose1:  TCF2Purpose{EnforceAlgo: TCF2EnforceAlgoBasic},
+				Purpose2:  TCF2Purpose{EnforceAlgo: TCF2EnforceAlgoFull},
+				Purpose3:  TCF2Purpose{EnforceAlgo: TCF2EnforceAlgoBasic},
+				Purpose4:  TCF2Purpose{EnforceAlgo: TCF2EnforceAlgoFull},
+				Purpose5:  TCF2Purpose{EnforceAlgo: TCF2EnforceAlgoBasic},
+				Purpose6:  TCF2Purpose{EnforceAlgo: TCF2EnforceAlgoFull},
+				Purpose7:  TCF2Purpose{EnforceAlgo: TCF2EnforceAlgoBasic},
+				Purpose8:  TCF2Purpose{EnforceAlgo: TCF2EnforceAlgoFull},
+				Purpose9:  TCF2Purpose{EnforceAlgo: TCF2EnforceAlgoBasic},
+				Purpose10: TCF2Purpose{EnforceAlgo: TCF2EnforceAlgoFull},
 			},
 		},
 		StoredRequests: StoredRequests{
@@ -1368,16 +1389,16 @@ func TestMigrateConfigTCF2EnforcePurposeFlags(t *testing.T) {
                     enforce_purpose: "no"
             `),
 			wantEnforceAlgosSet:         true,
-			wantPurpose1EnforceAlgo:     TCF2FullEnforcement,
-			wantPurpose2EnforceAlgo:     TCF2FullEnforcement,
-			wantPurpose3EnforceAlgo:     TCF2FullEnforcement,
-			wantPurpose4EnforceAlgo:     TCF2FullEnforcement,
-			wantPurpose5EnforceAlgo:     TCF2FullEnforcement,
-			wantPurpose6EnforceAlgo:     TCF2FullEnforcement,
-			wantPurpose7EnforceAlgo:     TCF2FullEnforcement,
-			wantPurpose8EnforceAlgo:     TCF2FullEnforcement,
-			wantPurpose9EnforceAlgo:     TCF2FullEnforcement,
-			wantPurpose10EnforceAlgo:    TCF2FullEnforcement,
+			wantPurpose1EnforceAlgo:     TCF2EnforceAlgoFull,
+			wantPurpose2EnforceAlgo:     TCF2EnforceAlgoFull,
+			wantPurpose3EnforceAlgo:     TCF2EnforceAlgoFull,
+			wantPurpose4EnforceAlgo:     TCF2EnforceAlgoFull,
+			wantPurpose5EnforceAlgo:     TCF2EnforceAlgoFull,
+			wantPurpose6EnforceAlgo:     TCF2EnforceAlgoFull,
+			wantPurpose7EnforceAlgo:     TCF2EnforceAlgoFull,
+			wantPurpose8EnforceAlgo:     TCF2EnforceAlgoFull,
+			wantPurpose9EnforceAlgo:     TCF2EnforceAlgoFull,
+			wantPurpose10EnforceAlgo:    TCF2EnforceAlgoFull,
 			wantEnforcePurposesSet:      true,
 			wantPurpose1EnforcePurpose:  trueStr,
 			wantPurpose2EnforcePurpose:  falseStr,
@@ -1456,16 +1477,16 @@ func TestMigrateConfigTCF2EnforcePurposeFlags(t *testing.T) {
                     enforce_algo: "full"
             `),
 			wantEnforceAlgosSet:      true,
-			wantPurpose1EnforceAlgo:  TCF2FullEnforcement,
-			wantPurpose2EnforceAlgo:  TCF2FullEnforcement,
-			wantPurpose3EnforceAlgo:  TCF2FullEnforcement,
-			wantPurpose4EnforceAlgo:  TCF2FullEnforcement,
-			wantPurpose5EnforceAlgo:  TCF2FullEnforcement,
-			wantPurpose6EnforceAlgo:  TCF2FullEnforcement,
-			wantPurpose7EnforceAlgo:  TCF2FullEnforcement,
-			wantPurpose8EnforceAlgo:  TCF2FullEnforcement,
-			wantPurpose9EnforceAlgo:  TCF2FullEnforcement,
-			wantPurpose10EnforceAlgo: TCF2FullEnforcement,
+			wantPurpose1EnforceAlgo:  TCF2EnforceAlgoFull,
+			wantPurpose2EnforceAlgo:  TCF2EnforceAlgoFull,
+			wantPurpose3EnforceAlgo:  TCF2EnforceAlgoFull,
+			wantPurpose4EnforceAlgo:  TCF2EnforceAlgoFull,
+			wantPurpose5EnforceAlgo:  TCF2EnforceAlgoFull,
+			wantPurpose6EnforceAlgo:  TCF2EnforceAlgoFull,
+			wantPurpose7EnforceAlgo:  TCF2EnforceAlgoFull,
+			wantPurpose8EnforceAlgo:  TCF2EnforceAlgoFull,
+			wantPurpose9EnforceAlgo:  TCF2EnforceAlgoFull,
+			wantPurpose10EnforceAlgo: TCF2EnforceAlgoFull,
 			wantEnforcePurposesSet:   false,
 		},
 		{
@@ -1505,16 +1526,16 @@ func TestMigrateConfigTCF2EnforcePurposeFlags(t *testing.T) {
                     enforce_purpose: "no"
             `),
 			wantEnforceAlgosSet:         true,
-			wantPurpose1EnforceAlgo:     TCF2FullEnforcement,
-			wantPurpose2EnforceAlgo:     TCF2FullEnforcement,
-			wantPurpose3EnforceAlgo:     TCF2FullEnforcement,
-			wantPurpose4EnforceAlgo:     TCF2FullEnforcement,
-			wantPurpose5EnforceAlgo:     TCF2FullEnforcement,
-			wantPurpose6EnforceAlgo:     TCF2FullEnforcement,
-			wantPurpose7EnforceAlgo:     TCF2FullEnforcement,
-			wantPurpose8EnforceAlgo:     TCF2FullEnforcement,
-			wantPurpose9EnforceAlgo:     TCF2FullEnforcement,
-			wantPurpose10EnforceAlgo:    TCF2FullEnforcement,
+			wantPurpose1EnforceAlgo:     TCF2EnforceAlgoFull,
+			wantPurpose2EnforceAlgo:     TCF2EnforceAlgoFull,
+			wantPurpose3EnforceAlgo:     TCF2EnforceAlgoFull,
+			wantPurpose4EnforceAlgo:     TCF2EnforceAlgoFull,
+			wantPurpose5EnforceAlgo:     TCF2EnforceAlgoFull,
+			wantPurpose6EnforceAlgo:     TCF2EnforceAlgoFull,
+			wantPurpose7EnforceAlgo:     TCF2EnforceAlgoFull,
+			wantPurpose8EnforceAlgo:     TCF2EnforceAlgoFull,
+			wantPurpose9EnforceAlgo:     TCF2EnforceAlgoFull,
+			wantPurpose10EnforceAlgo:    TCF2EnforceAlgoFull,
 			wantEnforcePurposesSet:      true,
 			wantPurpose1EnforcePurpose:  falseStr,
 			wantPurpose2EnforcePurpose:  falseStr,
@@ -1646,26 +1667,26 @@ func TestMissingGDPRDefaultValue(t *testing.T) {
 	assertOneError(t, cfg.validate(v), "gdpr.default_value is required and must be specified")
 }
 
-func TestInvalidEnforcePurpose(t *testing.T) {
+func TestInvalidEnforceAlgo(t *testing.T) {
 	cfg, v := newDefaultConfig(t)
 	cfg.GDPR.TCF2.Purpose1.EnforceAlgo = ""
-	cfg.GDPR.TCF2.Purpose2.EnforceAlgo = TCF2FullEnforcement
-	cfg.GDPR.TCF2.Purpose3.EnforceAlgo = TCF2FullEnforcement
-	cfg.GDPR.TCF2.Purpose4.EnforceAlgo = TCF2FullEnforcement
+	cfg.GDPR.TCF2.Purpose2.EnforceAlgo = TCF2EnforceAlgoFull
+	cfg.GDPR.TCF2.Purpose3.EnforceAlgo = TCF2EnforceAlgoBasic
+	cfg.GDPR.TCF2.Purpose4.EnforceAlgo = TCF2EnforceAlgoFull
 	cfg.GDPR.TCF2.Purpose5.EnforceAlgo = "invalid1"
 	cfg.GDPR.TCF2.Purpose6.EnforceAlgo = "invalid2"
-	cfg.GDPR.TCF2.Purpose7.EnforceAlgo = TCF2FullEnforcement
-	cfg.GDPR.TCF2.Purpose8.EnforceAlgo = TCF2FullEnforcement
-	cfg.GDPR.TCF2.Purpose9.EnforceAlgo = TCF2FullEnforcement
+	cfg.GDPR.TCF2.Purpose7.EnforceAlgo = TCF2EnforceAlgoFull
+	cfg.GDPR.TCF2.Purpose8.EnforceAlgo = TCF2EnforceAlgoBasic
+	cfg.GDPR.TCF2.Purpose9.EnforceAlgo = TCF2EnforceAlgoFull
 	cfg.GDPR.TCF2.Purpose10.EnforceAlgo = "invalid3"
 
 	errs := cfg.validate(v)
 
 	expectedErrs := []error{
-		errors.New("gdpr.tcf2.purpose1.enforce_algo must be \"full\". Got "),
-		errors.New("gdpr.tcf2.purpose5.enforce_algo must be \"full\". Got invalid1"),
-		errors.New("gdpr.tcf2.purpose6.enforce_algo must be \"full\". Got invalid2"),
-		errors.New("gdpr.tcf2.purpose10.enforce_algo must be \"full\". Got invalid3"),
+		errors.New("gdpr.tcf2.purpose1.enforce_algo must be \"basic\" or \"full\". Got "),
+		errors.New("gdpr.tcf2.purpose5.enforce_algo must be \"basic\" or \"full\". Got invalid1"),
+		errors.New("gdpr.tcf2.purpose6.enforce_algo must be \"basic\" or \"full\". Got invalid2"),
+		errors.New("gdpr.tcf2.purpose10.enforce_algo must be \"basic\" or \"full\". Got invalid3"),
 	}
 	assert.ElementsMatch(t, errs, expectedErrs, "gdpr.tcf2.purposeX.enforce_algo should prevent invalid values but it doesn't")
 }
@@ -1924,6 +1945,61 @@ func TestTCF2PurposeEnforced(t *testing.T) {
 	}
 }
 
+func TestTCF2PurposeEnforcementAlgo(t *testing.T) {
+	tests := []struct {
+		description          string
+		givePurposeConfigNil bool
+		givePurpose1Algo     TCF2EnforcementAlgo
+		givePurpose2Algo     TCF2EnforcementAlgo
+		givePurpose          consentconstants.Purpose
+		wantAlgo             TCF2EnforcementAlgo
+	}{
+		{
+			description:          "Purpose config is nil",
+			givePurposeConfigNil: true,
+			givePurpose:          1,
+			wantAlgo:             TCF2FullEnforcement,
+		},
+		{
+			description:      "Purpose 1 enforcement algo set to basic",
+			givePurpose1Algo: TCF2BasicEnforcement,
+			givePurpose:      1,
+			wantAlgo:         TCF2BasicEnforcement,
+		},
+		{
+			description:      "Purpose 1 enforcement algo set to full",
+			givePurpose1Algo: TCF2FullEnforcement,
+			givePurpose:      1,
+			wantAlgo:         TCF2FullEnforcement,
+		},
+		{
+			description:      "Purpose 2 Enforcement algo set to basic",
+			givePurpose2Algo: TCF2BasicEnforcement,
+			givePurpose:      2,
+			wantAlgo:         TCF2BasicEnforcement,
+		},
+	}
+
+	for _, tt := range tests {
+		tcf2 := TCF2{}
+
+		if !tt.givePurposeConfigNil {
+			tcf2.PurposeConfigs = map[consentconstants.Purpose]*TCF2Purpose{
+				1: {
+					EnforceAlgoID: tt.givePurpose1Algo,
+				},
+				2: {
+					EnforceAlgoID: tt.givePurpose2Algo,
+				},
+			}
+		}
+
+		value := tcf2.PurposeEnforcementAlgo(tt.givePurpose)
+
+		assert.Equal(t, tt.wantAlgo, value, tt.description)
+	}
+}
+
 func TestTCF2PurposeEnforcingVendors(t *testing.T) {
 	tests := []struct {
 		description           string
@@ -1979,65 +2055,44 @@ func TestTCF2PurposeEnforcingVendors(t *testing.T) {
 	}
 }
 
-func TestTCF2PurposeVendorException(t *testing.T) {
+func TestTCF2PurposeVendorExceptions(t *testing.T) {
 	tests := []struct {
 		description              string
 		givePurposeConfigNil     bool
 		givePurpose1ExceptionMap map[openrtb_ext.BidderName]struct{}
 		givePurpose2ExceptionMap map[openrtb_ext.BidderName]struct{}
 		givePurpose              consentconstants.Purpose
-		giveBidder               openrtb_ext.BidderName
-		wantIsVendorException    bool
+		wantExceptionMap         map[openrtb_ext.BidderName]struct{}
 	}{
 		{
-			description:           "Purpose config is nil",
-			givePurposeConfigNil:  true,
-			givePurpose:           1,
-			giveBidder:            "appnexus",
-			wantIsVendorException: false,
+			description:          "Purpose config is nil",
+			givePurposeConfigNil: true,
+			givePurpose:          1,
+			wantExceptionMap:     map[openrtb_ext.BidderName]struct{}{},
 		},
 		{
-			description:           "Nil - exception map not defined for purpose",
-			givePurpose:           1,
-			giveBidder:            "appnexus",
-			wantIsVendorException: false,
+			description:      "Nil - exception map not defined for purpose",
+			givePurpose:      1,
+			wantExceptionMap: map[openrtb_ext.BidderName]struct{}{},
 		},
 		{
 			description:              "Empty - exception map empty for purpose",
 			givePurpose:              1,
 			givePurpose1ExceptionMap: map[openrtb_ext.BidderName]struct{}{},
-			giveBidder:               "appnexus",
-			wantIsVendorException:    false,
-		},
-		{
-			description:              "One - bidder found in purpose exception map containing one entry",
-			givePurpose:              1,
-			givePurpose1ExceptionMap: map[openrtb_ext.BidderName]struct{}{"appnexus": {}},
-			giveBidder:               "appnexus",
-			wantIsVendorException:    true,
+			wantExceptionMap:         map[openrtb_ext.BidderName]struct{}{},
 		},
 		{
-			description:              "Many - bidder found in purpose exception map containing multiple entries",
+			description:              "Nonempty - exception map with multiple entries for purpose",
 			givePurpose:              1,
 			givePurpose1ExceptionMap: map[openrtb_ext.BidderName]struct{}{"rubicon": {}, "appnexus": {}, "index": {}},
-			giveBidder:               "appnexus",
-			wantIsVendorException:    true,
+			wantExceptionMap:         map[openrtb_ext.BidderName]struct{}{"rubicon": {}, "appnexus": {}, "index": {}},
 		},
 		{
-			description:              "Many - bidder not found in purpose exception map containing multiple entries",
-			givePurpose:              1,
-			givePurpose1ExceptionMap: map[openrtb_ext.BidderName]struct{}{"rubicon": {}, "appnexus": {}, "index": {}},
-			givePurpose2ExceptionMap: map[openrtb_ext.BidderName]struct{}{"rubicon": {}, "appnexus": {}, "openx": {}},
-			giveBidder:               "openx",
-			wantIsVendorException:    false,
-		},
-		{
-			description:              "Many - bidder found in different purpose exception map containing multiple entries",
+			description:              "Nonempty - exception map with multiple entries for different purpose",
 			givePurpose:              2,
 			givePurpose1ExceptionMap: map[openrtb_ext.BidderName]struct{}{"rubicon": {}, "appnexus": {}, "index": {}},
 			givePurpose2ExceptionMap: map[openrtb_ext.BidderName]struct{}{"rubicon": {}, "appnexus": {}, "openx": {}},
-			giveBidder:               "openx",
-			wantIsVendorException:    true,
+			wantExceptionMap:         map[openrtb_ext.BidderName]struct{}{"rubicon": {}, "appnexus": {}, "openx": {}},
 		},
 	}
 
@@ -2055,9 +2110,9 @@ func TestTCF2PurposeVendorException(t *testing.T) {
 			}
 		}
 
-		value := tcf2.PurposeVendorException(tt.givePurpose, tt.giveBidder)
+		value := tcf2.PurposeVendorExceptions(tt.givePurpose)
 
-		assert.Equal(t, tt.wantIsVendorException, value, tt.description)
+		assert.Equal(t, tt.wantExceptionMap, value, tt.description)
 	}
 }
 
diff --git a/gdpr/aggregated_config_test.go b/gdpr/aggregated_config_test.go
index 093d86f7..bf2d3bbb 100644
--- a/gdpr/aggregated_config_test.go
+++ b/gdpr/aggregated_config_test.go
@@ -165,6 +165,81 @@ func TestPurposeEnforced(t *testing.T) {
 	}
 }
 
+func TestPurposeEnforcementAlgo(t *testing.T) {
+
+	tests := []struct {
+		description             string
+		givePurpose1HostAlgo    config.TCF2EnforcementAlgo
+		givePurpose1AccountAlgo config.TCF2EnforcementAlgo
+		givePurpose2HostAlgo    config.TCF2EnforcementAlgo
+		givePurpose2AccountAlgo config.TCF2EnforcementAlgo
+		givePurpose             consentconstants.Purpose
+		wantAlgo                config.TCF2EnforcementAlgo
+	}{
+		{
+			description:             "Purpose 1 set at account level - use account setting basic",
+			givePurpose1HostAlgo:    config.TCF2FullEnforcement,
+			givePurpose1AccountAlgo: config.TCF2BasicEnforcement,
+			givePurpose:             1,
+			wantAlgo:                config.TCF2BasicEnforcement,
+		},
+		{
+			description:             "Purpose 1 set at account level - use account setting full",
+			givePurpose1HostAlgo:    config.TCF2BasicEnforcement,
+			givePurpose1AccountAlgo: config.TCF2FullEnforcement,
+			givePurpose:             1,
+			wantAlgo:                config.TCF2FullEnforcement,
+		},
+		{
+			description:             "Purpose 1 not set at account level - use host setting basic",
+			givePurpose1HostAlgo:    config.TCF2BasicEnforcement,
+			givePurpose1AccountAlgo: config.TCF2UndefinedEnforcement,
+			givePurpose:             1,
+			wantAlgo:                config.TCF2BasicEnforcement,
+		},
+		{
+			description:             "Purpose 1 not set at account level - use host setting full",
+			givePurpose1HostAlgo:    config.TCF2FullEnforcement,
+			givePurpose1AccountAlgo: config.TCF2UndefinedEnforcement,
+			givePurpose:             1,
+			wantAlgo:                config.TCF2FullEnforcement,
+		},
+		{
+			description:             "Some other purpose set at account level - use account setting basic",
+			givePurpose2HostAlgo:    config.TCF2FullEnforcement,
+			givePurpose2AccountAlgo: config.TCF2BasicEnforcement,
+			givePurpose:             2,
+			wantAlgo:                config.TCF2BasicEnforcement,
+		},
+	}
+
+	for _, tt := range tests {
+		cfg := tcf2Config{
+			AccountConfig: config.AccountGDPR{
+				Purpose1: config.AccountGDPRPurpose{
+					EnforceAlgoID: tt.givePurpose1AccountAlgo,
+				},
+				Purpose2: config.AccountGDPRPurpose{
+					EnforceAlgoID: tt.givePurpose2AccountAlgo,
+				},
+			},
+			HostConfig: config.TCF2{
+				Purpose1: config.TCF2Purpose{
+					EnforceAlgoID: tt.givePurpose1HostAlgo,
+				},
+				Purpose2: config.TCF2Purpose{
+					EnforceAlgoID: tt.givePurpose2HostAlgo,
+				},
+			},
+		}
+		MakeTCF2ConfigPurposeMaps(&cfg)
+
+		result := cfg.PurposeEnforcementAlgo(consentconstants.Purpose(tt.givePurpose))
+
+		assert.Equal(t, tt.wantAlgo, result, tt.description)
+	}
+}
+
 func TestPurposeEnforcingVendors(t *testing.T) {
 	tests := []struct {
 		description                  string
@@ -239,64 +314,57 @@ func TestPurposeEnforcingVendors(t *testing.T) {
 	}
 }
 
-func TestPurposeVendorException(t *testing.T) {
+func TestPurposeVendorExceptions(t *testing.T) {
 	tests := []struct {
-		description                           string
-		givePurpose1HostVendorExceptionMap    map[openrtb_ext.BidderName]struct{}
-		givePurpose1AccountVendorExceptionMap map[openrtb_ext.BidderName]struct{}
-		givePurpose2HostVendorExceptionMap    map[openrtb_ext.BidderName]struct{}
-		givePurpose2AccountVendorExceptionMap map[openrtb_ext.BidderName]struct{}
-		givePurpose                           consentconstants.Purpose
-		giveBidder                            openrtb_ext.BidderName
-		wantVendorException                   bool
+		description                     string
+		givePurpose1HostExceptionMap    map[openrtb_ext.BidderName]struct{}
+		givePurpose1AccountExceptionMap map[openrtb_ext.BidderName]struct{}
+		givePurpose2HostExceptionMap    map[openrtb_ext.BidderName]struct{}
+		givePurpose2AccountExceptionMap map[openrtb_ext.BidderName]struct{}
+		givePurpose                     consentconstants.Purpose
+		wantExceptionMap                map[openrtb_ext.BidderName]struct{}
 	}{
 		{
-			description:                           "Purpose 1 exception list set at account level - vendor found",
-			givePurpose1HostVendorExceptionMap:    map[openrtb_ext.BidderName]struct{}{},
-			givePurpose1AccountVendorExceptionMap: map[openrtb_ext.BidderName]struct{}{"appnexus": {}, "rubicon": {}},
-			givePurpose:                           1,
-			giveBidder:                            "appnexus",
-			wantVendorException:                   true,
+			description:                     "Purpose 1 exception list set at account level - use empty account list",
+			givePurpose1HostExceptionMap:    map[openrtb_ext.BidderName]struct{}{},
+			givePurpose1AccountExceptionMap: map[openrtb_ext.BidderName]struct{}{},
+			givePurpose:                     1,
+			wantExceptionMap:                map[openrtb_ext.BidderName]struct{}{},
 		},
 		{
-			description:                           "Purpose 1 exception list set at account level - vendor not found",
-			givePurpose1HostVendorExceptionMap:    map[openrtb_ext.BidderName]struct{}{},
-			givePurpose1AccountVendorExceptionMap: map[openrtb_ext.BidderName]struct{}{"rubicon": {}},
-			givePurpose:                           1,
-			giveBidder:                            "appnexus",
-			wantVendorException:                   false,
+			description:                     "Purpose 1 exception list set at account level - use nonempty account list",
+			givePurpose1HostExceptionMap:    map[openrtb_ext.BidderName]struct{}{},
+			givePurpose1AccountExceptionMap: map[openrtb_ext.BidderName]struct{}{"appnexus": {}, "rubicon": {}},
+			givePurpose:                     1,
+			wantExceptionMap:                map[openrtb_ext.BidderName]struct{}{"appnexus": {}, "rubicon": {}},
 		},
 		{
-			description:                           "Purpose 1 exception list not set at account level - vendor found in host list",
-			givePurpose1HostVendorExceptionMap:    map[openrtb_ext.BidderName]struct{}{"appnexus": {}, "rubicon": {}},
-			givePurpose1AccountVendorExceptionMap: nil,
-			givePurpose:                           1,
-			giveBidder:                            "appnexus",
-			wantVendorException:                   true,
+			description:                     "Purpose 1 exception list not set at account level - use empty host list",
+			givePurpose1HostExceptionMap:    map[openrtb_ext.BidderName]struct{}{},
+			givePurpose1AccountExceptionMap: nil,
+			givePurpose:                     1,
+			wantExceptionMap:                map[openrtb_ext.BidderName]struct{}{},
 		},
 		{
-			description:                           "Purpose 1 exception list not set at account level - vendor not found in host list",
-			givePurpose1HostVendorExceptionMap:    map[openrtb_ext.BidderName]struct{}{"rubicon": {}},
-			givePurpose1AccountVendorExceptionMap: nil,
-			givePurpose:                           1,
-			giveBidder:                            "appnexus",
-			wantVendorException:                   false,
+			description:                     "Purpose 1 exception list not set at account level - use nonempty host list",
+			givePurpose1HostExceptionMap:    map[openrtb_ext.BidderName]struct{}{"appnexus": {}, "rubicon": {}},
+			givePurpose1AccountExceptionMap: nil,
+			givePurpose:                     1,
+			wantExceptionMap:                map[openrtb_ext.BidderName]struct{}{"appnexus": {}, "rubicon": {}},
 		},
 		{
-			description:                           "Purpose 1 exception list not set at account level or host level - vendor not found",
-			givePurpose1HostVendorExceptionMap:    nil,
-			givePurpose1AccountVendorExceptionMap: nil,
-			givePurpose:                           1,
-			giveBidder:                            "appnexus",
-			wantVendorException:                   false,
+			description:                     "Purpose 1 exception list not set at account level or host level",
+			givePurpose1HostExceptionMap:    nil,
+			givePurpose1AccountExceptionMap: nil,
+			givePurpose:                     1,
+			wantExceptionMap:                map[openrtb_ext.BidderName]struct{}{},
 		},
 		{
-			description:                           "Some other purpose exception list set at account level - vendor found",
-			givePurpose2HostVendorExceptionMap:    map[openrtb_ext.BidderName]struct{}{},
-			givePurpose2AccountVendorExceptionMap: map[openrtb_ext.BidderName]struct{}{"appnexus": {}, "rubicon": {}},
-			givePurpose:                           2,
-			giveBidder:                            "appnexus",
-			wantVendorException:                   true,
+			description:                     "Some other purpose exception list set at account level",
+			givePurpose2HostExceptionMap:    map[openrtb_ext.BidderName]struct{}{},
+			givePurpose2AccountExceptionMap: map[openrtb_ext.BidderName]struct{}{"appnexus": {}, "rubicon": {}},
+			givePurpose:                     2,
+			wantExceptionMap:                map[openrtb_ext.BidderName]struct{}{"appnexus": {}, "rubicon": {}},
 		},
 	}
 
@@ -304,26 +372,26 @@ func TestPurposeVendorException(t *testing.T) {
 		cfg := tcf2Config{
 			AccountConfig: config.AccountGDPR{
 				Purpose1: config.AccountGDPRPurpose{
-					VendorExceptionMap: tt.givePurpose1AccountVendorExceptionMap,
+					VendorExceptionMap: tt.givePurpose1AccountExceptionMap,
 				},
 				Purpose2: config.AccountGDPRPurpose{
-					VendorExceptionMap: tt.givePurpose2AccountVendorExceptionMap,
+					VendorExceptionMap: tt.givePurpose2AccountExceptionMap,
 				},
 			},
 			HostConfig: config.TCF2{
 				Purpose1: config.TCF2Purpose{
-					VendorExceptionMap: tt.givePurpose1HostVendorExceptionMap,
+					VendorExceptionMap: tt.givePurpose1HostExceptionMap,
 				},
 				Purpose2: config.TCF2Purpose{
-					VendorExceptionMap: tt.givePurpose2HostVendorExceptionMap,
+					VendorExceptionMap: tt.givePurpose2HostExceptionMap,
 				},
 			},
 		}
 		MakeTCF2ConfigPurposeMaps(&cfg)
 
-		result := cfg.PurposeVendorException(consentconstants.Purpose(tt.givePurpose), tt.giveBidder)
+		result := cfg.PurposeVendorExceptions(consentconstants.Purpose(tt.givePurpose))
 
-		assert.Equal(t, tt.wantVendorException, result, tt.description)
+		assert.Equal(t, tt.wantExceptionMap, result, tt.description)
 	}
 }
 
@@ -552,42 +620,39 @@ func TestPurposeOneTreatmentAllowed(t *testing.T) {
 	}
 }
 
-func TestBasicEnforcementVendor(t *testing.T) {
+func TestBasicEnforcementVendors(t *testing.T) {
 	tests := []struct {
-		description                          string
-		giveAccountBasicEnforcementVendorMap map[string]struct{}
-		giveBidder                           openrtb_ext.BidderName
-		wantBasicEnforcement                 bool
+		description               string
+		giveAccountBasicVendorMap map[string]struct{}
+		wantBasicVendorMap        map[string]struct{}
 	}{
 		{
-			description:                          "Basic enforcement vendor list set at account level - vendor found",
-			giveAccountBasicEnforcementVendorMap: map[string]struct{}{"appnexus": {}, "rubicon": {}},
-			giveBidder:                           "appnexus",
-			wantBasicEnforcement:                 true,
+			description:               "Purpose 1 basic exception vendor list not set at account level",
+			giveAccountBasicVendorMap: nil,
+			wantBasicVendorMap:        map[string]struct{}{},
 		},
 		{
-			description:                          "Basic enforcement vendor list set at account level - vendor not found",
-			giveAccountBasicEnforcementVendorMap: map[string]struct{}{"rubicon": {}},
-			giveBidder:                           "appnexus",
-			wantBasicEnforcement:                 false,
+			description:               "Purpose 1 basic exception vendor list set at account level as empty list",
+			giveAccountBasicVendorMap: map[string]struct{}{},
+			wantBasicVendorMap:        map[string]struct{}{},
 		},
 		{
-			description:                          "Basic enforcement vendor list not set at account level - vendor not found",
-			giveAccountBasicEnforcementVendorMap: nil,
-			giveBidder:                           "appnexus",
-			wantBasicEnforcement:                 false,
+			description:               "Purpose 1 basic exception vendor list not set at account level as nonempty list",
+			giveAccountBasicVendorMap: map[string]struct{}{"appnexus": {}, "rubicon": {}},
+			wantBasicVendorMap:        map[string]struct{}{"appnexus": {}, "rubicon": {}},
 		},
 	}
 
 	for _, tt := range tests {
 		cfg := tcf2Config{
 			AccountConfig: config.AccountGDPR{
-				BasicEnforcementVendorsMap: tt.giveAccountBasicEnforcementVendorMap,
+				BasicEnforcementVendorsMap: tt.giveAccountBasicVendorMap,
 			},
 		}
+		MakeTCF2ConfigPurposeMaps(&cfg)
 
-		result := cfg.BasicEnforcementVendor(tt.giveBidder)
+		result := cfg.BasicEnforcementVendors()
 
-		assert.Equal(t, tt.wantBasicEnforcement, result, tt.description)
+		assert.Equal(t, tt.wantBasicVendorMap, result, tt.description)
 	}
 }
diff --git a/gdpr/basic_enforcement_test.go b/gdpr/basic_enforcement_test.go
new file mode 100644
index 00000000..c49e59ea
--- /dev/null
+++ b/gdpr/basic_enforcement_test.go
@@ -0,0 +1,240 @@
+package gdpr
+
+import (
+	"testing"
+
+	"github.com/prebid/go-gdpr/consentconstants"
+	"github.com/prebid/go-gdpr/vendorconsent"
+	tcf2 "github.com/prebid/go-gdpr/vendorconsent/tcf2"
+	"github.com/prebid/prebid-server/openrtb_ext"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestBasicLegalBasis(t *testing.T) {
+	appnexusID := uint16(32)
+
+	noConsents := "CPerMsAPerMsAAAAAAENCfCAAAAAAAAAAAAAAAAAAAAA"
+	purpose2Consent := "CPerMsAPerMsAAAAAAENCfCAAEAAAAAAAAAAAAAAAAAA"
+	purpose2LI := "CPerMsAPerMsAAAAAAENCfCAAAAAAEAAAAAAAAAAAAAA"
+	vendor32Consent := "CPerMsAPerMsAAAAAAENCfCAAAAAAAAAAAAAAQAAAAAEAAAAAAAA"
+	purpose2AndVendor32Consent := "CPerMsAPerMsAAAAAAENCfCAAEAAAAAAAAAAAQAAAAAEAAAAAAAA"
+
+	tests := []struct {
+		description string
+		config      purposeConfig
+		consent     string
+		overrides   Overrides
+		wantResult  bool
+	}{
+		{
+			description: "enforce purpose & vendors are off",
+			consent:     noConsents,
+			config: purposeConfig{
+				PurposeID:      consentconstants.Purpose(2),
+				EnforcePurpose: false,
+				EnforceVendors: false,
+			},
+			wantResult: true,
+		},
+		{
+			description: "enforce purpose on, purpose consent N",
+			consent:     noConsents,
+			config: purposeConfig{
+				PurposeID:      consentconstants.Purpose(2),
+				EnforcePurpose: true,
+				EnforceVendors: false,
+			},
+			wantResult: false,
+		},
+		{
+			description: "enforce purpose on, purpose consent Y",
+			consent:     purpose2Consent,
+			config: purposeConfig{
+				PurposeID:      consentconstants.Purpose(2),
+				EnforcePurpose: true,
+				EnforceVendors: false,
+			},
+			wantResult: true,
+		},
+		{
+			description: "enforce purpose on, purpose consent Y, enforce vendors off but overrides treats it as on",
+			consent:     purpose2Consent,
+			config: purposeConfig{
+				PurposeID:      consentconstants.Purpose(2),
+				EnforcePurpose: true,
+				EnforceVendors: false,
+			},
+			overrides:  Overrides{enforceVendors: true},
+			wantResult: false,
+		},
+		{
+			description: "enforce purpose on, purpose consent Y, vendor consent Y, enforce vendors off but overrides treats it as on",
+			consent:     purpose2AndVendor32Consent,
+			config: purposeConfig{
+				PurposeID:      consentconstants.Purpose(2),
+				EnforcePurpose: true,
+				EnforceVendors: false,
+			},
+			overrides:  Overrides{enforceVendors: true},
+			wantResult: true,
+		},
+		{
+			description: "enforce purpose on, purpose LI Transparency Y",
+			consent:     purpose2LI,
+			config: purposeConfig{
+				PurposeID:      consentconstants.Purpose(2),
+				EnforcePurpose: true,
+				EnforceVendors: false,
+			},
+			wantResult: false,
+		},
+		{
+			description: "enforce purpose on, purpose LI Transparency Y but overrides allow it",
+			consent:     purpose2LI,
+			config: purposeConfig{
+				PurposeID:      consentconstants.Purpose(2),
+				EnforcePurpose: true,
+				EnforceVendors: false,
+			},
+			overrides:  Overrides{allowLITransparency: true},
+			wantResult: true,
+		},
+		{
+			description: "enforce vendors on, vendor consent N",
+			consent:     noConsents,
+			config: purposeConfig{
+				PurposeID:      consentconstants.Purpose(2),
+				EnforcePurpose: false,
+				EnforceVendors: true,
+			},
+			wantResult: false,
+		},
+		{
+			description: "enforce vendors on, vendor consent Y",
+			consent:     vendor32Consent,
+			config: purposeConfig{
+				PurposeID:      consentconstants.Purpose(2),
+				EnforcePurpose: false,
+				EnforceVendors: true,
+			},
+			wantResult: true,
+		},
+		{
+			description: "enforce vendors on, vendor consent Y, enforce purpose off but overrides treats it as on",
+			consent:     vendor32Consent,
+			config: purposeConfig{
+				PurposeID:      consentconstants.Purpose(2),
+				EnforcePurpose: false,
+				EnforceVendors: true,
+			},
+			overrides:  Overrides{enforcePurpose: true},
+			wantResult: false,
+		},
+		{
+			description: "enforce vendors on, purpose consent Y, vendor consent Y, enforce purpose off but overrides treats it as on",
+			consent:     purpose2AndVendor32Consent,
+			config: purposeConfig{
+				PurposeID:      consentconstants.Purpose(2),
+				EnforcePurpose: false,
+				EnforceVendors: true,
+			},
+			overrides:  Overrides{enforcePurpose: true},
+			wantResult: true,
+		},
+		{
+			description: "enforce vendors on, vendor consent N, bidder is a basic vendor",
+			consent:     noConsents,
+			config: purposeConfig{
+				PurposeID:                  consentconstants.Purpose(2),
+				EnforcePurpose:             false,
+				EnforceVendors:             true,
+				BasicEnforcementVendorsMap: map[string]struct{}{string(openrtb_ext.BidderAppnexus): {}},
+			},
+			wantResult: true,
+		},
+		{
+			description: "enforce purpose & vendors are on",
+			consent:     noConsents,
+			config: purposeConfig{
+				PurposeID:      consentconstants.Purpose(2),
+				EnforcePurpose: true,
+				EnforceVendors: true,
+			},
+			wantResult: false,
+		},
+		{
+			description: "enforce purpose & vendors are on, bidder is a vendor exception",
+			consent:     noConsents,
+			config: purposeConfig{
+				PurposeID:          consentconstants.Purpose(2),
+				EnforcePurpose:     true,
+				EnforceVendors:     true,
+				VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderAppnexus: {}},
+			},
+			wantResult: true,
+		},
+		{
+			description: "enforce purpose & vendors are on, bidder is a vendor exception but overrides disallow them",
+			consent:     noConsents,
+			config: purposeConfig{
+				PurposeID:          consentconstants.Purpose(2),
+				EnforcePurpose:     true,
+				EnforceVendors:     true,
+				VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderAppnexus: {}},
+			},
+			overrides:  Overrides{blockVendorExceptions: true},
+			wantResult: false,
+		},
+		{
+			description: "enforce purpose & vendors are on, purpose consent Y, vendor consent N",
+			consent:     purpose2Consent,
+			config: purposeConfig{
+				PurposeID:      consentconstants.Purpose(2),
+				EnforcePurpose: true,
+				EnforceVendors: true,
+			},
+			wantResult: false,
+		},
+		{
+			description: "enforce purpose & vendors are on, purpose consent Y, vendor consent N, bidder is a basic vendor",
+			consent:     purpose2Consent,
+			config: purposeConfig{
+				PurposeID:                  consentconstants.Purpose(2),
+				EnforcePurpose:             true,
+				EnforceVendors:             true,
+				BasicEnforcementVendorsMap: map[string]struct{}{string(openrtb_ext.BidderAppnexus): {}},
+			},
+			wantResult: true,
+		},
+		{
+			description: "enforce purpose & vendors are on, purpose consent Y, vendor consent Y",
+			consent:     purpose2AndVendor32Consent,
+			config: purposeConfig{
+				PurposeID:      consentconstants.Purpose(2),
+				EnforcePurpose: true,
+				EnforceVendors: true,
+			},
+			wantResult: true,
+		},
+	}
+
+	for _, tt := range tests {
+		// convert consent string to TCF2 object
+		parsedConsent, err := vendorconsent.ParseString(tt.consent)
+		if err != nil {
+			t.Fatalf("Failed to parse consent %s: %s\n", tt.consent, tt.description)
+		}
+		consentMeta, ok := parsedConsent.(tcf2.ConsentMetadata)
+		if !ok {
+			t.Fatalf("Failed to convert consent %s: %s\n", tt.consent, tt.description)
+		}
+
+		enforcer := BasicEnforcement{cfg: tt.config}
+
+		vendorInfo := VendorInfo{vendorID: appnexusID, vendor: nil}
+		result := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, tt.overrides)
+
+		assert.Equal(t, tt.wantResult, result, tt.description)
+	}
+}
diff --git a/gdpr/full_enforcement_test.go b/gdpr/full_enforcement_test.go
new file mode 100644
index 00000000..e8ccd1e4
--- /dev/null
+++ b/gdpr/full_enforcement_test.go
@@ -0,0 +1,928 @@
+package gdpr
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/prebid/go-gdpr/consentconstants"
+	"github.com/prebid/go-gdpr/vendorconsent"
+	tcf2 "github.com/prebid/go-gdpr/vendorconsent/tcf2"
+	"github.com/prebid/go-gdpr/vendorlist"
+	"github.com/prebid/go-gdpr/vendorlist2"
+	"github.com/prebid/prebid-server/openrtb_ext"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestLegalBasisWithPubRestrictionAllowNone(t *testing.T) {
+	appnexusID := uint16(32)
+
+	NoConsentsWithP1P2P3V32RestrictionAllowNone := "CPfMKEAPfMKEAAAAAAENCgCAAAAAAAAAAAAAAQAAAAAAAIAAAAAAAGCAAgAgCAAQAQBgAIAIAAAA"
+	P1P2P3PurposeConsentAndV32VendorConsentWithP1P2P3V32RestrictionAllowNone := "CPfMKEAPfMKEAAAAAAENCgCAAOAAAAAAAAAAAQAAAAAEAIAAAAAAAGCAAgAgCAAQAQBgAIAIAAAA"
+
+	tests := []struct {
+		description              string
+		config                   purposeConfig
+		consent                  string
+		wantConsentPurposeResult bool
+		wantLIPurposeResult      bool
+		wantFlexPurposeResult    bool
+	}{
+		{
+			description: "enforce purpose & vendors off",
+			config: purposeConfig{
+				EnforcePurpose: false,
+				EnforceVendors: false,
+			},
+			consent:                  NoConsentsWithP1P2P3V32RestrictionAllowNone,
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+		{
+			description: "enforce purpose & vendors on, bidder is a vendor exception",
+			config: purposeConfig{
+				EnforcePurpose:     true,
+				EnforceVendors:     true,
+				VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderAppnexus: {}},
+			},
+			consent:                  NoConsentsWithP1P2P3V32RestrictionAllowNone,
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+		{
+			description: "enforce purpose & vendors on, purpose consent Y, vendor consent Y",
+			config: purposeConfig{
+				EnforcePurpose: true,
+				EnforceVendors: true,
+			},
+			consent:                  P1P2P3PurposeConsentAndV32VendorConsentWithP1P2P3V32RestrictionAllowNone,
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+	}
+
+	for _, tt := range tests {
+		// convert consent string to TCF2 object
+		parsedConsent, err := vendorconsent.ParseString(tt.consent)
+		if err != nil {
+			t.Fatalf("Failed to parse consent %s: %s\n", tt.consent, tt.description)
+		}
+		consentMeta, ok := parsedConsent.(tcf2.ConsentMetadata)
+		if !ok {
+			t.Fatalf("Failed to convert consent %s: %s\n", tt.consent, tt.description)
+		}
+
+		vendor := getVendorList(t).Vendor(appnexusID)
+		vendorInfo := VendorInfo{vendorID: appnexusID, vendor: vendor}
+		enforcer := FullEnforcement{cfg: tt.config}
+
+		enforcer.cfg.PurposeID = consentconstants.Purpose(1)
+		consentPurposeResult := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, Overrides{})
+		assert.Equal(t, tt.wantConsentPurposeResult, consentPurposeResult, tt.description+" -- GVL consent purpose")
+
+		enforcer.cfg.PurposeID = consentconstants.Purpose(2)
+		LIPurposeresult := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, Overrides{})
+		assert.Equal(t, tt.wantLIPurposeResult, LIPurposeresult, tt.description+" -- GVL LI purpose")
+
+		enforcer.cfg.PurposeID = consentconstants.Purpose(3)
+		flexPurposeResult := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, Overrides{})
+		assert.Equal(t, tt.wantFlexPurposeResult, flexPurposeResult, tt.description+" -- GVL flex purpose")
+	}
+}
+
+func TestLegalBasisWithNoPubRestrictionsAndWithPubRestrictionAllowAll(t *testing.T) {
+	appnexusID := uint16(32)
+
+	NoConsents := "CPfCRQAPfCRQAAAAAAENCgCAAAAAAAAAAAAAAAAAAAAA"
+	P1P2P3PurposeConsent := "CPfCRQAPfCRQAAAAAAENCgCAAOAAAAAAAAAAAAAAAAAA"
+	P1P2P3PurposeLI := "CPfCRQAPfCRQAAAAAAENCgCAAAAAAOAAAAAAAAAAAAAA"
+	V32VendorConsent := "CPfCRQAPfCRQAAAAAAENCgCAAAAAAAAAAAAAAQAAAAAEAAAAAAAA"
+	V32VendorLI := "CPfCRQAPfCRQAAAAAAENCgCAAAAAAAAAAAAAAQAAAAAAAIAAAAACAAAA"
+	P1P2P3PurposeConsentAndV32VendorConsent := "CPfCRQAPfCRQAAAAAAENCgCAAOAAAAAAAAAAAQAAAAAEAIAAAAAAAAAA"
+	P1P2P3PurposeLIAndV32VendorLI := "CPfCRQAPfCRQAAAAAAENCgCAAAAAAOAAAAAAAQAAAAAAAIAAAAACAAAA"
+
+	NoConsentsWithP1P2P3V32RestrictionAllowAll := "CPfMKEAPfMKEAAAAAAENCgCAAAAAAAAAAAAAAQAAAAAAAIAAAAAAAGDgAgAgCwAQAQB4AIAIAAAA"
+	P1P2P3PurposeConsentWithP1P2P3V32RestrictionAllowAll := "CPfMKEAPfMKEAAAAAAENCgCAAOAAAAAAAAAAAQAAAAAAAIAAAAAAAGDgAgAgCwAQAQB4AIAIAAAA"
+	P1P2P3PurposeLIWithP1P2P3V32RestrictionAllowAll := "CPfMKEAPfMKEAAAAAAENCgCAAAAAAOAAAAAAAQAAAAAAAIAAAAAAAGDgAgAgCwAQAQB4AIAIAAAA"
+	V32VendorConsentWithP1P2P3V32RestrictionAllowAll := "CPfMKEAPfMKEAAAAAAENCgCAAAAAAAAAAAAAAQAAAAAEAIAAAAAAAGDgAgAgCwAQAQB4AIAIAAAA"
+	V32VendorLIWithP1P2P3V32RestrictionAllowAll := "CPfMKEAPfMKEAAAAAAENCgCAAAAAAAAAAAAAAQAAAAAAAIAAAAACAGDgAgAgCwAQAQB4AIAIAAAA"
+	P1P2P3PurposeConsentAndV32VendorConsentWithP1P2P3V32RestrictionAllowAll := "CPfMKEAPfMKEAAAAAAENCgCAAOAAAAAAAAAAAQAAAAAEAIAAAAAAAGDgAgAgCwAQAQB4AIAIAAAA"
+	P1P2P3PurposeLIAndV32VendorLIWithP1P2P3V32RestrictionAllowAll := "CPfMKEAPfMKEAAAAAAENCgCAAAAAAOAAAAAAAQAAAAAAAIAAAAACAGDgAgAgCwAQAQB4AIAIAAAA"
+
+	tests := []struct {
+		description               string
+		config                    purposeConfig
+		consentNoPubRestriction   string
+		consentWithPubRestriction string
+		overrides                 Overrides
+		wantConsentPurposeResult  bool
+		wantLIPurposeResult       bool
+		wantFlexPurposeResult     bool
+	}{
+		{
+			description: "enforce purpose & vendors off",
+			config: purposeConfig{
+				EnforcePurpose: false,
+				EnforceVendors: false,
+			},
+			consentNoPubRestriction:   NoConsents,
+			consentWithPubRestriction: NoConsentsWithP1P2P3V32RestrictionAllowAll,
+			wantConsentPurposeResult:  true,
+			wantLIPurposeResult:       true,
+			wantFlexPurposeResult:     true,
+		},
+		{
+			description: "enforce purpose on, purpose consent N, legit interest N",
+			config: purposeConfig{
+				EnforcePurpose: true,
+			},
+			consentNoPubRestriction:   NoConsents,
+			consentWithPubRestriction: NoConsentsWithP1P2P3V32RestrictionAllowAll,
+			wantConsentPurposeResult:  false,
+			wantLIPurposeResult:       false,
+			wantFlexPurposeResult:     false,
+		},
+		{
+			description: "enforce purpose on, purpose consent Y",
+			config: purposeConfig{
+				EnforcePurpose: true,
+			},
+			consentNoPubRestriction:   P1P2P3PurposeConsent,
+			consentWithPubRestriction: P1P2P3PurposeConsentWithP1P2P3V32RestrictionAllowAll,
+			wantConsentPurposeResult:  true,
+			wantLIPurposeResult:       false,
+			wantFlexPurposeResult:     true,
+		},
+		{
+			description: "enforce purpose on, legit interest Y",
+			config: purposeConfig{
+				EnforcePurpose: true,
+			},
+			consentNoPubRestriction:   P1P2P3PurposeLI,
+			consentWithPubRestriction: P1P2P3PurposeLIWithP1P2P3V32RestrictionAllowAll,
+			wantConsentPurposeResult:  false,
+			wantLIPurposeResult:       true,
+			wantFlexPurposeResult:     true,
+		},
+		{
+			description: "enforce purpose on, purpose consent Y, enforce vendors off but overrides treats it as on",
+			config: purposeConfig{
+				EnforcePurpose: true,
+			},
+			consentNoPubRestriction:   P1P2P3PurposeConsent,
+			consentWithPubRestriction: P1P2P3PurposeConsentWithP1P2P3V32RestrictionAllowAll,
+			overrides:                 Overrides{enforceVendors: true},
+			wantConsentPurposeResult:  false,
+			wantLIPurposeResult:       false,
+			wantFlexPurposeResult:     false,
+		},
+		{
+			description: "enforce purpose on, purpose consent Y, vendor consent Y, enforce vendors off but overrides treats it as on",
+			config: purposeConfig{
+				EnforcePurpose: true,
+			},
+			consentNoPubRestriction:   P1P2P3PurposeConsentAndV32VendorConsent,
+			consentWithPubRestriction: P1P2P3PurposeConsentAndV32VendorConsentWithP1P2P3V32RestrictionAllowAll,
+			overrides:                 Overrides{enforceVendors: true},
+			wantConsentPurposeResult:  true,
+			wantLIPurposeResult:       false,
+			wantFlexPurposeResult:     true,
+		},
+		{
+			description: "enforce vendors on, vendor consent N, vendor legit interest N",
+			config: purposeConfig{
+				EnforceVendors: true,
+			},
+			consentNoPubRestriction:   NoConsents,
+			consentWithPubRestriction: NoConsentsWithP1P2P3V32RestrictionAllowAll,
+			wantConsentPurposeResult:  false,
+			wantLIPurposeResult:       false,
+			wantFlexPurposeResult:     false,
+		},
+		{
+			description: "enforce vendors on, vendor consent Y",
+			config: purposeConfig{
+				EnforceVendors: true,
+			},
+			consentNoPubRestriction:   V32VendorConsent,
+			consentWithPubRestriction: V32VendorConsentWithP1P2P3V32RestrictionAllowAll,
+			wantConsentPurposeResult:  true,
+			wantLIPurposeResult:       false,
+			wantFlexPurposeResult:     true,
+		},
+		{
+			description: "enforce vendors on, vendor legit interest Y",
+			config: purposeConfig{
+				EnforceVendors: true,
+			},
+			consentNoPubRestriction:   V32VendorLI,
+			consentWithPubRestriction: V32VendorLIWithP1P2P3V32RestrictionAllowAll,
+			wantConsentPurposeResult:  false,
+			wantLIPurposeResult:       true,
+			wantFlexPurposeResult:     true,
+		},
+		{
+			description: "enforce vendors on, vendor consent Y, enforce purpose off but overrides treats it as on",
+			config: purposeConfig{
+				EnforceVendors: true,
+			},
+			consentNoPubRestriction:   V32VendorConsent,
+			consentWithPubRestriction: V32VendorConsentWithP1P2P3V32RestrictionAllowAll,
+			overrides:                 Overrides{enforcePurpose: true},
+			wantConsentPurposeResult:  false,
+			wantLIPurposeResult:       false,
+			wantFlexPurposeResult:     false,
+		},
+		{
+			description: "enforce vendors on, purpose consent Y, vendor consent Y, enforce purpose off but overrides treats it as on",
+			config: purposeConfig{
+				EnforceVendors: true,
+			},
+			consentNoPubRestriction:   P1P2P3PurposeConsentAndV32VendorConsent,
+			consentWithPubRestriction: P1P2P3PurposeConsentAndV32VendorConsentWithP1P2P3V32RestrictionAllowAll,
+			overrides:                 Overrides{enforcePurpose: true},
+			wantConsentPurposeResult:  true,
+			wantLIPurposeResult:       false,
+			wantFlexPurposeResult:     true,
+		},
+		{
+			description: "enforce purpose & vendors on, purpose consent Y, vendor consent N",
+			config: purposeConfig{
+				EnforcePurpose: true,
+				EnforceVendors: true,
+			},
+			consentNoPubRestriction:   P1P2P3PurposeConsent,
+			consentWithPubRestriction: P1P2P3PurposeConsentWithP1P2P3V32RestrictionAllowAll,
+			wantConsentPurposeResult:  false,
+			wantLIPurposeResult:       false,
+			wantFlexPurposeResult:     false,
+		},
+		{
+			description: "enforce purpose & vendors on, purpose consent N, vendor consent Y",
+			config: purposeConfig{
+				EnforcePurpose: true,
+				EnforceVendors: true,
+			},
+			consentNoPubRestriction:   V32VendorConsent,
+			consentWithPubRestriction: V32VendorConsentWithP1P2P3V32RestrictionAllowAll,
+			wantConsentPurposeResult:  false,
+			wantLIPurposeResult:       false,
+			wantFlexPurposeResult:     false,
+		},
+		{
+			description: "enforce purpose & vendors on, purpose consent Y, vendor consent Y",
+			config: purposeConfig{
+				EnforcePurpose: true,
+				EnforceVendors: true,
+			},
+			consentNoPubRestriction:   P1P2P3PurposeConsentAndV32VendorConsent,
+			consentWithPubRestriction: P1P2P3PurposeConsentAndV32VendorConsentWithP1P2P3V32RestrictionAllowAll,
+			wantConsentPurposeResult:  true,
+			wantLIPurposeResult:       false,
+			wantFlexPurposeResult:     true,
+		},
+		{
+			description: "enforce purpose & vendors on, legit interest Y, vendor legit interest N",
+			config: purposeConfig{
+				EnforcePurpose: true,
+				EnforceVendors: true,
+			},
+			consentNoPubRestriction:   P1P2P3PurposeLI,
+			consentWithPubRestriction: P1P2P3PurposeLIWithP1P2P3V32RestrictionAllowAll,
+			wantConsentPurposeResult:  false,
+			wantLIPurposeResult:       false,
+			wantFlexPurposeResult:     false,
+		},
+		{
+			description: "enforce purpose & vendors on, legit interest N, vendor legit interest Y",
+			config: purposeConfig{
+				EnforcePurpose: true,
+				EnforceVendors: true,
+			},
+			consentNoPubRestriction:   V32VendorLI,
+			consentWithPubRestriction: V32VendorLIWithP1P2P3V32RestrictionAllowAll,
+			wantConsentPurposeResult:  false,
+			wantLIPurposeResult:       false,
+			wantFlexPurposeResult:     false,
+		},
+		{
+			description: "enforce purpose & vendors on, legit interest Y, vendor legit interest Y",
+			config: purposeConfig{
+				EnforcePurpose: true,
+				EnforceVendors: true,
+			},
+			consentNoPubRestriction:   P1P2P3PurposeLIAndV32VendorLI,
+			consentWithPubRestriction: P1P2P3PurposeLIAndV32VendorLIWithP1P2P3V32RestrictionAllowAll,
+			wantConsentPurposeResult:  false,
+			wantLIPurposeResult:       true,
+			wantFlexPurposeResult:     true,
+		},
+		{
+			description: "enforce purpose & vendors on, bidder is a vendor exception",
+			config: purposeConfig{
+				EnforcePurpose:     true,
+				EnforceVendors:     true,
+				VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderAppnexus: {}},
+			},
+			consentNoPubRestriction:   NoConsents,
+			consentWithPubRestriction: NoConsentsWithP1P2P3V32RestrictionAllowAll,
+			wantConsentPurposeResult:  true,
+			wantLIPurposeResult:       true,
+			wantFlexPurposeResult:     true,
+		},
+		{
+			description: "enforce purpose & vendors on, bidder is a vendor exception but overrides disallow them",
+			config: purposeConfig{
+				EnforcePurpose:     true,
+				EnforceVendors:     true,
+				VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderAppnexus: {}},
+			},
+			consentNoPubRestriction:   NoConsents,
+			consentWithPubRestriction: NoConsentsWithP1P2P3V32RestrictionAllowAll,
+			overrides:                 Overrides{blockVendorExceptions: true},
+			wantConsentPurposeResult:  false,
+			wantLIPurposeResult:       false,
+			wantFlexPurposeResult:     false,
+		},
+	}
+
+	for _, tt := range tests {
+		consents := [2]string{tt.consentNoPubRestriction, tt.consentWithPubRestriction}
+
+		for i := 0; i < len(consents); i++ {
+			consent := consents[i]
+
+			// convert consent string to TCF2 object
+			parsedConsent, err := vendorconsent.ParseString(consent)
+			if err != nil {
+				t.Fatalf("Failed to parse consent %s: %s\n", consent, tt.description)
+			}
+			consentMeta, ok := parsedConsent.(tcf2.ConsentMetadata)
+			if !ok {
+				t.Fatalf("Failed to convert consent %s: %s\n", consent, tt.description)
+			}
+
+			vendor := getVendorList(t).Vendor(appnexusID)
+			vendorInfo := VendorInfo{vendorID: appnexusID, vendor: vendor}
+			enforcer := FullEnforcement{cfg: tt.config}
+
+			enforcer.cfg.PurposeID = consentconstants.Purpose(1)
+			consentPurposeResult := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, tt.overrides)
+			assert.Equal(t, tt.wantConsentPurposeResult, consentPurposeResult, tt.description+" -- GVL consent purpose -- consent string %d of %d", i+1, len(consents))
+
+			enforcer.cfg.PurposeID = consentconstants.Purpose(2)
+			LIPurposeresult := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, tt.overrides)
+			assert.Equal(t, tt.wantLIPurposeResult, LIPurposeresult, tt.description+" -- GVL LI purpose -- consent string %d of %d", i+1, len(consents))
+
+			enforcer.cfg.PurposeID = consentconstants.Purpose(3)
+			flexPurposeResult := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, tt.overrides)
+			assert.Equal(t, tt.wantFlexPurposeResult, flexPurposeResult, tt.description+" -- GVL flex purpose -- consent string %d of %d", i+1, len(consents))
+		}
+	}
+}
+
+func TestLegalBasisWithPubRestrictionRequireConsent(t *testing.T) {
+	appnexusID := uint16(32)
+
+	NoConsentsWithP1P2P3V32RestrictionRequireConsent := "CPfFkMAPfFkMAAAAAAENCgCAAAAAAAAAAAAAAQAAAAAAAIAAAAAAAGCgAgAgCQAQAQBoAIAIAAAA"
+	P1P2P3PurposeConsentWithP1P2P3V32RestrictionRequireConsent := "CPfFkMAPfFkMAAAAAAENCgCAAOAAAAAAAAAAAQAAAAAAAIAAAAAAAGCgAgAgCQAQAQBoAIAIAAAA"
+	P1P2P3PurposeLIWithP1P2P3V32RestrictionRequireConsent := "CPfFkMAPfFkMAAAAAAENCgCAAAAAAOAAAAAAAQAAAAAAAIAAAAAAAGCgAgAgCQAQAQBoAIAIAAAA"
+	V32VendorConsentWithP1P2P3V32RestrictionRequireConsent := "CPfFkMAPfFkMAAAAAAENCgCAAAAAAAAAAAAAAQAAAAAEAIAAAAAAAGCgAgAgCQAQAQBoAIAIAAAA"
+	V32VendorLIWithP1P2P3V32RestrictionRequireConsent := "CPfFkMAPfFkMAAAAAAENCgCAAAAAAAAAAAAAAQAAAAAAAIAAAAACAGCgAgAgCQAQAQBoAIAIAAAA"
+	P1P2P3PurposeConsentAndV32VendorConsentWithP1P2P3V32RestrictionRequireConsent := "CPfFkMAPfFkMAAAAAAENCgCAAOAAAAAAAAAAAQAAAAAEAIAAAAAAAGCgAgAgCQAQAQBoAIAIAAAA"
+	P1P2P3PurposeLIAndV32VendorLIWithP1P2P3V32RestrictionRequireConsent := "CPfFkMAPfFkMAAAAAAENCgCAAAAAAOAAAAAAAQAAAAAAAIAAAAACAGCgAgAgCQAQAQBoAIAIAAAA"
+
+	tests := []struct {
+		description              string
+		config                   purposeConfig
+		consent                  string
+		overrides                Overrides
+		wantConsentPurposeResult bool
+		wantLIPurposeResult      bool
+		wantFlexPurposeResult    bool
+	}{
+		{
+			description: "enforce purpose & vendors off",
+			config: purposeConfig{
+				EnforcePurpose: false,
+				EnforceVendors: false,
+			},
+			consent:                  NoConsentsWithP1P2P3V32RestrictionRequireConsent,
+			wantConsentPurposeResult: true,
+			wantLIPurposeResult:      true,
+			wantFlexPurposeResult:    true,
+		},
+		{
+			description: "enforce purpose on, purpose consent N, legit interest N",
+			config: purposeConfig{
+				EnforcePurpose: true,
+			},
+			consent:                  NoConsentsWithP1P2P3V32RestrictionRequireConsent,
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+		{
+			description: "enforce purpose on, purpose consent Y",
+			config: purposeConfig{
+				EnforcePurpose: true,
+			},
+			consent:                  P1P2P3PurposeConsentWithP1P2P3V32RestrictionRequireConsent,
+			wantConsentPurposeResult: true,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    true,
+		},
+		{
+			description: "enforce purpose on, legit interest Y",
+			config: purposeConfig{
+				EnforcePurpose: true,
+			},
+			consent:                  P1P2P3PurposeLIWithP1P2P3V32RestrictionRequireConsent,
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+		{
+			description: "enforce purpose on, purpose consent Y, enforce vendors off but overrides treats it as on",
+			config: purposeConfig{
+				EnforcePurpose: true,
+			},
+			consent:                  P1P2P3PurposeConsentWithP1P2P3V32RestrictionRequireConsent,
+			overrides:                Overrides{enforceVendors: true},
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+		{
+			description: "enforce purpose on, purpose consent Y, vendor consent Y, enforce vendors off but overrides treats it as on",
+			config: purposeConfig{
+				EnforcePurpose: true,
+			},
+			consent:                  P1P2P3PurposeConsentAndV32VendorConsentWithP1P2P3V32RestrictionRequireConsent,
+			overrides:                Overrides{enforceVendors: true},
+			wantConsentPurposeResult: true,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    true,
+		},
+		{
+			description: "enforce vendors on, vendor consent N, vendor legit interest N",
+			config: purposeConfig{
+				EnforceVendors: true,
+			},
+			consent:                  NoConsentsWithP1P2P3V32RestrictionRequireConsent,
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+		{
+			description: "enforce vendors on, vendor consent Y",
+			config: purposeConfig{
+				EnforceVendors: true,
+			},
+			consent:                  V32VendorConsentWithP1P2P3V32RestrictionRequireConsent,
+			wantConsentPurposeResult: true,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    true,
+		},
+		{
+			description: "enforce vendors on, vendor legit interest Y",
+			config: purposeConfig{
+				EnforceVendors: true,
+			},
+			consent:                  V32VendorLIWithP1P2P3V32RestrictionRequireConsent,
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+		{
+			description: "enforce vendors on, vendor consent Y, enforce purpose off but overrides treats it as on",
+			config: purposeConfig{
+				EnforceVendors: true,
+			},
+			consent:                  V32VendorConsentWithP1P2P3V32RestrictionRequireConsent,
+			overrides:                Overrides{enforcePurpose: true},
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+		{
+			description: "enforce vendors on, purpose consent Y, vendor consent Y, enforce purpose off but overrides treats it as on",
+			config: purposeConfig{
+				EnforceVendors: true,
+			},
+			consent:                  P1P2P3PurposeConsentAndV32VendorConsentWithP1P2P3V32RestrictionRequireConsent,
+			overrides:                Overrides{enforcePurpose: true},
+			wantConsentPurposeResult: true,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    true,
+		},
+		{
+			description: "enforce purpose & vendors on, purpose consent Y, vendor consent N",
+			config: purposeConfig{
+				EnforcePurpose: true,
+				EnforceVendors: true,
+			},
+			consent:                  P1P2P3PurposeConsentWithP1P2P3V32RestrictionRequireConsent,
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+		{
+			description: "enforce purpose & vendors on, purpose consent N, vendor consent Y",
+			config: purposeConfig{
+				EnforcePurpose: true,
+				EnforceVendors: true,
+			},
+			consent:                  V32VendorConsentWithP1P2P3V32RestrictionRequireConsent,
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+		{
+			description: "enforce purpose & vendors on, purpose consent Y, vendor consent Y",
+			config: purposeConfig{
+				EnforcePurpose: true,
+				EnforceVendors: true,
+			},
+			consent:                  P1P2P3PurposeConsentAndV32VendorConsentWithP1P2P3V32RestrictionRequireConsent,
+			wantConsentPurposeResult: true,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    true,
+		},
+		{
+			description: "enforce purpose & vendors on, legit interest Y, vendor legit interest N",
+			config: purposeConfig{
+				EnforcePurpose: true,
+				EnforceVendors: true,
+			},
+			consent:                  P1P2P3PurposeLIWithP1P2P3V32RestrictionRequireConsent,
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+		{
+			description: "enforce purpose & vendors on, legit interest N, vendor legit interest Y",
+			config: purposeConfig{
+				EnforcePurpose: true,
+				EnforceVendors: true,
+			},
+			consent:                  V32VendorLIWithP1P2P3V32RestrictionRequireConsent,
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+		{
+			description: "enforce purpose & vendors on, legit interest Y, vendor legit interest Y",
+			config: purposeConfig{
+				EnforcePurpose: true,
+				EnforceVendors: true,
+			},
+			consent:                  P1P2P3PurposeLIAndV32VendorLIWithP1P2P3V32RestrictionRequireConsent,
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+		{
+			description: "enforce purpose & vendors on, bidder is a vendor exception",
+			config: purposeConfig{
+				EnforcePurpose:     true,
+				EnforceVendors:     true,
+				VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderAppnexus: {}},
+			},
+			consent:                  NoConsentsWithP1P2P3V32RestrictionRequireConsent,
+			wantConsentPurposeResult: true,
+			wantLIPurposeResult:      true,
+			wantFlexPurposeResult:    true,
+		},
+		{
+			description: "enforce purpose & vendors on, bidder is a vendor exception but overrides disallow them",
+			config: purposeConfig{
+				EnforcePurpose:     true,
+				EnforceVendors:     true,
+				VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderAppnexus: {}},
+			},
+			consent:                  NoConsentsWithP1P2P3V32RestrictionRequireConsent,
+			overrides:                Overrides{blockVendorExceptions: true},
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+	}
+
+	for _, tt := range tests {
+		// convert consent string to TCF2 object
+		parsedConsent, err := vendorconsent.ParseString(tt.consent)
+		if err != nil {
+			t.Fatalf("Failed to parse consent %s: %s\n", tt.consent, tt.description)
+		}
+		consentMeta, ok := parsedConsent.(tcf2.ConsentMetadata)
+		if !ok {
+			t.Fatalf("Failed to convert consent %s: %s\n", tt.consent, tt.description)
+		}
+
+		vendor := getVendorList(t).Vendor(appnexusID)
+		vendorInfo := VendorInfo{vendorID: appnexusID, vendor: vendor}
+		enforcer := FullEnforcement{cfg: tt.config}
+
+		enforcer.cfg.PurposeID = consentconstants.Purpose(1)
+		consentPurposeResult := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, tt.overrides)
+		assert.Equal(t, tt.wantConsentPurposeResult, consentPurposeResult, tt.description+" -- GVL consent purpose")
+
+		enforcer.cfg.PurposeID = consentconstants.Purpose(2)
+		LIPurposeresult := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, tt.overrides)
+		assert.Equal(t, tt.wantLIPurposeResult, LIPurposeresult, tt.description+" -- GVL LI purpose")
+
+		enforcer.cfg.PurposeID = consentconstants.Purpose(3)
+		flexPurposeResult := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, tt.overrides)
+		assert.Equal(t, tt.wantFlexPurposeResult, flexPurposeResult, tt.description+" -- GVL flex purpose")
+	}
+}
+
+func TestLegalBasisWithPubRestrictionRequireLI(t *testing.T) {
+	appnexusID := uint16(32)
+
+	NoConsentsWithP1P2P3V32RestrictionRequireLI := "CPfFkMAPfFkMAAAAAAENCgCAAAAAAAAAAAAAAQAAAAAAAIAAAAAAAGDAAgAgCgAQAQBwAIAIAAAA"
+	P1P2P3PurposeConsentWithP1P2P3V32RestrictionRequireLI := "CPfFkMAPfFkMAAAAAAENCgCAAOAAAAAAAAAAAQAAAAAAAIAAAAAAAGDAAgAgCgAQAQBwAIAIAAAA"
+	P1P2P3PurposeLIWithP1P2P3V32RestrictionRequireLI := "CPfFkMAPfFkMAAAAAAENCgCAAAAAAOAAAAAAAQAAAAAAAIAAAAAAAGDAAgAgCgAQAQBwAIAIAAAA"
+	V32VendorConsentWithP1P2P3V32RestrictionRequireLI := "CPfFkMAPfFkMAAAAAAENCgCAAAAAAAAAAAAAAQAAAAAEAIAAAAAAAGDAAgAgCgAQAQBwAIAIAAAA"
+	V32VendorLIWithP1P2P3V32RestrictionRequireLI := "CPfFkMAPfFkMAAAAAAENCgCAAAAAAAAAAAAAAQAAAAAAAIAAAAACAGDAAgAgCgAQAQBwAIAIAAAA"
+	P1P2P3PurposeConsentAndV32VendorConsentWithP1P2P3V32RestrictionRequireLI := "CPfFkMAPfFkMAAAAAAENCgCAAOAAAAAAAAAAAQAAAAAEAIAAAAAAAGDAAgAgCgAQAQBwAIAIAAAA"
+	P1P2P3PurposeLIAndV32VendorLIWithP1P2P3V32RestrictionRequireLI := "CPfFkMAPfFkMAAAAAAENCgCAAAAAAOAAAAAAAQAAAAAAAIAAAAACAGDAAgAgCgAQAQBwAIAIAAAA"
+
+	tests := []struct {
+		description              string
+		config                   purposeConfig
+		consent                  string
+		overrides                Overrides
+		wantConsentPurposeResult bool
+		wantLIPurposeResult      bool
+		wantFlexPurposeResult    bool
+	}{
+		{
+			description: "enforce purpose & vendors off",
+			config: purposeConfig{
+				EnforcePurpose: false,
+				EnforceVendors: false,
+			},
+			consent:                  NoConsentsWithP1P2P3V32RestrictionRequireLI,
+			wantConsentPurposeResult: true,
+			wantLIPurposeResult:      true,
+			wantFlexPurposeResult:    true,
+		},
+		{
+			description: "enforce purpose on, purpose consent N, legit interest N",
+			config: purposeConfig{
+				EnforcePurpose: true,
+			},
+			consent:                  NoConsentsWithP1P2P3V32RestrictionRequireLI,
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+		{
+			description: "enforce purpose on, purpose consent Y",
+			config: purposeConfig{
+				EnforcePurpose: true,
+			},
+			consent:                  P1P2P3PurposeConsentWithP1P2P3V32RestrictionRequireLI,
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+		{
+			description: "enforce purpose on, legit interest Y",
+			config: purposeConfig{
+				EnforcePurpose: true,
+			},
+			consent:                  P1P2P3PurposeLIWithP1P2P3V32RestrictionRequireLI,
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      true,
+			wantFlexPurposeResult:    true,
+		},
+		{
+			description: "enforce purpose on, vendor legit interest Y, enforce vendors off but overrides treats it as on",
+			config: purposeConfig{
+				EnforcePurpose: true,
+			},
+			consent:                  P1P2P3PurposeLIWithP1P2P3V32RestrictionRequireLI,
+			overrides:                Overrides{enforceVendors: true},
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+		{
+			description: "enforce purpose on, vendor legit interest Y, vendor consent Y, enforce vendors off but overrides treats it as on",
+			config: purposeConfig{
+				EnforcePurpose: true,
+			},
+			consent:                  P1P2P3PurposeLIAndV32VendorLIWithP1P2P3V32RestrictionRequireLI,
+			overrides:                Overrides{enforceVendors: true},
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      true,
+			wantFlexPurposeResult:    true,
+		},
+		{
+			description: "enforce vendors on, vendor consent N, vendor legit interest N",
+			config: purposeConfig{
+				EnforceVendors: true,
+			},
+			consent:                  NoConsentsWithP1P2P3V32RestrictionRequireLI,
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+		{
+			description: "enforce vendors on, vendor consent Y",
+			config: purposeConfig{
+				EnforceVendors: true,
+			},
+			consent:                  V32VendorConsentWithP1P2P3V32RestrictionRequireLI,
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+		{
+			description: "enforce vendors on, vendor legit interest Y",
+			config: purposeConfig{
+				EnforceVendors: true,
+			},
+			consent:                  V32VendorLIWithP1P2P3V32RestrictionRequireLI,
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      true,
+			wantFlexPurposeResult:    true,
+		},
+		{
+			description: "enforce vendors on, vendor legit interest Y, enforce purpose off but overrides treats it as on",
+			config: purposeConfig{
+				EnforceVendors: true,
+			},
+			consent:                  V32VendorLIWithP1P2P3V32RestrictionRequireLI,
+			overrides:                Overrides{enforcePurpose: true},
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+		{
+			description: "enforce vendors on, vendor legit interest Y, vendor consent Y, enforce purpose off but overrides treats it as on",
+			config: purposeConfig{
+				EnforceVendors: true,
+			},
+			consent:                  P1P2P3PurposeLIAndV32VendorLIWithP1P2P3V32RestrictionRequireLI,
+			overrides:                Overrides{enforcePurpose: true},
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      true,
+			wantFlexPurposeResult:    true,
+		},
+		{
+			description: "enforce purpose & vendors on, purpose consent Y, vendor consent N",
+			config: purposeConfig{
+				EnforcePurpose: true,
+				EnforceVendors: true,
+			},
+			consent:                  P1P2P3PurposeConsentWithP1P2P3V32RestrictionRequireLI,
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+		{
+			description: "enforce purpose & vendors on, purpose consent N, vendor consent Y",
+			config: purposeConfig{
+				EnforcePurpose: true,
+				EnforceVendors: true,
+			},
+			consent:                  V32VendorConsentWithP1P2P3V32RestrictionRequireLI,
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+		{
+			description: "enforce purpose & vendors on, purpose consent Y, vendor consent Y",
+			config: purposeConfig{
+				EnforcePurpose: true,
+				EnforceVendors: true,
+			},
+			consent:                  P1P2P3PurposeConsentAndV32VendorConsentWithP1P2P3V32RestrictionRequireLI,
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+		{
+			description: "enforce purpose & vendors on, legit interest Y, vendor legit interest N",
+			config: purposeConfig{
+				EnforcePurpose: true,
+				EnforceVendors: true,
+			},
+			consent:                  P1P2P3PurposeLIWithP1P2P3V32RestrictionRequireLI,
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+		{
+			description: "enforce purpose & vendors on, legit interest N, vendor legit interest Y",
+			config: purposeConfig{
+				EnforcePurpose: true,
+				EnforceVendors: true,
+			},
+			consent:                  V32VendorLIWithP1P2P3V32RestrictionRequireLI,
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+		{
+			description: "enforce purpose & vendors on, legit interest Y, vendor legit interest Y",
+			config: purposeConfig{
+				EnforcePurpose: true,
+				EnforceVendors: true,
+			},
+			consent:                  P1P2P3PurposeLIAndV32VendorLIWithP1P2P3V32RestrictionRequireLI,
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      true,
+			wantFlexPurposeResult:    true,
+		},
+		{
+			description: "enforce purpose & vendors on, bidder is a vendor exception",
+			config: purposeConfig{
+				EnforcePurpose:     true,
+				EnforceVendors:     true,
+				VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderAppnexus: {}},
+			},
+			consent:                  NoConsentsWithP1P2P3V32RestrictionRequireLI,
+			wantConsentPurposeResult: true,
+			wantLIPurposeResult:      true,
+			wantFlexPurposeResult:    true,
+		},
+		{
+			description: "enforce purpose & vendors on, bidder is a vendor exception but overrides disallow them",
+			config: purposeConfig{
+				EnforcePurpose:     true,
+				EnforceVendors:     true,
+				VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderAppnexus: {}},
+			},
+			consent:                  NoConsentsWithP1P2P3V32RestrictionRequireLI,
+			overrides:                Overrides{blockVendorExceptions: true},
+			wantConsentPurposeResult: false,
+			wantLIPurposeResult:      false,
+			wantFlexPurposeResult:    false,
+		},
+	}
+
+	for _, tt := range tests {
+		// convert consent string to TCF2 object
+		parsedConsent, err := vendorconsent.ParseString(tt.consent)
+		if err != nil {
+			t.Fatalf("Failed to parse consent %s: %s\n", tt.consent, tt.description)
+		}
+		consentMeta, ok := parsedConsent.(tcf2.ConsentMetadata)
+		if !ok {
+			t.Fatalf("Failed to convert consent %s: %s\n", tt.consent, tt.description)
+		}
+
+		vendor := getVendorList(t).Vendor(appnexusID)
+		vendorInfo := VendorInfo{vendorID: appnexusID, vendor: vendor}
+		enforcer := FullEnforcement{cfg: tt.config}
+
+		enforcer.cfg.PurposeID = consentconstants.Purpose(1)
+		consentPurposeResult := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, tt.overrides)
+		assert.Equal(t, tt.wantConsentPurposeResult, consentPurposeResult, tt.description+" -- GVL consent purpose")
+
+		enforcer.cfg.PurposeID = consentconstants.Purpose(2)
+		LIPurposeresult := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, tt.overrides)
+		assert.Equal(t, tt.wantLIPurposeResult, LIPurposeresult, tt.description+" -- GVL LI purpose")
+
+		enforcer.cfg.PurposeID = consentconstants.Purpose(3)
+		flexPurposeResult := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, tt.overrides)
+		assert.Equal(t, tt.wantFlexPurposeResult, flexPurposeResult, tt.description+" -- GVL flex purpose")
+	}
+}
+
+func getVendorList(t *testing.T) vendorlist.VendorList {
+	GVL := makeVendorList()
+
+	marshaledGVL, err := json.Marshal(GVL)
+	if err != nil {
+		t.Fatalf("Failed to marshal GVL")
+	}
+
+	parsedGVL, err := vendorlist2.ParseEagerly(marshaledGVL)
+	if err != nil {
+		t.Fatalf("Failed to parse vendor list data. %v", err)
+	}
+	return parsedGVL
+}
+
+func makeVendorList() vendorList {
+	return vendorList{
+		VendorListVersion: 2,
+		Vendors: map[string]*vendor{
+			"32": {
+				ID:               32,
+				Purposes:         []int{1},
+				LegIntPurposes:   []int{2},
+				FlexiblePurposes: []int{3},
+			},
+		},
+	}
+}
diff --git a/gdpr/gdpr_test.go b/gdpr/gdpr_test.go
index 7425cafe..138da5d9 100644
--- a/gdpr/gdpr_test.go
+++ b/gdpr/gdpr_test.go
@@ -4,9 +4,11 @@ import (
 	"context"
 	"testing"
 
+	"github.com/prebid/go-gdpr/consentconstants"
 	"github.com/prebid/go-gdpr/vendorlist"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/openrtb_ext"
+
 	"github.com/stretchr/testify/assert"
 )
 
@@ -45,8 +47,19 @@ func TestNewPermissions(t *testing.T) {
 			return nil, nil
 		}
 
-		perms := NewPermissions(config, &tcf2Config{}, vendorIDs, vendorListFetcher, RequestInfo{})
+		fakePurposeEnforcerBuilder := fakePurposeEnforcerBuilder{
+			purposeEnforcer: nil,
+		}.Builder
+		perms := NewPermissions(config, &tcf2Config{}, vendorIDs, vendorListFetcher, fakePurposeEnforcerBuilder, RequestInfo{})
 
 		assert.IsType(t, tt.wantType, perms, tt.description)
 	}
 }
+
+type fakePurposeEnforcerBuilder struct {
+	purposeEnforcer PurposeEnforcer
+}
+
+func (fpeb fakePurposeEnforcerBuilder) Builder(consentconstants.Purpose, openrtb_ext.BidderName) PurposeEnforcer {
+	return fpeb.purposeEnforcer
+}
diff --git a/gdpr/impl_test.go b/gdpr/impl_test.go
index 489907b7..3512a271 100644
--- a/gdpr/impl_test.go
+++ b/gdpr/impl_test.go
@@ -84,8 +84,9 @@ func TestAllowedSyncs(t *testing.T) {
 		fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{
 			1: parseVendorListDataV2(t, vendorListData),
 		}),
-		gdprSignal: SignalYes,
-		consent:    vendor2AndPurpose1Consent,
+		purposeEnforcerBuilder: NewPurposeEnforcerBuilder(&tcf2AggConfig),
+		gdprSignal:             SignalYes,
+		consent:                vendor2AndPurpose1Consent,
 	}
 
 	allowSync, err := perms.HostCookiesAllowed(context.Background())
@@ -129,8 +130,9 @@ func TestProhibitedPurposes(t *testing.T) {
 		fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{
 			1: parseVendorListDataV2(t, vendorListData),
 		}),
-		gdprSignal: SignalYes,
-		consent:    vendor2NoPurpose1Consent,
+		purposeEnforcerBuilder: NewPurposeEnforcerBuilder(&tcf2AggConfig),
+		gdprSignal:             SignalYes,
+		consent:                vendor2NoPurpose1Consent,
 	}
 
 	allowSync, err := perms.HostCookiesAllowed(context.Background())
@@ -175,8 +177,9 @@ func TestProhibitedVendors(t *testing.T) {
 		fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{
 			1: parseVendorListDataV2(t, vendorListData),
 		}),
-		gdprSignal: SignalYes,
-		consent:    purpose1NoVendorConsent,
+		purposeEnforcerBuilder: NewPurposeEnforcerBuilder(&tcf2AggConfig),
+		gdprSignal:             SignalYes,
+		consent:                purpose1NoVendorConsent,
 	}
 
 	allowSync, err := perms.HostCookiesAllowed(context.Background())
@@ -315,6 +318,7 @@ func TestAllowActivities(t *testing.T) {
 		fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{
 			1: parseVendorListDataV2(t, vendorListData),
 		}),
+		purposeEnforcerBuilder: NewPurposeEnforcerBuilder(&tcf2AggConfig),
 	}
 
 	for _, tt := range tests {
@@ -373,16 +377,16 @@ func allPurposesEnabledTCF2Config() (TCF2AggConfig tcf2Config) {
 	TCF2AggConfig = tcf2Config{
 		HostConfig: config.TCF2{
 			Enabled:         true,
-			Purpose1:        config.TCF2Purpose{EnforcePurpose: true, EnforceVendors: true},
-			Purpose2:        config.TCF2Purpose{EnforcePurpose: true, EnforceVendors: true},
-			Purpose3:        config.TCF2Purpose{EnforcePurpose: true, EnforceVendors: true},
-			Purpose4:        config.TCF2Purpose{EnforcePurpose: true, EnforceVendors: true},
-			Purpose5:        config.TCF2Purpose{EnforcePurpose: true, EnforceVendors: true},
-			Purpose6:        config.TCF2Purpose{EnforcePurpose: true, EnforceVendors: true},
-			Purpose7:        config.TCF2Purpose{EnforcePurpose: true, EnforceVendors: true},
-			Purpose8:        config.TCF2Purpose{EnforcePurpose: true, EnforceVendors: true},
-			Purpose9:        config.TCF2Purpose{EnforcePurpose: true, EnforceVendors: true},
-			Purpose10:       config.TCF2Purpose{EnforcePurpose: true, EnforceVendors: true},
+			Purpose1:        config.TCF2Purpose{EnforceAlgoID: config.TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: true},
+			Purpose2:        config.TCF2Purpose{EnforceAlgoID: config.TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: true},
+			Purpose3:        config.TCF2Purpose{EnforceAlgoID: config.TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: true},
+			Purpose4:        config.TCF2Purpose{EnforceAlgoID: config.TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: true},
+			Purpose5:        config.TCF2Purpose{EnforceAlgoID: config.TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: true},
+			Purpose6:        config.TCF2Purpose{EnforceAlgoID: config.TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: true},
+			Purpose7:        config.TCF2Purpose{EnforceAlgoID: config.TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: true},
+			Purpose8:        config.TCF2Purpose{EnforceAlgoID: config.TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: true},
+			Purpose9:        config.TCF2Purpose{EnforceAlgoID: config.TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: true},
+			Purpose10:       config.TCF2Purpose{EnforceAlgoID: config.TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: true},
 			SpecialFeature1: config.TCF2SpecialFeature{Enforce: true},
 		},
 		AccountConfig: config.AccountGDPR{
@@ -539,6 +543,7 @@ func TestAllowActivitiesGeoAndID(t *testing.T) {
 		perms.cfg = &tcf2AggConfig
 		perms.aliasGVLIDs = td.aliasGVLIDs
 		perms.consent = td.consent
+		perms.purposeEnforcerBuilder = NewPurposeEnforcerBuilder(&tcf2AggConfig)
 
 		permissions, err := perms.AuctionActivitiesAllowed(context.Background(), td.bidderCoreName, td.bidder)
 		assert.NoErrorf(t, err, "Error processing AuctionActivitiesAllowed for %s", td.description)
@@ -567,10 +572,11 @@ func TestAllowActivitiesWhitelist(t *testing.T) {
 		fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{
 			34: parseVendorListDataV2(t, vendorListData),
 		}),
-		aliasGVLIDs: map[string]uint16{},
-		consent:     fullConsentToPurposesAndVendorsTwoSixEight,
-		gdprSignal:  SignalYes,
-		publisherID: "appNexusAppID",
+		purposeEnforcerBuilder: NewPurposeEnforcerBuilder(&tcf2AggConfig),
+		aliasGVLIDs:            map[string]uint16{},
+		consent:                fullConsentToPurposesAndVendorsTwoSixEight,
+		gdprSignal:             SignalYes,
+		publisherID:            "appNexusAppID",
 	}
 
 	// Assert that an item that otherwise would not be allowed PI access, gets approved because it is found in the GDPR.NonStandardPublishers array
@@ -595,7 +601,8 @@ func TestAllowActivitiesPubRestrict(t *testing.T) {
 		fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{
 			15: parseVendorListDataV2(t, vendorListData),
 		}),
-		gdprSignal: SignalYes,
+		purposeEnforcerBuilder: NewPurposeEnforcerBuilder(&tcf2AggConfig),
+		gdprSignal:             SignalYes,
 	}
 
 	// COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA - vendors 1-10 legit interest only,
@@ -667,8 +674,9 @@ func TestAllowSync(t *testing.T) {
 		fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{
 			34: parseVendorListDataV2(t, vendorListData),
 		}),
-		gdprSignal: SignalYes,
-		consent:    fullConsentToPurposesAndVendorsTwoSixEight,
+		purposeEnforcerBuilder: NewPurposeEnforcerBuilder(&tcf2AggConfig),
+		gdprSignal:             SignalYes,
+		consent:                fullConsentToPurposesAndVendorsTwoSixEight,
 	}
 
 	allowSync, err := perms.HostCookiesAllowed(context.Background())
@@ -700,8 +708,9 @@ func TestProhibitedPurposeSync(t *testing.T) {
 		fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{
 			34: parseVendorListDataV2(t, vendorListData),
 		}),
-		gdprSignal: SignalYes,
-		consent:    fullConsentToPurposesAndVendorsTwoSixEight,
+		purposeEnforcerBuilder: NewPurposeEnforcerBuilder(&tcf2AggConfig),
+		gdprSignal:             SignalYes,
+		consent:                fullConsentToPurposesAndVendorsTwoSixEight,
 	}
 
 	allowSync, err := perms.HostCookiesAllowed(context.Background())
@@ -731,8 +740,9 @@ func TestProhibitedVendorSync(t *testing.T) {
 		fetchVendorList: listFetcher(map[uint16]vendorlist.VendorList{
 			34: parseVendorListDataV2(t, vendorListData),
 		}),
-		gdprSignal: SignalYes,
-		consent:    fullConsentToPurposesAndVendorsTwoSixEight,
+		purposeEnforcerBuilder: NewPurposeEnforcerBuilder(&tcf2AggConfig),
+		gdprSignal:             SignalYes,
+		consent:                fullConsentToPurposesAndVendorsTwoSixEight,
 	}
 
 	// COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : full consents to purposes for vendors 2, 6, 8
@@ -857,19 +867,41 @@ func TestAllowActivitiesBidRequests(t *testing.T) {
 			consent:                purpose2ConsentWithoutVendorConsent,
 			allowBidRequest:        true,
 			passGeo:                false,
-			passID:                 true,
+			passID:                 false,
 		},
 		{
-			description:            "Bid allowed - p2 disabled, user consents to p2 but not vendor, vendor consents to p2",
+			description:            "Bid allowed - p2 disabled and enforcing vendors, user consents to p2 but not vendor, vendor consents to p2",
 			purpose2EnforcePurpose: false,
 			purpose2EnforceVendors: true,
 			bidder:                 openrtb_ext.BidderPubmatic,
 			bidderCoreName:         openrtb_ext.BidderPubmatic,
 			consent:                purpose2ConsentWithoutVendorConsent,
+			allowBidRequest:        false,
+			passGeo:                false,
+			passID:                 false,
+		},
+		{
+			description:            "Bid allowed - p2 disabled not enforcing vendors, user consents to p2 but not vendor, vendor consents to p2",
+			purpose2EnforcePurpose: false,
+			purpose2EnforceVendors: false,
+			bidder:                 openrtb_ext.BidderPubmatic,
+			bidderCoreName:         openrtb_ext.BidderPubmatic,
+			consent:                purpose2ConsentWithoutVendorConsent,
 			allowBidRequest:        true,
 			passGeo:                false,
 			passID:                 false,
 		},
+		{
+			description:            "Bid allowed - p2 disabled and enforcing vendors, user consents to p2 and vendor, vendor consents to p2",
+			purpose2EnforcePurpose: false,
+			purpose2EnforceVendors: true,
+			bidder:                 openrtb_ext.BidderPubmatic,
+			bidderCoreName:         openrtb_ext.BidderPubmatic,
+			consent:                purpose2AndVendorConsent,
+			allowBidRequest:        true,
+			passGeo:                false,
+			passID:                 true,
+		},
 		{
 			description:            "Bid allowed - p2 enabled, user consents to p2 and vendor, vendor consents to p2",
 			purpose2EnforcePurpose: true,
@@ -912,7 +944,7 @@ func TestAllowActivitiesBidRequests(t *testing.T) {
 			consent:                purpose2AndVendorLI,
 			allowBidRequest:        true,
 			passGeo:                false,
-			passID:                 true,
+			passID:                 false,
 		},
 	}
 
@@ -942,6 +974,7 @@ func TestAllowActivitiesBidRequests(t *testing.T) {
 		tcf2AggConfig.HostConfig.PurposeConfigs[consentconstants.Purpose(2)] = p2Config
 		tcf2AggConfig.HostConfig.PurposeConfigs[consentconstants.Purpose(2)] = &tcf2AggConfig.HostConfig.Purpose2
 		perms.cfg = &tcf2AggConfig
+		perms.purposeEnforcerBuilder = NewPurposeEnforcerBuilder(&tcf2AggConfig)
 
 		permissions, err := perms.AuctionActivitiesAllowed(context.Background(), td.bidderCoreName, td.bidder)
 		assert.NoErrorf(t, err, "Error processing AuctionActivitiesAllowed for %s", td.description)
@@ -1055,6 +1088,7 @@ func TestAllowActivitiesVendorException(t *testing.T) {
 		tcf2AggConfig.HostConfig.PurposeConfigs[consentconstants.Purpose(2)] = &tcf2AggConfig.HostConfig.Purpose2
 		tcf2AggConfig.HostConfig.PurposeConfigs[consentconstants.Purpose(3)] = &tcf2AggConfig.HostConfig.Purpose3
 		perms.cfg = &tcf2AggConfig
+		perms.purposeEnforcerBuilder = NewPurposeEnforcerBuilder(&tcf2AggConfig)
 
 		permissions, err := perms.AuctionActivitiesAllowed(context.Background(), td.bidderCoreName, td.bidder)
 		assert.NoErrorf(t, err, "Error processing AuctionActivitiesAllowed for %s", td.description)
@@ -1116,6 +1150,7 @@ func TestBidderSyncAllowedVendorException(t *testing.T) {
 		tcf2AggConfig.HostConfig.Purpose1.VendorExceptionMap = td.p1VendorExceptionMap
 		tcf2AggConfig.HostConfig.PurposeConfigs[consentconstants.Purpose(1)] = &tcf2AggConfig.HostConfig.Purpose1
 		perms.cfg = &tcf2AggConfig
+		perms.purposeEnforcerBuilder = NewPurposeEnforcerBuilder(&tcf2AggConfig)
 
 		allowSync, err := perms.BidderSyncAllowed(context.Background(), td.bidder)
 		assert.NoErrorf(t, err, "Error processing BidderSyncAllowed for %s", td.description)
diff --git a/gdpr/purpose_config_test.go b/gdpr/purpose_config_test.go
new file mode 100644
index 00000000..e80733cc
--- /dev/null
+++ b/gdpr/purpose_config_test.go
@@ -0,0 +1,145 @@
+package gdpr
+
+import (
+	"testing"
+
+	"github.com/prebid/prebid-server/openrtb_ext"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestPurposeConfigBasicEnforcementVendor(t *testing.T) {
+	tests := []struct {
+		description      string
+		giveBasicVendors map[string]struct{}
+		giveBidder       openrtb_ext.BidderName
+		wantFound        bool
+	}{
+		{
+			description:      "vendor map is nil",
+			giveBasicVendors: nil,
+			giveBidder:       openrtb_ext.BidderAppnexus,
+			wantFound:        false,
+		},
+		{
+			description:      "vendor map is empty",
+			giveBasicVendors: map[string]struct{}{},
+			giveBidder:       openrtb_ext.BidderAppnexus,
+			wantFound:        false,
+		},
+		{
+			description: "vendor map has one bidders - bidder not found",
+			giveBasicVendors: map[string]struct{}{
+				string(openrtb_ext.BidderPubmatic): {},
+			},
+			giveBidder: openrtb_ext.BidderAppnexus,
+			wantFound:  false,
+		},
+		{
+			description: "vendor map has one bidders - bidder found",
+			giveBasicVendors: map[string]struct{}{
+				string(openrtb_ext.BidderAppnexus): {},
+			},
+			giveBidder: openrtb_ext.BidderAppnexus,
+			wantFound:  true,
+		},
+		{
+			description: "vendor map has many bidderss - bidder not found",
+			giveBasicVendors: map[string]struct{}{
+				string(openrtb_ext.BidderIx):       {},
+				string(openrtb_ext.BidderPubmatic): {},
+				string(openrtb_ext.BidderRubicon):  {},
+			},
+			giveBidder: openrtb_ext.BidderAppnexus,
+			wantFound:  false,
+		},
+		{
+			description: "vendor map has many bidderss - bidder found",
+			giveBasicVendors: map[string]struct{}{
+				string(openrtb_ext.BidderIx):       {},
+				string(openrtb_ext.BidderPubmatic): {},
+				string(openrtb_ext.BidderAppnexus): {},
+				string(openrtb_ext.BidderRubicon):  {},
+			},
+			giveBidder: openrtb_ext.BidderAppnexus,
+			wantFound:  true,
+		},
+	}
+
+	for _, tt := range tests {
+		cfg := purposeConfig{
+			BasicEnforcementVendorsMap: tt.giveBasicVendors,
+		}
+		found := cfg.basicEnforcementVendor(tt.giveBidder)
+
+		assert.Equal(t, tt.wantFound, found, tt.description)
+	}
+}
+
+func TestPurposeConfigVendorException(t *testing.T) {
+	tests := []struct {
+		description    string
+		giveExceptions map[openrtb_ext.BidderName]struct{}
+		giveBidder     openrtb_ext.BidderName
+		wantFound      bool
+	}{
+		{
+			description:    "vendor exception map is nil",
+			giveExceptions: nil,
+			giveBidder:     openrtb_ext.BidderAppnexus,
+			wantFound:      false,
+		},
+		{
+			description:    "vendor exception map is empty",
+			giveExceptions: map[openrtb_ext.BidderName]struct{}{},
+			giveBidder:     openrtb_ext.BidderAppnexus,
+			wantFound:      false,
+		},
+		{
+			description: "vendor exception map has one bidders - bidder not found",
+			giveExceptions: map[openrtb_ext.BidderName]struct{}{
+				openrtb_ext.BidderPubmatic: {},
+			},
+			giveBidder: openrtb_ext.BidderAppnexus,
+			wantFound:  false,
+		},
+		{
+			description: "vendor exception map has one bidders - bidder found",
+			giveExceptions: map[openrtb_ext.BidderName]struct{}{
+				openrtb_ext.BidderAppnexus: {},
+			},
+			giveBidder: openrtb_ext.BidderAppnexus,
+			wantFound:  true,
+		},
+		{
+			description: "vendor exception map has many bidderss - bidder not found",
+			giveExceptions: map[openrtb_ext.BidderName]struct{}{
+				openrtb_ext.BidderIx:       {},
+				openrtb_ext.BidderPubmatic: {},
+				openrtb_ext.BidderRubicon:  {},
+			},
+			giveBidder: openrtb_ext.BidderAppnexus,
+			wantFound:  false,
+		},
+		{
+			description: "vendor exception map has many bidderss - bidder found",
+			giveExceptions: map[openrtb_ext.BidderName]struct{}{
+				openrtb_ext.BidderIx:       {},
+				openrtb_ext.BidderPubmatic: {},
+				openrtb_ext.BidderAppnexus: {},
+				openrtb_ext.BidderRubicon:  {},
+			},
+			giveBidder: openrtb_ext.BidderAppnexus,
+			wantFound:  true,
+		},
+	}
+
+	for _, tt := range tests {
+		cfg := purposeConfig{
+			VendorExceptionMap: tt.giveExceptions,
+		}
+		found := cfg.vendorException(tt.giveBidder)
+
+		assert.Equal(t, tt.wantFound, found, tt.description)
+	}
+}
diff --git a/gdpr/purpose_enforcer_test.go b/gdpr/purpose_enforcer_test.go
new file mode 100644
index 00000000..ea2075d9
--- /dev/null
+++ b/gdpr/purpose_enforcer_test.go
@@ -0,0 +1,237 @@
+package gdpr
+
+import (
+	"testing"
+
+	"github.com/prebid/go-gdpr/consentconstants"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/openrtb_ext"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewPurposeEnforcerBuilder(t *testing.T) {
+	tests := []struct {
+		description        string
+		enforceAlgo        config.TCF2EnforcementAlgo
+		enforcePurpose     bool
+		enforceVendors     bool
+		basicVendorsMap    map[string]struct{}
+		vendorExceptionMap map[openrtb_ext.BidderName]struct{}
+		purpose            consentconstants.Purpose
+		bidder             openrtb_ext.BidderName
+		wantType           PurposeEnforcer
+	}{
+		{
+			description:        "purpose 1 full algo -- full enforcer returned",
+			enforceAlgo:        config.TCF2FullEnforcement,
+			enforcePurpose:     true,
+			enforceVendors:     true,
+			basicVendorsMap:    map[string]struct{}{},
+			vendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
+			purpose:            consentconstants.Purpose(1),
+			bidder:             openrtb_ext.BidderAppnexus,
+			wantType:           &FullEnforcement{},
+		},
+		{
+			description:        "purpose 1 full algo, basic enforcement vendor -- full enforcer returned",
+			enforceAlgo:        config.TCF2FullEnforcement,
+			enforcePurpose:     true,
+			enforceVendors:     true,
+			basicVendorsMap:    map[string]struct{}{string(openrtb_ext.BidderAppnexus): {}},
+			vendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
+			purpose:            consentconstants.Purpose(1),
+			bidder:             openrtb_ext.BidderAppnexus,
+			wantType:           &FullEnforcement{},
+		},
+		{
+			description:        "purpose 1 basic algo -- basic enforcer returned",
+			enforceAlgo:        config.TCF2BasicEnforcement,
+			enforcePurpose:     true,
+			enforceVendors:     true,
+			basicVendorsMap:    map[string]struct{}{},
+			vendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
+			purpose:            consentconstants.Purpose(1),
+			bidder:             openrtb_ext.BidderAppnexus,
+			wantType:           &BasicEnforcement{},
+		},
+		{
+			description:        "purpose 2 full algo -- full enforcer returned",
+			enforceAlgo:        config.TCF2FullEnforcement,
+			enforcePurpose:     true,
+			enforceVendors:     true,
+			basicVendorsMap:    map[string]struct{}{},
+			vendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
+			purpose:            consentconstants.Purpose(2),
+			bidder:             openrtb_ext.BidderAppnexus,
+			wantType:           &FullEnforcement{},
+		},
+		{
+			description:        "purpose 2 full algo, basic enforcement vendor -- basic enforcer returned",
+			enforceAlgo:        config.TCF2FullEnforcement,
+			enforcePurpose:     true,
+			enforceVendors:     true,
+			basicVendorsMap:    map[string]struct{}{string(openrtb_ext.BidderAppnexus): {}},
+			vendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
+			purpose:            consentconstants.Purpose(2),
+			bidder:             openrtb_ext.BidderAppnexus,
+			wantType:           &BasicEnforcement{},
+		},
+		{
+			description:        "purpose 2 basic algo -- basic enforcer returned",
+			enforceAlgo:        config.TCF2BasicEnforcement,
+			enforcePurpose:     true,
+			enforceVendors:     true,
+			basicVendorsMap:    map[string]struct{}{},
+			vendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
+			purpose:            consentconstants.Purpose(2),
+			bidder:             openrtb_ext.BidderAppnexus,
+			wantType:           &BasicEnforcement{},
+		},
+	}
+
+	for _, tt := range tests {
+		cfg := fakeTCF2ConfigReader{
+			enforceAlgo:                tt.enforceAlgo,
+			enforcePurpose:             tt.enforcePurpose,
+			enforceVendors:             tt.enforceVendors,
+			basicEnforcementVendorsMap: tt.basicVendorsMap,
+			vendorExceptionMap:         tt.vendorExceptionMap,
+		}
+
+		builder := NewPurposeEnforcerBuilder(&cfg)
+
+		enforcer1 := builder(tt.purpose, tt.bidder)
+		enforcer2 := builder(tt.purpose, tt.bidder)
+
+		// assert that enforcer1 and enforcer2 are same objects; enforcer2 pulled from cache
+		assert.Same(t, enforcer1, enforcer2, tt.description)
+		assert.IsType(t, tt.wantType, enforcer1, tt.description)
+
+		// assert enforcer 1 config values are properly set
+		switch enforcer1.(type) {
+		case *FullEnforcement:
+			{
+				fullEnforcer := enforcer1.(*FullEnforcement)
+				assert.Equal(t, fullEnforcer.cfg.PurposeID, tt.purpose, tt.description)
+				assert.Equal(t, fullEnforcer.cfg.EnforceAlgo, tt.enforceAlgo, tt.description)
+				assert.Equal(t, fullEnforcer.cfg.EnforcePurpose, tt.enforcePurpose, tt.description)
+				assert.Equal(t, fullEnforcer.cfg.EnforceVendors, tt.enforceVendors, tt.description)
+				assert.Equal(t, fullEnforcer.cfg.BasicEnforcementVendorsMap, tt.basicVendorsMap, tt.description)
+				assert.Equal(t, fullEnforcer.cfg.VendorExceptionMap, tt.vendorExceptionMap, tt.description)
+			}
+		case PurposeEnforcer:
+			{
+				basicEnforcer := enforcer1.(*BasicEnforcement)
+				assert.Equal(t, basicEnforcer.cfg.PurposeID, tt.purpose, tt.description)
+				assert.Equal(t, basicEnforcer.cfg.EnforceAlgo, tt.enforceAlgo, tt.description)
+				assert.Equal(t, basicEnforcer.cfg.EnforcePurpose, tt.enforcePurpose, tt.description)
+				assert.Equal(t, basicEnforcer.cfg.EnforceVendors, tt.enforceVendors, tt.description)
+				assert.Equal(t, basicEnforcer.cfg.BasicEnforcementVendorsMap, tt.basicVendorsMap, tt.description)
+				assert.Equal(t, basicEnforcer.cfg.VendorExceptionMap, tt.vendorExceptionMap, tt.description)
+			}
+		}
+	}
+}
+
+func TestNewPurposeEnforcerBuilderCaching(t *testing.T) {
+
+	bidder1 := openrtb_ext.BidderAppnexus
+	bidder1Enforcers := make([]PurposeEnforcer, 11)
+	bidder2 := openrtb_ext.BidderIx
+	bidder2Enforcers := make([]PurposeEnforcer, 11)
+	bidder3 := openrtb_ext.BidderPubmatic
+	bidder3Enforcers := make([]PurposeEnforcer, 11)
+	bidder4 := openrtb_ext.BidderRubicon
+	bidder4Enforcers := make([]PurposeEnforcer, 11)
+
+	cfg := fakeTCF2ConfigReader{
+		enforceAlgo: config.TCF2FullEnforcement,
+		basicEnforcementVendorsMap: map[string]struct{}{
+			string(bidder3): {},
+			string(bidder4): {},
+		},
+	}
+	builder := NewPurposeEnforcerBuilder(&cfg)
+
+	for i := 1; i <= 10; i++ {
+		bidder1Enforcers[i] = builder(consentconstants.Purpose(i), bidder1)
+		bidder2Enforcers[i] = builder(consentconstants.Purpose(i), bidder2)
+		bidder3Enforcers[i] = builder(consentconstants.Purpose(i), bidder3)
+		bidder4Enforcers[i] = builder(consentconstants.Purpose(i), bidder4)
+	}
+
+	for i := 1; i <= 10; i++ {
+		if i == 1 {
+			assert.IsType(t, bidder1Enforcers[i], &FullEnforcement{}, "purpose 1 bidder 1 enforcer is full")
+			assert.IsType(t, bidder3Enforcers[i], &FullEnforcement{}, "purpose 1 bidder 3 enforcer is full")
+
+			// verify cross-bidder enforcer objects for a given purpose are the same
+			assert.Same(t, bidder1Enforcers[i], bidder2Enforcers[i], "purpose 1 compare bidder 1 & 2 enforcers")
+			assert.Same(t, bidder2Enforcers[i], bidder3Enforcers[i], "purpose 1 compare bidder 2 & 3 enforcers")
+			assert.Same(t, bidder3Enforcers[i], bidder4Enforcers[i], "purpose 1 compare bidder 3 & 4 enforcers")
+
+			// verify cross-purpose enforcer objects are different
+			assert.Equal(t, bidder1Enforcers[i].(*FullEnforcement).cfg.PurposeID, consentconstants.Purpose(i), "purpose 1 bidder 1 enforcer purpose check")
+			assert.Equal(t, bidder2Enforcers[i].(*FullEnforcement).cfg.PurposeID, consentconstants.Purpose(i), "purpose 1 bidder 2 enforcer purpose check")
+			assert.Equal(t, bidder3Enforcers[i].(*FullEnforcement).cfg.PurposeID, consentconstants.Purpose(i), "purpose 1 bidder 3 enforcer purpose check")
+			assert.Equal(t, bidder4Enforcers[i].(*FullEnforcement).cfg.PurposeID, consentconstants.Purpose(i), "purpose 1 bidder 4 enforcer purpose check")
+		} else {
+			assert.IsType(t, bidder1Enforcers[i], &FullEnforcement{}, "purpose %d bidder 1 enforcer is full", i)
+			assert.IsType(t, bidder3Enforcers[i], &BasicEnforcement{}, "purpose %d bidder 3 enforcer is basic", i)
+
+			// verify some cross-bidder enforcer objects for a given purpose are the same and some are different
+			assert.Same(t, bidder1Enforcers[i], bidder2Enforcers[i], "purpose %d compare bidder 1 & 2 enforcers", i)
+			assert.NotSame(t, bidder2Enforcers[i], bidder3Enforcers[i], "purpose %d compare bidder 2 & 3 enforcers", i)
+			assert.Same(t, bidder3Enforcers[i], bidder4Enforcers[i], "purpose %d compare bidder 3 & 4 enforcers", i)
+
+			// verify cross-purpose enforcer objects are different
+			assert.Equal(t, bidder1Enforcers[i].(*FullEnforcement).cfg.PurposeID, consentconstants.Purpose(i), "purpose %d bidder 1 enforcer purpose check", i)
+			assert.Equal(t, bidder2Enforcers[i].(*FullEnforcement).cfg.PurposeID, consentconstants.Purpose(i), "purpose %d bidder 2 enforcer purpose check", i)
+			assert.Equal(t, bidder3Enforcers[i].(*BasicEnforcement).cfg.PurposeID, consentconstants.Purpose(i), "purpose %d bidder 3 enforcer purpose check", i)
+			assert.Equal(t, bidder4Enforcers[i].(*BasicEnforcement).cfg.PurposeID, consentconstants.Purpose(i), "purpose %d bidder 4 enforcer purpose check", i)
+		}
+	}
+}
+
+type fakeTCF2ConfigReader struct {
+	enforceAlgo                config.TCF2EnforcementAlgo
+	enforcePurpose             bool
+	enforceVendors             bool
+	vendorExceptionMap         map[openrtb_ext.BidderName]struct{}
+	basicEnforcementVendorsMap map[string]struct{}
+}
+
+func (fcr *fakeTCF2ConfigReader) BasicEnforcementVendors() map[string]struct{} {
+	return fcr.basicEnforcementVendorsMap
+}
+func (fcr *fakeTCF2ConfigReader) FeatureOneEnforced() bool {
+	return false
+}
+func (fcr *fakeTCF2ConfigReader) FeatureOneVendorException(openrtb_ext.BidderName) bool {
+	return false
+}
+func (fcr *fakeTCF2ConfigReader) ChannelEnabled(config.ChannelType) bool {
+	return false
+}
+func (fcr *fakeTCF2ConfigReader) IsEnabled() bool {
+	return false
+}
+func (fcr *fakeTCF2ConfigReader) PurposeEnforced(purpose consentconstants.Purpose) bool {
+	return fcr.enforcePurpose
+}
+func (fcr *fakeTCF2ConfigReader) PurposeEnforcementAlgo(purpose consentconstants.Purpose) config.TCF2EnforcementAlgo {
+	return fcr.enforceAlgo
+}
+func (fcr *fakeTCF2ConfigReader) PurposeEnforcingVendors(purpose consentconstants.Purpose) bool {
+	return fcr.enforceVendors
+}
+func (fcr *fakeTCF2ConfigReader) PurposeVendorExceptions(purpose consentconstants.Purpose) map[openrtb_ext.BidderName]struct{} {
+	return fcr.vendorExceptionMap
+}
+func (fcr *fakeTCF2ConfigReader) PurposeOneTreatmentEnabled() bool {
+	return false
+}
+func (fcr *fakeTCF2ConfigReader) PurposeOneTreatmentAccessAllowed() bool {
+	return false
+}