Repository: prebid/prebid-server
Pull request #2449 (2022-11-07 17:55:03)

IX Adapter: Pass version information in Request.Ext field
This update to the IX Adapter is to pass Prebid Server Version information and PBJS version information as part of a new "IXDiag" field. This field will be used to pass any diagnostic information as needed.

Changes include changese to ix.go and ix_test.go.


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/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/ix/ix.go b/adapters/ix/ix.go
index e0e306bd..217e28e9 100644
--- a/adapters/ix/ix.go
+++ b/adapters/ix/ix.go
@@ -11,6 +11,7 @@ import (
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/prebid/prebid-server/version"
 
 	"github.com/prebid/openrtb/v17/native1"
 	native1response "github.com/prebid/openrtb/v17/native1/response"
@@ -22,6 +23,17 @@ type IxAdapter struct {
 	maxRequests int
 }
 
+type ExtRequest struct {
+	Prebid *openrtb_ext.ExtRequestPrebid `json:"prebid"`
+	SChain *openrtb2.SupplyChain         `json:"schain,omitempty"`
+	IxDiag *IxDiag                       `json:"ixdiag,omitempty"`
+}
+
+type IxDiag struct {
+	PbsV  string `json:"pbsv,omitempty"`
+	PbjsV string `json:"pbjsv,omitempty"`
+}
+
 func (a *IxAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
 	nImp := len(request.Imp)
 	if nImp > a.maxRequests {
@@ -29,13 +41,18 @@ func (a *IxAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters
 		nImp = a.maxRequests
 	}
 
+	errs := make([]error, 0)
+
+	if err := BuildIxDiag(request); err != nil {
+		errs = append(errs, err)
+	}
+
 	// Multi-size banner imps are split into single-size requests.
 	// The first size imp requests are added to the first slice.
 	// Additional size requests are added to the second slice and are merged with the first at the end.
 	// Preallocate the max possible size to avoid reallocating arrays.
 	requests := make([]*adapters.RequestData, 0, a.maxRequests)
 	multiSizeRequests := make([]*adapters.RequestData, 0, a.maxRequests-nImp)
-	errs := make([]error, 0, 1)
 
 	headers := http.Header{
 		"Content-Type": {"application/json;charset=utf-8"},
@@ -309,3 +326,35 @@ func marshalJsonWithoutUnicode(v interface{}) (string, error) {
 	// https://pkg.go.dev/encoding/json#Encoder.Encode
 	return strings.TrimSuffix(sb.String(), "\n"), nil
 }
+
+func BuildIxDiag(request *openrtb2.BidRequest) error {
+	extRequest := &ExtRequest{}
+	if request.Ext != nil {
+		if err := json.Unmarshal(request.Ext, &extRequest); err != nil {
+			return err
+		}
+	}
+	ixdiag := &IxDiag{}
+
+	if extRequest.Prebid != nil {
+		ixdiag.PbjsV = extRequest.Prebid.Channel.Version
+	}
+
+	// Slice commit hash out of version
+	if strings.Contains(version.Ver, "-") {
+		ixdiag.PbsV = version.Ver[:strings.Index(version.Ver, "-")]
+	} else if version.Ver != "" {
+		ixdiag.PbsV = version.Ver
+	}
+
+	// Only set request.ext if ixDiag is not empty
+	if *ixdiag != (IxDiag{}) {
+		extRequest.IxDiag = ixdiag
+		requestExtJson, err := json.Marshal(extRequest)
+		if err != nil {
+			return err
+		}
+		request.Ext = requestExtJson
+	}
+	return 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 13feb09b..26abc217 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -52,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"
@@ -210,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 5d14ade0..e4c68fe6 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -132,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"
@@ -298,6 +299,7 @@ func CoreBidderNames() []BidderName {
 		BidderBmtm,
 		BidderBoldwin,
 		BidderBrightroll,
+		BidderCcx,
 		BidderCoinzilla,
 		BidderColossus,
 		BidderCompass,
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/openrtb_ext/request_wrapper.go b/openrtb_ext/request_wrapper.go
index b5e7ca41..63d16185 100644
--- a/openrtb_ext/request_wrapper.go
+++ b/openrtb_ext/request_wrapper.go
@@ -44,8 +44,15 @@ type RequestWrapper struct {
 
 const (
 	jsonEmptyObjectLength               = 2
-	ConsentedProvidersSettingsStringKey = "ConsentedProvidersSettings"
-	ConsentedProvidersSettingsListKey   = "consented_providers_settings"
+	consentedProvidersSettingsStringKey = "ConsentedProvidersSettings"
+	consentedProvidersSettingsListKey   = "consented_providers_settings"
+	consentKey                          = "consent"
+	ampKey                              = "amp"
+	eidsKey                             = "eids"
+	gdprKey                             = "gdpr"
+	prebidKey                           = "prebid"
+	schainKey                           = "schain"
+	us_privacyKey                       = "us_privacy"
 )
 
 // LenImp returns the number of impressions without causing the creation of ImpWrapper objects.
@@ -377,14 +384,14 @@ func (ue *UserExt) unmarshal(extJson json.RawMessage) error {
 		return err
 	}
 
-	consentJson, hasConsent := ue.ext["consent"]
+	consentJson, hasConsent := ue.ext[consentKey]
 	if hasConsent {
 		if err := json.Unmarshal(consentJson, &ue.consent); err != nil {
 			return err
 		}
 	}
 
-	prebidJson, hasPrebid := ue.ext["prebid"]
+	prebidJson, hasPrebid := ue.ext[prebidKey]
 	if hasPrebid {
 		ue.prebid = &ExtUserPrebid{}
 		if err := json.Unmarshal(prebidJson, ue.prebid); err != nil {
@@ -392,7 +399,7 @@ func (ue *UserExt) unmarshal(extJson json.RawMessage) error {
 		}
 	}
 
-	eidsJson, hasEids := ue.ext["eids"]
+	eidsJson, hasEids := ue.ext[eidsKey]
 	if hasEids {
 		ue.eids = &[]openrtb2.EID{}
 		if err := json.Unmarshal(eidsJson, ue.eids); err != nil {
@@ -400,14 +407,14 @@ func (ue *UserExt) unmarshal(extJson json.RawMessage) error {
 		}
 	}
 
-	if consentedProviderSettingsJson, hasCPSettings := ue.ext[ConsentedProvidersSettingsStringKey]; hasCPSettings {
+	if consentedProviderSettingsJson, hasCPSettings := ue.ext[consentedProvidersSettingsStringKey]; hasCPSettings {
 		ue.consentedProvidersSettingsIn = &ConsentedProvidersSettingsIn{}
 		if err := json.Unmarshal(consentedProviderSettingsJson, ue.consentedProvidersSettingsIn); err != nil {
 			return err
 		}
 	}
 
-	if consentedProviderSettingsJson, hasCPSettings := ue.ext[ConsentedProvidersSettingsListKey]; hasCPSettings {
+	if consentedProviderSettingsJson, hasCPSettings := ue.ext[consentedProvidersSettingsListKey]; hasCPSettings {
 		ue.consentedProvidersSettingsOut = &ConsentedProvidersSettingsOut{}
 		if err := json.Unmarshal(consentedProviderSettingsJson, ue.consentedProvidersSettingsOut); err != nil {
 			return err
@@ -424,9 +431,9 @@ func (ue *UserExt) marshal() (json.RawMessage, error) {
 			if err != nil {
 				return nil, err
 			}
-			ue.ext["consent"] = json.RawMessage(consentJson)
+			ue.ext[consentKey] = json.RawMessage(consentJson)
 		} else {
-			delete(ue.ext, "consent")
+			delete(ue.ext, consentKey)
 		}
 		ue.consentDirty = false
 	}
@@ -438,12 +445,12 @@ func (ue *UserExt) marshal() (json.RawMessage, error) {
 				return nil, err
 			}
 			if len(prebidJson) > jsonEmptyObjectLength {
-				ue.ext["prebid"] = json.RawMessage(prebidJson)
+				ue.ext[prebidKey] = json.RawMessage(prebidJson)
 			} else {
-				delete(ue.ext, "prebid")
+				delete(ue.ext, prebidKey)
 			}
 		} else {
-			delete(ue.ext, "prebid")
+			delete(ue.ext, prebidKey)
 		}
 		ue.prebidDirty = false
 	}
@@ -455,12 +462,12 @@ func (ue *UserExt) marshal() (json.RawMessage, error) {
 				return nil, err
 			}
 			if len(cpSettingsJson) > jsonEmptyObjectLength {
-				ue.ext[ConsentedProvidersSettingsStringKey] = json.RawMessage(cpSettingsJson)
+				ue.ext[consentedProvidersSettingsStringKey] = json.RawMessage(cpSettingsJson)
 			} else {
-				delete(ue.ext, ConsentedProvidersSettingsStringKey)
+				delete(ue.ext, consentedProvidersSettingsStringKey)
 			}
 		} else {
-			delete(ue.ext, ConsentedProvidersSettingsStringKey)
+			delete(ue.ext, consentedProvidersSettingsStringKey)
 		}
 		ue.consentedProvidersSettingsInDirty = false
 	}
@@ -472,12 +479,12 @@ func (ue *UserExt) marshal() (json.RawMessage, error) {
 				return nil, err
 			}
 			if len(cpSettingsJson) > jsonEmptyObjectLength {
-				ue.ext[ConsentedProvidersSettingsListKey] = json.RawMessage(cpSettingsJson)
+				ue.ext[consentedProvidersSettingsListKey] = json.RawMessage(cpSettingsJson)
 			} else {
-				delete(ue.ext, ConsentedProvidersSettingsListKey)
+				delete(ue.ext, consentedProvidersSettingsListKey)
 			}
 		} else {
-			delete(ue.ext, ConsentedProvidersSettingsListKey)
+			delete(ue.ext, consentedProvidersSettingsListKey)
 		}
 		ue.consentedProvidersSettingsOutDirty = false
 	}
@@ -488,9 +495,9 @@ func (ue *UserExt) marshal() (json.RawMessage, error) {
 			if err != nil {
 				return nil, err
 			}
-			ue.ext["eids"] = json.RawMessage(eidsJson)
+			ue.ext[eidsKey] = json.RawMessage(eidsJson)
 		} else {
-			delete(ue.ext, "eids")
+			delete(ue.ext, eidsKey)
 		}
 		ue.eidsDirty = false
 	}
@@ -626,7 +633,7 @@ func (re *RequestExt) unmarshal(extJson json.RawMessage) error {
 		return err
 	}
 
-	prebidJson, hasPrebid := re.ext["prebid"]
+	prebidJson, hasPrebid := re.ext[prebidKey]
 	if hasPrebid {
 		re.prebid = &ExtRequestPrebid{}
 		if err := json.Unmarshal(prebidJson, re.prebid); err != nil {
@@ -634,7 +641,7 @@ func (re *RequestExt) unmarshal(extJson json.RawMessage) error {
 		}
 	}
 
-	schainJson, hasSChain := re.ext["schain"]
+	schainJson, hasSChain := re.ext[schainKey]
 	if hasSChain {
 		re.schain = &openrtb2.SupplyChain{}
 		if err := json.Unmarshal(schainJson, re.schain); err != nil {
@@ -653,12 +660,12 @@ func (re *RequestExt) marshal() (json.RawMessage, error) {
 				return nil, err
 			}
 			if len(prebidJson) > jsonEmptyObjectLength {
-				re.ext["prebid"] = json.RawMessage(prebidJson)
+				re.ext[prebidKey] = json.RawMessage(prebidJson)
 			} else {
-				delete(re.ext, "prebid")
+				delete(re.ext, prebidKey)
 			}
 		} else {
-			delete(re.ext, "prebid")
+			delete(re.ext, prebidKey)
 		}
 		re.prebidDirty = false
 	}
@@ -670,12 +677,12 @@ func (re *RequestExt) marshal() (json.RawMessage, error) {
 				return nil, err
 			}
 			if len(schainJson) > jsonEmptyObjectLength {
-				re.ext["schain"] = json.RawMessage(schainJson)
+				re.ext[schainKey] = json.RawMessage(schainJson)
 			} else {
-				delete(re.ext, "schain")
+				delete(re.ext, schainKey)
 			}
 		} else {
-			delete(re.ext, "schain")
+			delete(re.ext, schainKey)
 		}
 		re.schainDirty = false
 	}
@@ -763,7 +770,7 @@ func (de *DeviceExt) unmarshal(extJson json.RawMessage) error {
 		return err
 	}
 
-	prebidJson, hasPrebid := de.ext["prebid"]
+	prebidJson, hasPrebid := de.ext[prebidKey]
 	if hasPrebid {
 		de.prebid = &ExtDevicePrebid{}
 		if err := json.Unmarshal(prebidJson, de.prebid); err != nil {
@@ -782,12 +789,12 @@ func (de *DeviceExt) marshal() (json.RawMessage, error) {
 				return nil, err
 			}
 			if len(prebidJson) > jsonEmptyObjectLength {
-				de.ext["prebid"] = json.RawMessage(prebidJson)
+				de.ext[prebidKey] = json.RawMessage(prebidJson)
 			} else {
-				delete(de.ext, "prebid")
+				delete(de.ext, prebidKey)
 			}
 		} else {
-			delete(de.ext, "prebid")
+			delete(de.ext, prebidKey)
 		}
 		de.prebidDirty = false
 	}
@@ -855,7 +862,7 @@ func (ae *AppExt) unmarshal(extJson json.RawMessage) error {
 		return err
 	}
 
-	prebidJson, hasPrebid := ae.ext["prebid"]
+	prebidJson, hasPrebid := ae.ext[prebidKey]
 	if hasPrebid {
 		ae.prebid = &ExtAppPrebid{}
 		if err := json.Unmarshal(prebidJson, ae.prebid); err != nil {
@@ -874,12 +881,12 @@ func (ae *AppExt) marshal() (json.RawMessage, error) {
 				return nil, err
 			}
 			if len(prebidJson) > jsonEmptyObjectLength {
-				ae.ext["prebid"] = json.RawMessage(prebidJson)
+				ae.ext[prebidKey] = json.RawMessage(prebidJson)
 			} else {
-				delete(ae.ext, "prebid")
+				delete(ae.ext, prebidKey)
 			}
 		} else {
-			delete(ae.ext, "prebid")
+			delete(ae.ext, prebidKey)
 		}
 		ae.prebidDirty = false
 	}
@@ -949,14 +956,14 @@ func (re *RegExt) unmarshal(extJson json.RawMessage) error {
 		return err
 	}
 
-	gdprJson, hasGDPR := re.ext["gdpr"]
+	gdprJson, hasGDPR := re.ext[gdprKey]
 	if hasGDPR {
 		if err := json.Unmarshal(gdprJson, &re.gdpr); err != nil {
 			return errors.New("gdpr must be an integer")
 		}
 	}
 
-	uspJson, hasUsp := re.ext["us_privacy"]
+	uspJson, hasUsp := re.ext[us_privacyKey]
 	if hasUsp {
 		if err := json.Unmarshal(uspJson, &re.usPrivacy); err != nil {
 			return err
@@ -973,9 +980,9 @@ func (re *RegExt) marshal() (json.RawMessage, error) {
 			if err != nil {
 				return nil, err
 			}
-			re.ext["gdpr"] = rawjson
+			re.ext[gdprKey] = rawjson
 		} else {
-			delete(re.ext, "gdpr")
+			delete(re.ext, gdprKey)
 		}
 		re.gdprDirty = false
 	}
@@ -986,9 +993,9 @@ func (re *RegExt) marshal() (json.RawMessage, error) {
 			if err != nil {
 				return nil, err
 			}
-			re.ext["us_privacy"] = rawjson
+			re.ext[us_privacyKey] = rawjson
 		} else {
-			delete(re.ext, "us_privacy")
+			delete(re.ext, us_privacyKey)
 		}
 		re.usPrivacyDirty = false
 	}
@@ -1063,7 +1070,7 @@ func (se *SiteExt) unmarshal(extJson json.RawMessage) error {
 		return err
 	}
 
-	ampJson, hasAmp := se.ext["amp"]
+	ampJson, hasAmp := se.ext[ampKey]
 	if hasAmp {
 		if err := json.Unmarshal(ampJson, &se.amp); err != nil {
 			return errors.New(`request.site.ext.amp must be either 1, 0, or undefined`)
@@ -1080,9 +1087,9 @@ func (se *SiteExt) marshal() (json.RawMessage, error) {
 			if err != nil {
 				return nil, err
 			}
-			se.ext["amp"] = json.RawMessage(ampJson)
+			se.ext[ampKey] = json.RawMessage(ampJson)
 		} else {
-			delete(se.ext, "amp")
+			delete(se.ext, ampKey)
 		}
 		se.ampDirty = false
 	}
@@ -1146,7 +1153,7 @@ func (se *SourceExt) unmarshal(extJson json.RawMessage) error {
 		return err
 	}
 
-	schainJson, hasSChain := se.ext["schain"]
+	schainJson, hasSChain := se.ext[schainKey]
 	if hasSChain {
 		if err := json.Unmarshal(schainJson, &se.schain); err != nil {
 			return err
@@ -1164,12 +1171,12 @@ func (se *SourceExt) marshal() (json.RawMessage, error) {
 				return nil, err
 			}
 			if len(schainJson) > jsonEmptyObjectLength {
-				se.ext["schain"] = json.RawMessage(schainJson)
+				se.ext[schainKey] = json.RawMessage(schainJson)
 			} else {
-				delete(se.ext, "schain")
+				delete(se.ext, schainKey)
 			}
 		} else {
-			delete(se.ext, "schain")
+			delete(se.ext, schainKey)
 		}
 		se.schainDirty = false
 	}
@@ -1283,7 +1290,7 @@ func (e *ImpExt) unmarshal(extJson json.RawMessage) error {
 		return err
 	}
 
-	prebidJson, hasPrebid := e.ext["prebid"]
+	prebidJson, hasPrebid := e.ext[prebidKey]
 	if hasPrebid {
 		e.prebid = &ExtImpPrebid{}
 		if err := json.Unmarshal(prebidJson, e.prebid); err != nil {
@@ -1302,12 +1309,12 @@ func (e *ImpExt) marshal() (json.RawMessage, error) {
 				return nil, err
 			}
 			if len(prebidJson) > jsonEmptyObjectLength {
-				e.ext["prebid"] = json.RawMessage(prebidJson)
+				e.ext[prebidKey] = json.RawMessage(prebidJson)
 			} else {
-				delete(e.ext, "prebid")
+				delete(e.ext, prebidKey)
 			}
 		} else {
-			delete(e.ext, "prebid")
+			delete(e.ext, prebidKey)
 		}
 		e.prebidDirty = false
 	}
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/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/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/ix/ix_test.go b/adapters/ix/ix_test.go
index 1aa4637e..acc8b132 100644
--- a/adapters/ix/ix_test.go
+++ b/adapters/ix/ix_test.go
@@ -8,6 +8,8 @@ import (
 	"github.com/prebid/prebid-server/adapters/adapterstest"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/prebid/prebid-server/version"
+	"github.com/stretchr/testify/assert"
 
 	"github.com/prebid/openrtb/v17/adcom1"
 	"github.com/prebid/openrtb/v17/openrtb2"
@@ -101,3 +103,112 @@ func TestIxMakeBidsWithCategoryDuration(t *testing.T) {
 		t.Errorf("should not have any errors, errors=%v", errors)
 	}
 }
+
+func TestBuildIxDiag(t *testing.T) {
+	testCases := []struct {
+		description     string
+		request         *openrtb2.BidRequest
+		expectedRequest *openrtb2.BidRequest
+		expectError     bool
+		pbsVersion      string
+	}{
+		{
+			description: "Base Test",
+			request: &openrtb2.BidRequest{
+				ID:  "1",
+				Ext: json.RawMessage(`{"prebid":{"channel":{"name":"web","version":"7.20"}}}`),
+			},
+			expectedRequest: &openrtb2.BidRequest{
+				ID:  "1",
+				Ext: json.RawMessage(`{"prebid":{"channel":{"name":"web","version":"7.20"}},"ixdiag":{"pbsv":"1.880","pbjsv":"7.20"}}`),
+			},
+			expectError: false,
+			pbsVersion:  "1.880-abcdef",
+		},
+		{
+			description: "No Ext",
+			request: &openrtb2.BidRequest{
+				ID: "1",
+			},
+			expectedRequest: &openrtb2.BidRequest{
+				ID:  "1",
+				Ext: json.RawMessage(`{"prebid":null,"ixdiag":{"pbsv":"1.880"}}`),
+			},
+			expectError: false,
+			pbsVersion:  "1.880-abcdef",
+		},
+		{
+			description: "PBS Version Two Hypens",
+			request: &openrtb2.BidRequest{
+				ID: "1",
+			},
+			expectedRequest: &openrtb2.BidRequest{
+				ID:  "1",
+				Ext: json.RawMessage(`{"prebid":null,"ixdiag":{"pbsv":"0.23.1"}}`),
+			},
+			expectError: false,
+			pbsVersion:  "0.23.1-3-g4ee257d8",
+		},
+		{
+			description: "PBS Version no Hyphen",
+			request: &openrtb2.BidRequest{
+				ID:  "1",
+				Ext: json.RawMessage(`{"prebid":{"channel":{"name":"web","version":"7.20"}}}`),
+			},
+			expectedRequest: &openrtb2.BidRequest{
+				ID:  "1",
+				Ext: json.RawMessage(`{"prebid":{"channel":{"name":"web","version":"7.20"}},"ixdiag":{"pbsv":"1.880","pbjsv":"7.20"}}`),
+			},
+			expectError: false,
+			pbsVersion:  "1.880",
+		},
+		{
+			description: "PBS Version empty string",
+			request: &openrtb2.BidRequest{
+				ID:  "1",
+				Ext: json.RawMessage(`{"prebid":{"channel":{"name":"web","version":"7.20"}}}`),
+			},
+			expectedRequest: &openrtb2.BidRequest{
+				ID:  "1",
+				Ext: json.RawMessage(`{"prebid":{"channel":{"name":"web","version":"7.20"}},"ixdiag":{"pbjsv":"7.20"}}`),
+			},
+			expectError: false,
+			pbsVersion:  "",
+		},
+		{
+			description: "Error Test",
+			request: &openrtb2.BidRequest{
+				ID:  "1",
+				Ext: json.RawMessage(`{"prebid":"channel":{"name":"web","version":"7.20"}}}`),
+			},
+			expectedRequest: &openrtb2.BidRequest{
+				ID:  "1",
+				Ext: nil,
+			},
+			expectError: true,
+			pbsVersion:  "1.880-abcdef",
+		},
+	}
+
+	for _, test := range testCases {
+		version.Ver = test.pbsVersion
+		err := BuildIxDiag(test.request)
+		if test.expectError {
+			assert.NotNil(t, err)
+		} else {
+			assert.Equal(t, test.request, test.expectedRequest)
+			assert.Nil(t, err)
+		}
+		version.Ver = ""
+	}
+}
+
+func TestMakeRequestsErrIxDiag(t *testing.T) {
+	bidder := &IxAdapter{}
+	req := &openrtb2.BidRequest{
+		ID:  "1",
+		Ext: json.RawMessage(`{"prebid":"channel":{"name":"web","version":"7.20"}}}`),
+	}
+	_, errs := bidder.MakeRequests(req, nil)
+	assert.Len(t, errs, 1)
+}
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
+}