Repository: prebid/prebid-server
Pull request #2955 (2023-07-20 12:56:12)

adapters package fixes reported by go vet
Refer #2954 for details.


clho40: LGTM
onkarvhanumante: > LGTM

@clho40 also requesting to update bidder params to include Huaweiads  email address 

Patch:
diff --git a/adapters/huaweiads/huaweiads.go b/adapters/huaweiads/huaweiads.go
index 2008eecd..071f0761 100644
--- a/adapters/huaweiads/huaweiads.go
+++ b/adapters/huaweiads/huaweiads.go
@@ -902,7 +902,6 @@ func getReqConsentInfo(request *huaweiAdsRequest, openRTBRequest *openrtb2.BidRe
 	if openRTBRequest.User != nil && openRTBRequest.User.Ext != nil {
 		var extUser openrtb_ext.ExtUser
 		if err := json.Unmarshal(openRTBRequest.User.Ext, &extUser); err != nil {
-			fmt.Errorf("failed to parse ExtUser in HuaweiAds GDPR check: %v", err)
 			return
 		}
 		request.Consent = extUser.Consent
diff --git a/adapters/taboola/taboola.go b/adapters/taboola/taboola.go
index cb47fc9b..087f6611 100644
--- a/adapters/taboola/taboola.go
+++ b/adapters/taboola/taboola.go
@@ -253,8 +253,7 @@ func makeRequestExt(pageType string) (json.RawMessage, error) {
 
 	requestExtJson, err := json.Marshal(requestExt)
 	if err != nil {
-		fmt.Errorf("could not marshal %s", requestExt)
-		return nil, err
+		return nil, fmt.Errorf("could not marshal %s, err: %s", requestExt, err)
 	}
 	return requestExtJson, nil
 
diff --git a/adapters/tpmn/tpmn.go b/adapters/tpmn/tpmn.go
new file mode 100644
index 00000000..7afe94e5
--- /dev/null
+++ b/adapters/tpmn/tpmn.go
@@ -0,0 +1,126 @@
+package tpmn
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+// TpmnAdapter struct
+type adapter struct {
+	uri string
+}
+
+// MakeRequests makes the HTTP requests which should be made to fetch bids from TpmnBidder.
+func (rcv *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	validImps, errs := getValidImpressions(request, reqInfo)
+	if len(validImps) == 0 {
+		return nil, errs
+	}
+
+	request.Imp = validImps
+
+	requestBodyJSON, err := json.Marshal(request)
+	if err != nil {
+		errs = append(errs, err)
+		return nil, errs
+	}
+
+	headers := http.Header{}
+	headers.Add("Content-Type", "application/json;charset=utf-8")
+	headers.Add("Accept", "application/json")
+
+	return []*adapters.RequestData{{
+		Method:  http.MethodPost,
+		Uri:     rcv.uri,
+		Body:    requestBodyJSON,
+		Headers: headers,
+	}}, errs
+}
+
+// getValidImpressions validate imps and check for bid floor currency. Convert to EUR if necessary
+func getValidImpressions(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]openrtb2.Imp, []error) {
+	var errs []error
+	var validImps []openrtb2.Imp
+
+	for _, imp := range request.Imp {
+		if err := preprocessBidFloorCurrency(&imp, reqInfo); err != nil {
+			errs = append(errs, err)
+			continue
+		}
+		validImps = append(validImps, imp)
+	}
+	return validImps, errs
+}
+
+func preprocessBidFloorCurrency(imp *openrtb2.Imp, reqInfo *adapters.ExtraRequestInfo) error {
+	// we expect every currency related data to be EUR
+	if imp.BidFloor > 0 && strings.ToUpper(imp.BidFloorCur) != "USD" && imp.BidFloorCur != "" {
+		if convertedValue, err := reqInfo.ConvertCurrency(imp.BidFloor, imp.BidFloorCur, "USD"); err != nil {
+			return err
+		} else {
+			imp.BidFloor = convertedValue
+		}
+	}
+	imp.BidFloorCur = "USD"
+	return nil
+}
+
+func (a *adapter) MakeBids(request *openrtb2.BidRequest, _ *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	if adapters.IsResponseStatusCodeNoContent(responseData) {
+		return nil, nil
+	}
+
+	if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil {
+		return nil, []error{err}
+	}
+
+	var response openrtb2.BidResponse
+	if err := json.Unmarshal(responseData.Body, &response); err != nil {
+		return nil, []error{fmt.Errorf("bid response unmarshal: %v", err)}
+	}
+
+	bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
+	bidResponse.Currency = response.Cur
+	for _, seatBid := range response.SeatBid {
+		for i, bid := range seatBid.Bid {
+			bidType, err := getMediaTypeForImp(bid)
+			if err != nil {
+				return nil, []error{err}
+			}
+			b := &adapters.TypedBid{
+				Bid:     &seatBid.Bid[i],
+				BidType: bidType,
+			}
+			bidResponse.Bids = append(bidResponse.Bids, b)
+		}
+	}
+	return bidResponse, nil
+}
+
+func getMediaTypeForImp(bid openrtb2.Bid) (openrtb_ext.BidType, error) {
+	switch bid.MType {
+	case openrtb2.MarkupBanner:
+		return openrtb_ext.BidTypeBanner, nil
+	case openrtb2.MarkupVideo:
+		return openrtb_ext.BidTypeVideo, nil
+	case openrtb2.MarkupNative:
+		return openrtb_ext.BidTypeNative, nil
+	default:
+		return "", fmt.Errorf("unsupported MType %d", bid.MType)
+	}
+}
+
+// Builder builds a new instance of the TpmnBidder adapter for the given bidder with the given config.
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
+	bidder := &adapter{
+		uri: config.Endpoint,
+	}
+	return bidder, nil
+}
diff --git a/endpoints/cookie_sync.go b/endpoints/cookie_sync.go
index ef3c40e8..49ef7074 100644
--- a/endpoints/cookie_sync.go
+++ b/endpoints/cookie_sync.go
@@ -149,6 +149,13 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, pr
 		}
 	}
 
+	activityControl, activitiesErr := privacy.NewActivityControl(account.Privacy)
+	if activitiesErr != nil {
+		if errortypes.ContainsFatalError([]error{activitiesErr}) {
+			activityControl = privacy.ActivityControl{}
+		}
+	}
+
 	syncTypeFilter, err := parseTypeFilter(request.FilterSettings)
 	if err != nil {
 		return usersync.Request{}, privacy.Policies{}, err
@@ -172,6 +179,7 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, pr
 		Privacy: usersyncPrivacy{
 			gdprPermissions:  gdprPerms,
 			ccpaParsedPolicy: ccpaParsedPolicy,
+			activityControl:  activityControl,
 		},
 		SyncTypeFilter: syncTypeFilter,
 	}
@@ -501,6 +509,7 @@ type usersyncPrivacyConfig struct {
 type usersyncPrivacy struct {
 	gdprPermissions  gdpr.Permissions
 	ccpaParsedPolicy ccpa.ParsedPolicy
+	activityControl  privacy.ActivityControl
 }
 
 func (p usersyncPrivacy) GDPRAllowsHostCookie() bool {
@@ -517,3 +526,9 @@ func (p usersyncPrivacy) CCPAAllowsBidderSync(bidder string) bool {
 	enforce := p.ccpaParsedPolicy.CanEnforce() && p.ccpaParsedPolicy.ShouldEnforce(bidder)
 	return !enforce
 }
+
+func (p usersyncPrivacy) ActivityAllowsUserSync(bidder string) bool {
+	activityResult := p.activityControl.Allow(privacy.ActivitySyncUser,
+		privacy.ScopedName{Scope: privacy.ScopeTypeBidder, Name: bidder})
+	return activityResult == privacy.ActivityAllow
+}
diff --git a/endpoints/setuid.go b/endpoints/setuid.go
index 25fb2465..8f691389 100644
--- a/endpoints/setuid.go
+++ b/endpoints/setuid.go
@@ -18,6 +18,7 @@ import (
 	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/gdpr"
 	"github.com/prebid/prebid-server/metrics"
+	"github.com/prebid/prebid-server/privacy"
 	gppPrivacy "github.com/prebid/prebid-server/privacy/gpp"
 	"github.com/prebid/prebid-server/stored_requests"
 	"github.com/prebid/prebid-server/usersync"
@@ -58,7 +59,7 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use
 
 		query := r.URL.Query()
 
-		syncer, err := getSyncer(query, syncersByBidder)
+		syncer, bidderName, err := getSyncer(query, syncersByBidder)
 		if err != nil {
 			w.WriteHeader(http.StatusBadRequest)
 			w.Write([]byte(err.Error()))
@@ -103,6 +104,20 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use
 			return
 		}
 
+		activities, activitiesErr := privacy.NewActivityControl(account.Privacy)
+		if activitiesErr != nil {
+			if errortypes.ContainsFatalError([]error{activitiesErr}) {
+				activities = privacy.ActivityControl{}
+			}
+		}
+
+		userSyncActivityAllowed := activities.Allow(privacy.ActivitySyncUser,
+			privacy.ScopedName{Scope: privacy.ScopeTypeBidder, Name: bidderName})
+		if userSyncActivityAllowed == privacy.ActivityDeny {
+			w.WriteHeader(http.StatusUnavailableForLegalReasons)
+			return
+		}
+
 		gdprRequestInfo, err := extractGDPRInfo(query)
 		if err != nil {
 			// Only exit if non-warning
@@ -178,7 +193,6 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use
 // first and the 'gdpr' and 'gdpr_consent' query params second. If found in both, throws a
 // warning. Can also throw a parsing or validation error
 func extractGDPRInfo(query url.Values) (reqInfo gdpr.RequestInfo, err error) {
-
 	reqInfo, err = parseGDPRFromGPP(query)
 	if err != nil {
 		return gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, err
@@ -306,19 +320,19 @@ func parseConsentFromGppStr(gppQueryValue string) (string, error) {
 	return gdprConsent, nil
 }
 
-func getSyncer(query url.Values, syncersByBidder map[string]usersync.Syncer) (usersync.Syncer, error) {
+func getSyncer(query url.Values, syncersByBidder map[string]usersync.Syncer) (usersync.Syncer, string, error) {
 	bidder := query.Get("bidder")
 
 	if bidder == "" {
-		return nil, errors.New(`"bidder" query param is required`)
+		return nil, "", errors.New(`"bidder" query param is required`)
 	}
 
 	syncer, syncerExists := syncersByBidder[bidder]
 	if !syncerExists {
-		return nil, errors.New("The bidder name provided is not supported by Prebid Server")
+		return nil, "", errors.New("The bidder name provided is not supported by Prebid Server")
 	}
 
-	return syncer, nil
+	return syncer, bidder, nil
 }
 
 // getResponseFormat reads the format query parameter or falls back to the syncer's default.
diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go
index a1038d71..11529c5e 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -157,6 +157,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/taboola"
 	"github.com/prebid/prebid-server/adapters/tappx"
 	"github.com/prebid/prebid-server/adapters/telaria"
+	"github.com/prebid/prebid-server/adapters/tpmn"
 	"github.com/prebid/prebid-server/adapters/trafficgate"
 	"github.com/prebid/prebid-server/adapters/triplelift"
 	"github.com/prebid/prebid-server/adapters/triplelift_native"
@@ -355,6 +356,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderTaboola:           taboola.Builder,
 		openrtb_ext.BidderTappx:             tappx.Builder,
 		openrtb_ext.BidderTelaria:           telaria.Builder,
+		openrtb_ext.BidderTpmn:              tpmn.Builder,
 		openrtb_ext.BidderTrafficGate:       trafficgate.Builder,
 		openrtb_ext.BidderTriplelift:        triplelift.Builder,
 		openrtb_ext.BidderTripleliftNative:  triplelift_native.Builder,
diff --git a/exchange/utils.go b/exchange/utils.go
index 5b29fd62..e70f5249 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -112,15 +112,13 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context,
 	lmtEnforcer := extractLMT(req.BidRequest, rs.privacyConfig)
 
 	// request level privacy policies
-	privacyEnforcement := privacy.Enforcement{
-		COPPA: req.BidRequest.Regs != nil && req.BidRequest.Regs.COPPA == 1,
-		LMT:   lmtEnforcer.ShouldEnforce(unknownBidder),
-	}
+	coppa := req.BidRequest.Regs != nil && req.BidRequest.Regs.COPPA == 1
+	lmt := lmtEnforcer.ShouldEnforce(unknownBidder)
 
 	privacyLabels.CCPAProvided = ccpaEnforcer.CanEnforce()
 	privacyLabels.CCPAEnforced = ccpaEnforcer.ShouldEnforce(unknownBidder)
-	privacyLabels.COPPAEnforced = privacyEnforcement.COPPA
-	privacyLabels.LMTEnforced = lmtEnforcer.ShouldEnforce(unknownBidder)
+	privacyLabels.COPPAEnforced = coppa
+	privacyLabels.LMTEnforced = lmt
 
 	var gdprEnforced bool
 	var gdprPerms gdpr.Permissions = &gdpr.AlwaysAllow{}
@@ -148,46 +146,75 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context,
 
 	// bidder level privacy policies
 	for _, bidderRequest := range allBidderRequests {
-		bidRequestAllowed := true
+		privacyEnforcement := privacy.Enforcement{
+			COPPA: coppa,
+			LMT:   lmt,
+		}
 
 		// fetchBids activity
-		fetchBidsActivityAllowed := auctionReq.Activities.Allow(privacy.ActivityFetchBids,
-			privacy.ScopedName{Scope: privacy.ScopeTypeBidder, Name: bidderRequest.BidderName.String()})
+		scopedName := privacy.ScopedName{Scope: privacy.ScopeTypeBidder, Name: bidderRequest.BidderName.String()}
+		fetchBidsActivityAllowed := auctionReq.Activities.Allow(privacy.ActivityFetchBids, scopedName)
 		if fetchBidsActivityAllowed == privacy.ActivityDeny {
 			// skip the call to a bidder if fetchBids activity is not allowed
 			// do not add this bidder to allowedBidderRequests
 			continue
 		}
 
-		// CCPA
-		privacyEnforcement.CCPA = ccpaEnforcer.ShouldEnforce(bidderRequest.BidderName.String())
+		var auctionPermissions gdpr.AuctionPermissions
+		var gdprErr error
 
-		// GDPR
 		if gdprEnforced {
-			auctionPermissions, err := gdprPerms.AuctionActivitiesAllowed(ctx, bidderRequest.BidderCoreName, bidderRequest.BidderName)
-			bidRequestAllowed = auctionPermissions.AllowBidRequest
-
-			if err == nil {
-				privacyEnforcement.GDPRGeo = !auctionPermissions.PassGeo
-				privacyEnforcement.GDPRID = !auctionPermissions.PassID
-			} else {
-				privacyEnforcement.GDPRGeo = true
-				privacyEnforcement.GDPRID = true
+			auctionPermissions, gdprErr = gdprPerms.AuctionActivitiesAllowed(ctx, bidderRequest.BidderCoreName, bidderRequest.BidderName)
+			if !auctionPermissions.AllowBidRequest {
+				// auction request is not permitted by GDPR
+				// do not add this bidder to allowedBidderRequests
+				rs.me.RecordAdapterGDPRRequestBlocked(bidderRequest.BidderCoreName)
+				continue
 			}
+		}
 
-			if !bidRequestAllowed {
-				rs.me.RecordAdapterGDPRRequestBlocked(bidderRequest.BidderCoreName)
+		passIDActivityAllowed := auctionReq.Activities.Allow(privacy.ActivityTransmitUserFPD, scopedName)
+		if passIDActivityAllowed == privacy.ActivityDeny {
+			privacyEnforcement.UFPD = true
+		} else {
+			// run existing policies (GDPR, CCPA, COPPA, LMT)
+			// potentially block passing IDs based on GDPR
+			if gdprEnforced {
+				if gdprErr == nil {
+					privacyEnforcement.GDPRID = !auctionPermissions.PassID
+				} else {
+					privacyEnforcement.GDPRID = true
+				}
 			}
+			// potentially block passing IDs based on CCPA
+			privacyEnforcement.CCPA = ccpaEnforcer.ShouldEnforce(bidderRequest.BidderName.String())
+		}
+
+		passGeoActivityAllowed := auctionReq.Activities.Allow(privacy.ActivityTransmitPreciseGeo, scopedName)
+		if passGeoActivityAllowed == privacy.ActivityDeny {
+			privacyEnforcement.PreciseGeo = true
+		} else {
+			// run existing policies (GDPR, CCPA, COPPA, LMT)
+			// potentially block passing geo based on GDPR
+			if gdprEnforced {
+				if gdprErr == nil {
+					privacyEnforcement.GDPRGeo = !auctionPermissions.PassGeo
+				} else {
+					privacyEnforcement.GDPRGeo = true
+				}
+			}
+			// potentially block passing geo based on CCPA
+			privacyEnforcement.CCPA = ccpaEnforcer.ShouldEnforce(bidderRequest.BidderName.String())
+
 		}
 
 		if auctionReq.FirstPartyData != nil && auctionReq.FirstPartyData[bidderRequest.BidderName] != nil {
 			applyFPD(auctionReq.FirstPartyData[bidderRequest.BidderName], bidderRequest.BidRequest)
 		}
 
-		if bidRequestAllowed {
-			privacyEnforcement.Apply(bidderRequest.BidRequest)
-			allowedBidderRequests = append(allowedBidderRequests, bidderRequest)
-		}
+		privacyEnforcement.Apply(bidderRequest.BidRequest)
+		allowedBidderRequests = append(allowedBidderRequests, bidderRequest)
+
 		// GPP downgrade: always downgrade unless we can confirm GPP is supported
 		if shouldSetLegacyPrivacy(rs.bidderInfo, string(bidderRequest.BidderCoreName)) {
 			setLegacyGDPRFromGPP(bidderRequest.BidRequest, gpp)
@@ -363,12 +390,13 @@ func buildRequestExtForBidder(bidder string, requestExt json.RawMessage, request
 	}
 
 	if requestExtParsed != nil {
-		prebid.CurrencyConversions = requestExtParsed.Prebid.CurrencyConversions
-		prebid.Integration = requestExtParsed.Prebid.Integration
 		prebid.Channel = requestExtParsed.Prebid.Channel
+		prebid.CurrencyConversions = requestExtParsed.Prebid.CurrencyConversions
 		prebid.Debug = requestExtParsed.Prebid.Debug
-		prebid.Server = requestExtParsed.Prebid.Server
+		prebid.Integration = requestExtParsed.Prebid.Integration
 		prebid.MultiBid = buildRequestExtMultiBid(bidder, requestExtParsed.Prebid.MultiBid, alternateBidderCodes)
+		prebid.Sdk = requestExtParsed.Prebid.Sdk
+		prebid.Server = requestExtParsed.Prebid.Server
 	}
 
 	// Marshal New Prebid Object
diff --git a/openrtb_ext/bid.go b/openrtb_ext/bid.go
index 20bbf0b3..7dc620d4 100644
--- a/openrtb_ext/bid.go
+++ b/openrtb_ext/bid.go
@@ -50,6 +50,7 @@ type ExtBidPrebidCacheBids struct {
 
 // ExtBidPrebidMeta defines the contract for bidresponse.seatbid.bid[i].ext.prebid.meta
 type ExtBidPrebidMeta struct {
+	AdapterCode          string          `json:"adaptercode,omitempty"`
 	AdvertiserDomains    []string        `json:"advertiserDomains,omitempty"`
 	AdvertiserID         int             `json:"advertiserId,omitempty"`
 	AdvertiserName       string          `json:"advertiserName,omitempty"`
@@ -57,14 +58,15 @@ type ExtBidPrebidMeta struct {
 	AgencyName           string          `json:"agencyName,omitempty"`
 	BrandID              int             `json:"brandId,omitempty"`
 	BrandName            string          `json:"brandName,omitempty"`
-	DemandSource         string          `json:"demandSource,omitempty"`
 	DChain               json.RawMessage `json:"dchain,omitempty"`
+	DemandSource         string          `json:"demandSource,omitempty"`
 	MediaType            string          `json:"mediaType,omitempty"`
 	NetworkID            int             `json:"networkId,omitempty"`
 	NetworkName          string          `json:"networkName,omitempty"`
 	PrimaryCategoryID    string          `json:"primaryCatId,omitempty"`
+	RendererName         string          `json:"rendererName,omitempty"`
+	RendererVersion      string          `json:"rendererVersion,omitempty"`
 	SecondaryCategoryIDs []string        `json:"secondaryCatIds,omitempty"`
-	AdapterCode          string          `json:"adaptercode,omitempty"`
 }
 
 // ExtBidPrebidVideo defines the contract for bidresponse.seatbid.bid[i].ext.prebid.video
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index e9cc81f8..c873e9ab 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -254,6 +254,7 @@ const (
 	BidderTaboola           BidderName = "taboola"
 	BidderTappx             BidderName = "tappx"
 	BidderTelaria           BidderName = "telaria"
+	BidderTpmn              BidderName = "tpmn"
 	BidderTrafficGate       BidderName = "trafficgate"
 	BidderTriplelift        BidderName = "triplelift"
 	BidderTripleliftNative  BidderName = "triplelift_native"
@@ -454,6 +455,7 @@ func CoreBidderNames() []BidderName {
 		BidderTaboola,
 		BidderTappx,
 		BidderTelaria,
+		BidderTpmn,
 		BidderTrafficGate,
 		BidderTriplelift,
 		BidderTripleliftNative,
diff --git a/openrtb_ext/imp_adnuntius.go b/openrtb_ext/imp_adnuntius.go
index 3759ad50..86023c48 100644
--- a/openrtb_ext/imp_adnuntius.go
+++ b/openrtb_ext/imp_adnuntius.go
@@ -3,6 +3,6 @@ package openrtb_ext
 type ImpExtAdnunitus struct {
 	Auid      string `json:"auId"`
 	Network   string `json:"network"`
-	NoCookies bool   `json:"noCookies'"`
+	NoCookies bool   `json:"noCookies"`
 	MaxDeals  int    `json:"maxDeals"`
 }
diff --git a/openrtb_ext/imp_tpmn.go b/openrtb_ext/imp_tpmn.go
new file mode 100644
index 00000000..373b3089
--- /dev/null
+++ b/openrtb_ext/imp_tpmn.go
@@ -0,0 +1,6 @@
+package openrtb_ext
+
+// ExtImpTpmn defines TPMN specifiec param
+type ExtImpTpmn struct {
+	InventoryId int `json:"inventoryId"`
+}
diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go
index adf00fcd..6f39f0be 100644
--- a/openrtb_ext/request.go
+++ b/openrtb_ext/request.go
@@ -41,52 +41,54 @@ type ExtRequest struct {
 
 // ExtRequestPrebid defines the contract for bidrequest.ext.prebid
 type ExtRequestPrebid struct {
-	Aliases              map[string]string         `json:"aliases,omitempty"`
-	AliasGVLIDs          map[string]uint16         `json:"aliasgvlids,omitempty"`
-	BidAdjustmentFactors map[string]float64        `json:"bidadjustmentfactors,omitempty"`
-	BidderConfigs        []BidderConfig            `json:"bidderconfig,omitempty"`
-	BidderParams         json.RawMessage           `json:"bidderparams,omitempty"`
-	Cache                *ExtRequestPrebidCache    `json:"cache,omitempty"`
-	Channel              *ExtRequestPrebidChannel  `json:"channel,omitempty"`
-	CurrencyConversions  *ExtRequestCurrency       `json:"currency,omitempty"`
-	Data                 *ExtRequestPrebidData     `json:"data,omitempty"`
-	Debug                bool                      `json:"debug,omitempty"`
-	Events               json.RawMessage           `json:"events,omitempty"`
-	Experiment           *Experiment               `json:"experiment,omitempty"`
-	Integration          string                    `json:"integration,omitempty"`
-	MultiBid             []*ExtMultiBid            `json:"multibid,omitempty"`
-	Passthrough          json.RawMessage           `json:"passthrough,omitempty"`
-	SChains              []*ExtRequestPrebidSChain `json:"schains,omitempty"`
-	Server               *ExtRequestPrebidServer   `json:"server,omitempty"`
-	StoredRequest        *ExtStoredRequest         `json:"storedrequest,omitempty"`
-	SupportDeals         bool                      `json:"supportdeals,omitempty"`
-	Targeting            *ExtRequestTargeting      `json:"targeting,omitempty"`
+	AdServerTargeting    []AdServerTarget                `json:"adservertargeting,omitempty"`
+	Aliases              map[string]string               `json:"aliases,omitempty"`
+	AliasGVLIDs          map[string]uint16               `json:"aliasgvlids,omitempty"`
+	BidAdjustmentFactors map[string]float64              `json:"bidadjustmentfactors,omitempty"`
+	BidAdjustments       *ExtRequestPrebidBidAdjustments `json:"bidadjustments,omitempty"`
+	BidderConfigs        []BidderConfig                  `json:"bidderconfig,omitempty"`
+	BidderParams         json.RawMessage                 `json:"bidderparams,omitempty"`
+	Cache                *ExtRequestPrebidCache          `json:"cache,omitempty"`
+	Channel              *ExtRequestPrebidChannel        `json:"channel,omitempty"`
+	CurrencyConversions  *ExtRequestCurrency             `json:"currency,omitempty"`
+	Data                 *ExtRequestPrebidData           `json:"data,omitempty"`
+	Debug                bool                            `json:"debug,omitempty"`
+	Events               json.RawMessage                 `json:"events,omitempty"`
+	Experiment           *Experiment                     `json:"experiment,omitempty"`
+	Floors               *PriceFloorRules                `json:"floors,omitempty"`
+	Integration          string                          `json:"integration,omitempty"`
+	MultiBid             []*ExtMultiBid                  `json:"multibid,omitempty"`
+	MultiBidMap          map[string]ExtMultiBid          `json:"-"`
+	Passthrough          json.RawMessage                 `json:"passthrough,omitempty"`
+	SChains              []*ExtRequestPrebidSChain       `json:"schains,omitempty"`
+	Sdk                  *ExtRequestSdk                  `json:"sdk,omitempty"`
+	Server               *ExtRequestPrebidServer         `json:"server,omitempty"`
+	StoredRequest        *ExtStoredRequest               `json:"storedrequest,omitempty"`
+	SupportDeals         bool                            `json:"supportdeals,omitempty"`
+	Targeting            *ExtRequestTargeting            `json:"targeting,omitempty"`
+
+	//AlternateBidderCodes is populated with host's AlternateBidderCodes config if not defined in request
+	AlternateBidderCodes *ExtAlternateBidderCodes `json:"alternatebiddercodes,omitempty"`
+
+	// Macros specifies list of custom macros along with the values. This is used while forming
+	// the tracker URLs, where PBS will replace the Custom Macro with its value with url-encoding
+	Macros map[string]string `json:"macros,omitempty"`
 
 	// NoSale specifies bidders with whom the publisher has a legal relationship where the
 	// passing of personally identifiable information doesn't constitute a sale per CCPA law.
 	// The array may contain a single sstar ('*') entry to represent all bidders.
 	NoSale []string `json:"nosale,omitempty"`
 
-	//AlternateBidderCodes is populated with host's AlternateBidderCodes config if not defined in request
-	AlternateBidderCodes *ExtAlternateBidderCodes `json:"alternatebiddercodes,omitempty"`
+	// ReturnAllBidStatus if true populates bidresponse.ext.prebid.seatnonbid with all bids which was
+	// either rejected, nobid, input error
+	ReturnAllBidStatus bool `json:"returnallbidstatus,omitempty"`
 
-	Floors      *PriceFloorRules       `json:"floors,omitempty"`
-	MultiBidMap map[string]ExtMultiBid `json:"-"`
 	// Trace controls the level of detail in the output information returned from executing hooks.
 	// There are two options:
 	// - verbose: sets maximum level of output information
 	// - basic: excludes debugmessages and analytic_tags from output
 	// any other value or an empty string disables trace output at all.
 	Trace string `json:"trace,omitempty"`
-
-	// Macros specifies list of custom macros along with the values. This is used while forming
-	// the tracker URLs, where PBS will replace the Custom Macro with its value with url-encoding
-	Macros            map[string]string               `json:"macros,omitempty"`
-	AdServerTargeting []AdServerTarget                `json:"adservertargeting,omitempty"`
-	BidAdjustments    *ExtRequestPrebidBidAdjustments `json:"bidadjustments,omitempty"`
-	// ReturnAllBidStatus if true populates bidresponse.ext.prebid.seatnonbid with all bids which was
-	// either rejected, nobid, input error
-	ReturnAllBidStatus bool `json:"returnallbidstatus,omitempty"`
 }
 
 type AdServerTarget struct {
@@ -345,6 +347,15 @@ type ExtRequestPrebidDataEidPermission struct {
 	Bidders []string `json:"bidders"`
 }
 
+type ExtRequestSdk struct {
+	Renderers []ExtRequestSdkRenderer `json:"renderers,omitempty"`
+}
+
+type ExtRequestSdkRenderer struct {
+	Name    string `json:"name,omitempty"`
+	Version string `json:"version,omitempty"`
+}
+
 type ExtMultiBid struct {
 	Bidder                 string   `json:"bidder,omitempty"`
 	Bidders                []string `json:"bidders,omitempty"`
diff --git a/privacy/enforcement.go b/privacy/enforcement.go
index 50d42e89..1a66cfab 100644
--- a/privacy/enforcement.go
+++ b/privacy/enforcement.go
@@ -9,22 +9,39 @@ type Enforcement struct {
 	GDPRGeo bool
 	GDPRID  bool
 	LMT     bool
+
+	// activities
+	UFPD       bool
+	Eids       bool
+	PreciseGeo bool
+	TID        bool
 }
 
 // Any returns true if at least one privacy policy requires enforcement.
-func (e Enforcement) Any() bool {
+func (e Enforcement) AnyLegacy() bool {
 	return e.CCPA || e.COPPA || e.GDPRGeo || e.GDPRID || e.LMT
 }
 
+func (e Enforcement) AnyActivities() bool {
+	return e.UFPD || e.PreciseGeo || e.Eids || e.TID
+}
+
 // Apply cleans personally identifiable information from an OpenRTB bid request.
 func (e Enforcement) Apply(bidRequest *openrtb2.BidRequest) {
 	e.apply(bidRequest, NewScrubber())
 }
 
 func (e Enforcement) apply(bidRequest *openrtb2.BidRequest, scrubber Scrubber) {
-	if bidRequest != nil && e.Any() {
-		bidRequest.Device = scrubber.ScrubDevice(bidRequest.Device, e.getDeviceIDScrubStrategy(), e.getIPv4ScrubStrategy(), e.getIPv6ScrubStrategy(), e.getGeoScrubStrategy())
-		bidRequest.User = scrubber.ScrubUser(bidRequest.User, e.getUserScrubStrategy(), e.getGeoScrubStrategy())
+	if bidRequest != nil {
+		if e.AnyActivities() {
+			bidRequest = scrubber.ScrubRequest(bidRequest, e)
+		}
+		if e.AnyLegacy() && !(e.UFPD && e.PreciseGeo && e.Eids) {
+			bidRequest.User = scrubber.ScrubUser(bidRequest.User, e.getUserScrubStrategy(), e.getGeoScrubStrategy())
+		}
+		if e.AnyLegacy() && !(e.UFPD && e.PreciseGeo) {
+			bidRequest.Device = scrubber.ScrubDevice(bidRequest.Device, e.getDeviceIDScrubStrategy(), e.getIPv4ScrubStrategy(), e.getIPv6ScrubStrategy(), e.getGeoScrubStrategy())
+		}
 	}
 }
 
diff --git a/privacy/enforcer.go b/privacy/enforcer.go
index d63cd8de..bcd9e948 100644
--- a/privacy/enforcer.go
+++ b/privacy/enforcer.go
@@ -3,7 +3,6 @@ package privacy
 import (
 	"fmt"
 	"github.com/prebid/prebid-server/config"
-	"github.com/prebid/prebid-server/errortypes"
 	"strings"
 )
 
@@ -32,10 +31,7 @@ func NewActivityControl(privacyConf *config.AccountPrivacy) (ActivityControl, er
 	var err error
 
 	if privacyConf == nil {
-		return ac, err
-	} else {
-		//temporarily disable Activities if they are specified at the account level
-		return ac, &errortypes.Warning{Message: "account.Privacy has no effect as the feature is under development."}
+		return ac, nil
 	}
 
 	plans := make(map[Activity]ActivityPlan)
diff --git a/privacy/scrubber.go b/privacy/scrubber.go
index 428193bc..97419128 100644
--- a/privacy/scrubber.go
+++ b/privacy/scrubber.go
@@ -2,6 +2,7 @@ package privacy
 
 import (
 	"encoding/json"
+	"github.com/prebid/prebid-server/util/ptrutil"
 	"strings"
 
 	"github.com/prebid/openrtb/v19/openrtb2"
@@ -70,6 +71,7 @@ const (
 
 // Scrubber removes PII from parts of an OpenRTB request.
 type Scrubber interface {
+	ScrubRequest(bidRequest *openrtb2.BidRequest, enforcement Enforcement) *openrtb2.BidRequest
 	ScrubDevice(device *openrtb2.Device, id ScrubStrategyDeviceID, ipv4 ScrubStrategyIPV4, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb2.Device
 	ScrubUser(user *openrtb2.User, strategy ScrubStrategyUser, geo ScrubStrategyGeo) *openrtb2.User
 }
@@ -81,6 +83,99 @@ func NewScrubber() Scrubber {
 	return scrubber{}
 }
 
+func (scrubber) ScrubRequest(bidRequest *openrtb2.BidRequest, enforcement Enforcement) *openrtb2.BidRequest {
+	var userExtParsed map[string]json.RawMessage
+	userExtModified := false
+
+	var userCopy *openrtb2.User
+	userCopy = ptrutil.Clone(bidRequest.User)
+
+	var deviceCopy *openrtb2.Device
+	deviceCopy = ptrutil.Clone(bidRequest.Device)
+
+	if userCopy != nil && (enforcement.UFPD || enforcement.Eids) {
+		if len(userCopy.Ext) != 0 {
+			json.Unmarshal(userCopy.Ext, &userExtParsed)
+		}
+	}
+
+	if enforcement.UFPD {
+		// transmitUfpd covers user.ext.data, user.data, user.id, user.buyeruid, user.yob, user.gender, user.keywords, user.kwarray
+		// and device.{ifa, macsha1, macmd5, dpidsha1, dpidmd5, didsha1, didmd5}
+		if deviceCopy != nil {
+			deviceCopy.DIDMD5 = ""
+			deviceCopy.DIDSHA1 = ""
+			deviceCopy.DPIDMD5 = ""
+			deviceCopy.DPIDSHA1 = ""
+			deviceCopy.IFA = ""
+			deviceCopy.MACMD5 = ""
+			deviceCopy.MACSHA1 = ""
+		}
+		if userCopy != nil {
+			userCopy.Data = nil
+			userCopy.ID = ""
+			userCopy.BuyerUID = ""
+			userCopy.Yob = 0
+			userCopy.Gender = ""
+			userCopy.Keywords = ""
+			userCopy.KwArray = nil
+
+			_, hasField := userExtParsed["data"]
+			if hasField {
+				delete(userExtParsed, "data")
+				userExtModified = true
+			}
+		}
+	}
+	if enforcement.Eids {
+		//transmitEids covers user.eids and user.ext.eids
+		if userCopy != nil {
+			userCopy.EIDs = nil
+			_, hasField := userExtParsed["eids"]
+			if hasField {
+				delete(userExtParsed, "eids")
+				userExtModified = true
+			}
+		}
+	}
+
+	if userExtModified {
+		userExt, _ := json.Marshal(userExtParsed)
+		userCopy.Ext = userExt
+	}
+
+	if enforcement.TID {
+		//remove source.tid and imp.ext.tid
+		if bidRequest.Source != nil {
+			bidRequest.Source.TID = ""
+		}
+		for ind, imp := range bidRequest.Imp {
+			impExt := scrubExtIDs(imp.Ext, "tid")
+			bidRequest.Imp[ind].Ext = impExt
+		}
+	}
+
+	if enforcement.PreciseGeo {
+		//round user's geographic location by rounding off IP address and lat/lng data.
+		//this applies to both device.geo and user.geo
+		if userCopy != nil && userCopy.Geo != nil {
+			userCopy.Geo = scrubGeoPrecision(userCopy.Geo)
+		}
+
+		if deviceCopy != nil {
+			if deviceCopy.Geo != nil {
+				deviceCopy.Geo = scrubGeoPrecision(deviceCopy.Geo)
+			}
+			deviceCopy.IP = scrubIPV4Lowest8(deviceCopy.IP)
+			deviceCopy.IPv6 = scrubIPV6Lowest32Bits(deviceCopy.IPv6)
+		}
+	}
+
+	bidRequest.Device = deviceCopy
+	bidRequest.User = userCopy
+	return bidRequest
+}
+
 func (scrubber) ScrubDevice(device *openrtb2.Device, id ScrubStrategyDeviceID, ipv4 ScrubStrategyIPV4, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb2.Device {
 	if device == nil {
 		return nil
@@ -131,7 +226,7 @@ func (scrubber) ScrubUser(user *openrtb2.User, strategy ScrubStrategyUser, geo S
 	if strategy == ScrubStrategyUserIDAndDemographic {
 		userCopy.BuyerUID = ""
 		userCopy.ID = ""
-		userCopy.Ext = scrubUserExtIDs(userCopy.Ext)
+		userCopy.Ext = scrubExtIDs(userCopy.Ext, "eids")
 		userCopy.Yob = 0
 		userCopy.Gender = ""
 	}
@@ -205,25 +300,25 @@ func scrubGeoPrecision(geo *openrtb2.Geo) *openrtb2.Geo {
 	return &geoCopy
 }
 
-func scrubUserExtIDs(userExt json.RawMessage) json.RawMessage {
-	if len(userExt) == 0 {
-		return userExt
+func scrubExtIDs(ext json.RawMessage, fieldName string) json.RawMessage {
+	if len(ext) == 0 {
+		return ext
 	}
 
 	var userExtParsed map[string]json.RawMessage
-	err := json.Unmarshal(userExt, &userExtParsed)
+	err := json.Unmarshal(ext, &userExtParsed)
 	if err != nil {
-		return userExt
+		return ext
 	}
 
-	_, hasEids := userExtParsed["eids"]
-	if hasEids {
-		delete(userExtParsed, "eids")
+	_, hasField := userExtParsed[fieldName]
+	if hasField {
+		delete(userExtParsed, fieldName)
 		result, err := json.Marshal(userExtParsed)
 		if err == nil {
 			return result
 		}
 	}
 
-	return userExt
+	return ext
 }
diff --git a/static/bidder-info/tpmn.yaml b/static/bidder-info/tpmn.yaml
new file mode 100644
index 00000000..8d88c451
--- /dev/null
+++ b/static/bidder-info/tpmn.yaml
@@ -0,0 +1,20 @@
+endpoint: "https://gat.tpmn.io/ortb/pbs_bidder"
+maintainer:
+  email: "prebid@tpmn.io"
+modifyingVastXmlAllowed: true
+endpointCompression: GZIP
+capabilities:
+  app:
+    mediaTypes:
+      - banner
+      - video
+      - native
+  site:
+    mediaTypes:
+      - banner
+      - video
+      - native
+userSync:
+  redirect:
+    url: "https://gat.tpmn.io/sync/redirect?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir={{.RedirectURL}}"
+    userMacro: "$UID"
\ No newline at end of file
diff --git a/static/bidder-params/tpmn.json b/static/bidder-params/tpmn.json
new file mode 100644
index 00000000..68b54373
--- /dev/null
+++ b/static/bidder-params/tpmn.json
@@ -0,0 +1,16 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "TPMN Adapter Params",
+  "description": "A schema which validates params accepted by the TPMN adapter",
+  "type": "object",
+  "properties": {
+    "inventoryId": {
+      "description": "Inventory ID",
+      "type": "integer",
+      "minLength": 1
+    }
+  },
+  "required": [
+    "inventoryId"
+  ]
+}
\ No newline at end of file
diff --git a/usersync/chooser.go b/usersync/chooser.go
index 97fa1471..3f478049 100644
--- a/usersync/chooser.go
+++ b/usersync/chooser.go
@@ -85,6 +85,9 @@ const (
 
 	// StatusDuplicate specifies the bidder is a duplicate or shared a syncer key with another bidder choice.
 	StatusDuplicate
+
+	// StatusBlockedByPrivacy specifies a bidder sync url is not allowed by privacy activities
+	StatusBlockedByPrivacy
 )
 
 // Privacy determines which privacy policies will be enforced for a user sync request.
@@ -92,6 +95,7 @@ type Privacy interface {
 	GDPRAllowsHostCookie() bool
 	GDPRAllowsBidderSync(bidder string) bool
 	CCPAAllowsBidderSync(bidder string) bool
+	ActivityAllowsUserSync(bidder string) bool
 }
 
 // standardChooser implements the user syncer algorithm per official Prebid specification.
@@ -151,6 +155,11 @@ func (c standardChooser) evaluate(bidder string, syncersSeen map[string]struct{}
 		return nil, BidderEvaluation{Status: StatusAlreadySynced, Bidder: bidder, SyncerKey: syncer.Key()}
 	}
 
+	userSyncActivityAllowed := privacy.ActivityAllowsUserSync(bidder)
+	if !userSyncActivityAllowed {
+		return nil, BidderEvaluation{Status: StatusBlockedByPrivacy, Bidder: bidder, SyncerKey: syncer.Key()}
+	}
+
 	if !privacy.GDPRAllowsBidderSync(bidder) {
 		return nil, BidderEvaluation{Status: StatusBlockedByGDPR, Bidder: bidder, SyncerKey: syncer.Key()}
 	}


Test patch:
diff --git a/adapters/rubicon/rubicon_test.go b/adapters/rubicon/rubicon_test.go
index b665746e..5f75ea5f 100644
--- a/adapters/rubicon/rubicon_test.go
+++ b/adapters/rubicon/rubicon_test.go
@@ -290,12 +290,12 @@ type mockCurrencyConversion struct {
 	mock.Mock
 }
 
-func (m mockCurrencyConversion) GetRate(from string, to string) (float64, error) {
+func (m *mockCurrencyConversion) GetRate(from string, to string) (float64, error) {
 	args := m.Called(from, to)
 	return args.Get(0).(float64), args.Error(1)
 }
 
-func (m mockCurrencyConversion) GetRates() *map[string]map[string]float64 {
+func (m *mockCurrencyConversion) GetRates() *map[string]map[string]float64 {
 	args := m.Called()
 	return args.Get(0).(*map[string]map[string]float64)
 }
diff --git a/adapters/tpmn/params_test.go b/adapters/tpmn/params_test.go
new file mode 100644
index 00000000..7bd7c478
--- /dev/null
+++ b/adapters/tpmn/params_test.go
@@ -0,0 +1,44 @@
+package tpmn
+
+import (
+	"encoding/json"
+	"github.com/prebid/prebid-server/openrtb_ext"
+	"testing"
+)
+
+var validParams = []string{
+	`{"inventoryId": 10000001}`,
+}
+
+var invalidParams = []string{
+	`{"inventoryId": "00000001"}`,
+	`{"inventoryid": 100000000}`,
+}
+
+// TestValidParams makes sure that the tpmn schema accepts all imp.ext fields which we intend to support.
+func TestValidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json-schemas. %v", err)
+	}
+
+	for _, validParam := range validParams {
+		if err := validator.Validate(openrtb_ext.BidderTpmn, json.RawMessage(validParam)); err != nil {
+			t.Errorf("Schema rejected TPMN params: %s", validParam)
+		}
+	}
+}
+
+// TestInvalidParams makes sure that the tpmn schema rejects all the imp.ext fields we don't support.
+func TestInvalidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json-schemas. %v", err)
+	}
+
+	for _, invalidParam := range invalidParams {
+		if err := validator.Validate(openrtb_ext.BidderTpmn, json.RawMessage(invalidParam)); err == nil {
+			t.Errorf("Schema allowed unexpected params: %s", invalidParam)
+		}
+	}
+}
diff --git a/adapters/tpmn/tpmn_test.go b/adapters/tpmn/tpmn_test.go
new file mode 100644
index 00000000..6fbd8593
--- /dev/null
+++ b/adapters/tpmn/tpmn_test.go
@@ -0,0 +1,20 @@
+package tpmn
+
+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.BidderTpmn, config.Adapter{
+		Endpoint: "https://gat.tpmn.io/ortb/pbs_bidder"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})
+
+	if buildErr != nil {
+		t.Fatalf("Builder returned unexpected error %v", buildErr)
+	}
+
+	adapterstest.RunJSONBidderTest(t, "tpmntest", bidder)
+}
diff --git a/adapters/tpmn/tpmntest/exemplary/simple-banner.json b/adapters/tpmn/tpmntest/exemplary/simple-banner.json
new file mode 100644
index 00000000..197d03b1
--- /dev/null
+++ b/adapters/tpmn/tpmntest/exemplary/simple-banner.json
@@ -0,0 +1,135 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "tagid": "00000001",
+        "ext": {
+          "bidder": {
+            "inventoryId": 10000001
+          }
+        }
+      }
+    ],
+    "app": {
+      "id": "1",
+      "bundle": "com.wls.testwlsapplication"
+    },
+    "device": {
+      "ip": "123.123.123.123",
+      "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx"
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://gat.tpmn.io/ortb/pbs_bidder",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  },
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              },
+              "tagid": "00000001",
+              "bidfloorcur": "USD",
+              "ext": {
+                "bidder": {
+                  "inventoryId": 10000001
+                }
+              }
+            }
+          ],
+          "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+          },
+          "device": {
+            "ip": "123.123.123.123",
+            "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "test_bid_id",
+                  "impid": "test-imp-id",
+                  "price": 0.27543,
+                  "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"https://gat.tpmn.io/ortb/pbs_bidder&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                  "cid": "test_cid",
+                  "crid": "test_crid",
+                  "dealid": "test_dealid",
+                  "w": 300,
+                  "h": 250,
+                  "mtype": 1,
+                  "ext": {
+                    "prebid": {
+                      "type": "banner"
+                    }
+                  }
+                }
+              ],
+              "seat": "tpmn"
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "bids": [
+        {
+          "bid": {
+            "id": "test_bid_id",
+            "impid": "test-imp-id",
+            "price": 0.27543,
+            "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"https://gat.tpmn.io/ortb/pbs_bidder&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+            "cid": "test_cid",
+            "crid": "test_crid",
+            "dealid": "test_dealid",
+            "w": 300,
+            "h": 250,
+            "mtype": 1,
+            "ext": {
+              "prebid": {
+                "type": "banner"
+              }
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/tpmn/tpmntest/exemplary/simple-native.json b/adapters/tpmn/tpmntest/exemplary/simple-native.json
new file mode 100644
index 00000000..1880c74a
--- /dev/null
+++ b/adapters/tpmn/tpmntest/exemplary/simple-native.json
@@ -0,0 +1,119 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "device": {
+            "ip": "123.123.123.123",
+            "ua": "iPad"
+        },
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "native": {
+                    "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}",
+                    "ver": "1.1"
+                },
+                "ext": {
+                    "bidder": {
+                        "inventoryId": 10000001
+                    }
+                }
+            }
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "https://gat.tpmn.io/ortb/pbs_bidder",
+                "body": {
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "tagid": "test",
+                            "native": {
+                                "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}",
+                                "ver": "1.1"
+                            },
+                            "bidfloorcur": "USD",
+                            "ext": {
+                                "bidder": {
+                                    "inventoryId": 10000001
+                                }
+                            }
+                        }
+                    ],
+                    "app": {
+                        "id": "1",
+                        "bundle": "com.wls.testwlsapplication"
+                    },
+                    "device": {
+                        "ip": "123.123.123.123",
+                        "ua": "iPad"
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "bid": [
+                                {
+                                    "id": "test_bid_id",
+                                    "impid": "test-imp-id",
+                                    "price": 0.27543,
+                                    "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://example.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                                    "cid": "test_cid",
+                                    "crid": "test_crid",
+                                    "dealid": "test_dealid",
+                                    "w": 300,
+                                    "h": 250,
+                                    "mtype": 4,
+                                    "ext": {
+                                        "prebid": {
+                                            "type": "native"
+                                        }
+                                    }
+                                }
+                            ],
+                            "seat": "tpmn"
+                        }
+                    ],
+                    "cur": "USD"
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "bids": [
+                {
+                    "bid": {
+                        "id": "test_bid_id",
+                        "impid": "test-imp-id",
+                        "price": 0.27543,
+                        "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://example.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                        "cid": "test_cid",
+                        "crid": "test_crid",
+                        "dealid": "test_dealid",
+                        "w": 300,
+                        "h": 250,
+                        "mtype": 4,
+                        "ext": {
+                            "prebid": {
+                                "type": "native"
+                            }
+                        }
+                    },
+                    "type": "native"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/tpmn/tpmntest/exemplary/simple-site-banner.json b/adapters/tpmn/tpmntest/exemplary/simple-site-banner.json
new file mode 100644
index 00000000..8f7c5d59
--- /dev/null
+++ b/adapters/tpmn/tpmntest/exemplary/simple-site-banner.json
@@ -0,0 +1,132 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "inventoryId": 10000001
+          }
+        }
+      }
+    ],
+    "site": {
+      "id": "1",
+      "domain": "test.com"
+    },
+    "device": {
+      "ip": "123.123.123.123"
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://gat.tpmn.io/ortb/pbs_bidder",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  },
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              },
+              "bidfloorcur": "USD",
+              "ext": {
+                "bidder": {
+                  "inventoryId": 10000001
+                }
+              }
+            }
+          ],
+          "site": {
+            "id": "1",
+            "domain": "test.com"
+          },
+          "device": {
+            "ip": "123.123.123.123"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "test_bid_id",
+                  "impid": "test-imp-id",
+                  "price": 0.27543,
+                  "adm": "<iframe id=\"adm-banner-1\" width=\"468\" height=\"60\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"https://gat.tpmn.io/ortb/pbs_bidder&k=bc2d316f39931a07d9a8dd249bf85fc0\"></iframe>",
+                  "cid": "test_cid",
+                  "crid": "test_crid",
+                  "dealid": "test_dealid",
+                  "w": 468,
+                  "h": 60,
+                  "mtype": 1,
+                  "ext": {
+                    "prebid": {
+                      "type": "banner"
+                    }
+                  }
+                }
+              ],
+              "seat": "tpmn"
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "bids": [
+        {
+          "bid": {
+            "id": "test_bid_id",
+            "impid": "test-imp-id",
+            "price": 0.27543,
+            "adm": "<iframe id=\"adm-banner-1\" width=\"468\" height=\"60\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"https://gat.tpmn.io/ortb/pbs_bidder&k=bc2d316f39931a07d9a8dd249bf85fc0\"></iframe>",
+            "cid": "test_cid",
+            "crid": "test_crid",
+            "dealid": "test_dealid",
+            "w": 468,
+            "h": 60,
+            "mtype": 1,
+            "ext": {
+              "prebid": {
+                "type": "banner"
+              }
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
+  
\ No newline at end of file
diff --git a/adapters/tpmn/tpmntest/exemplary/simple-site-native.json b/adapters/tpmn/tpmntest/exemplary/simple-site-native.json
new file mode 100644
index 00000000..20e6c23e
--- /dev/null
+++ b/adapters/tpmn/tpmntest/exemplary/simple-site-native.json
@@ -0,0 +1,104 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "site": {
+      "page": "prebid.org"
+    },
+    "device": {
+      "ip":"123.123.123.123"
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "native": {
+          "ver": "1.1",
+          "request": "{\"ver\":\"1.0\",\"layout\":1,\"adunit\":1,\"plcmttype\":1,\"plcmtcnt\":1,\"seq\":0,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":60,\"hmin\":60,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":3,\"required\":0,\"data\":{\"type\":2,\"len\":75}},{\"id\":4,\"required\":0,\"data\":{\"type\":6,\"len\":1000}},{\"id\":5,\"required\":0,\"data\":{\"type\":7,\"len\":1000}},{\"id\":6,\"required\":0,\"data\":{\"type\":11,\"len\":1000}}]}"
+        },
+        "ext": {
+          "bidder": {
+            "inventoryId": 10000001
+          }
+        }
+      }
+    ]
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://gat.tpmn.io/ortb/pbs_bidder",
+        "headers": {
+          "Accept": [
+            "application/json"
+          ],
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ]
+        },
+        "body": {
+          "id": "test-request-id",
+          "site": {
+            "page": "prebid.org"
+          },
+          "device": {
+            "ip":"123.123.123.123"
+          },
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "native": {
+                "ver": "1.1",
+                "request": "{\"ver\":\"1.0\",\"layout\":1,\"adunit\":1,\"plcmttype\":1,\"plcmtcnt\":1,\"seq\":0,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":60,\"hmin\":60,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":3,\"required\":0,\"data\":{\"type\":2,\"len\":75}},{\"id\":4,\"required\":0,\"data\":{\"type\":6,\"len\":1000}},{\"id\":5,\"required\":0,\"data\":{\"type\":7,\"len\":1000}},{\"id\":6,\"required\":0,\"data\":{\"type\":11,\"len\":1000}}]}"
+              },
+              "bidfloorcur": "USD",
+              "ext": {
+                "bidder": {
+                  "inventoryId": 10000001
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "tpmn",
+              "bid": [{
+                "id": "1",
+                "impid": "test-imp-id",
+                "price": 0.500000,
+                "adm": "some-test-adm",
+                "crid": "test-crid",
+                "mtype": 4
+              }]
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "1",
+            "impid": "test-imp-id",
+            "price": 0.5,
+            "adm": "some-test-adm",
+            "crid": "test-crid",
+            "mtype": 4
+          },
+          "type": "native"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/tpmn/tpmntest/exemplary/simple-site-video.json b/adapters/tpmn/tpmntest/exemplary/simple-site-video.json
new file mode 100644
index 00000000..7b2375cd
--- /dev/null
+++ b/adapters/tpmn/tpmntest/exemplary/simple-site-video.json
@@ -0,0 +1,128 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "site": {
+      "page": "prebid.org"
+    },
+    "device": {
+      "ip":"123.123.123.123"
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "video": {
+          "mimes": [
+            "video/mp4"
+          ],
+          "protocols": [
+            2,
+            3,
+            5,
+            6
+          ],
+          "w": 1024,
+          "h": 576
+        },
+        "ext": {
+          "bidder": {
+            "inventoryId": 10000001
+          }
+        }
+      }
+    ]
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://gat.tpmn.io/ortb/pbs_bidder",
+        "headers": {
+          "Accept": [
+            "application/json"
+          ],
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ]
+        },
+        "body": {
+          "id": "test-request-id",
+          "site": {
+            "page": "prebid.org"
+          },
+          "device": {
+            "ip":"123.123.123.123"
+          },
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "video": {
+                "mimes": [
+                  "video/mp4"
+                ],
+                "protocols": [
+                  2,
+                  3,
+                  5,
+                  6
+                ],
+                "w": 1024,
+                "h": 576
+              },
+              "bidfloorcur": "USD",
+              "ext": {
+                "bidder": {
+                  "inventoryId": 10000001
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "cur": "USD",
+          "seatbid": [
+            {
+              "seat": "tpmn",
+              "bid": [
+                {
+                  "id": "1",
+                  "impid": "test-imp-id",
+                  "price": 0.500000,
+                  "adm": "some-test-adm",
+                  "crid": "test-crid",
+                  "w": 1024,
+                  "h": 576,
+                  "mtype": 2
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "1",
+            "impid": "test-imp-id",
+            "price": 0.5,
+            "adm": "some-test-adm",
+            "crid": "test-crid",
+            "w": 1024,
+            "h": 576,
+            "mtype": 2
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/tpmn/tpmntest/exemplary/simple-video.json b/adapters/tpmn/tpmntest/exemplary/simple-video.json
new file mode 100644
index 00000000..505b6167
--- /dev/null
+++ b/adapters/tpmn/tpmntest/exemplary/simple-video.json
@@ -0,0 +1,130 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "device": {
+      "ip": "123.123.123.123",
+      "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx"
+    },
+    "app": {
+      "id": "1",
+      "bundle": "com.wls.testwlsapplication"
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "video": {
+          "mimes": [
+            "video/mp4"
+          ],
+          "protocols": [
+            2,
+            5
+          ],
+          "w": 1024,
+          "h": 576
+        },
+        "bidfloorcur": "USD",
+        "ext": {
+          "bidder": {
+            "inventoryId": 10000001
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://gat.tpmn.io/ortb/pbs_bidder",
+        "body": {
+          "id": "test-request-id",
+          "device": {
+            "ip": "123.123.123.123",
+            "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx"
+          },
+          "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+          },
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "video": {
+                "mimes": [
+                  "video/mp4"
+                ],
+                "protocols": [
+                  2,
+                  5
+                ],
+                "w": 1024,
+                "h": 576
+              },
+
+              "bidfloorcur": "USD",
+              "ext": {
+                "bidder": {
+                  "inventoryId": 10000001
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "test_bid_id",
+                  "impid": "test-imp-id",
+                  "price": 0.27543,
+                  "adm": "<VAST version=\"3.0\"><Ad id=\"20001\" sequence=\"1\"><InLine><AdSystem version=\"4.0\"><![CDATA[iabtechlab]]></AdSystem><AdTitle><![CDATA[Inline Simple Ad]]></AdTitle><Impression><![CDATA[]]></Impression><Creatives><Creative id=\"5480\" sequence=\"1\"><Linear><Duration>00:01:00</Duration><MediaFiles><MediaFile id=\"5246\" delivery=\"progressive\" type=\"video/mp4\" bitrate=\"600\" width=\"640\" height=\"360\" minBitrate=\"500\" maxBitrate=\"700\" scalable=\"1\" maintainAspectRatio=\"1\" codec=\"0\"><![CDATA[https://s0.2mdn.net/4253510/google_ddm_animation_480P.mp4]]></MediaFile></MediaFiles><VideoClicks><ClickThrough id=\"blog\"><![CDATA[https://example.com]]></ClickThrough></VideoClicks></Linear></Creative></Creatives></InLine></Ad></VAST>",
+                  "cid": "test_cid",
+                  "crid": "test_crid",
+                  "dealid": "test_dealid",
+                  "mtype": 2,
+                  "ext": {
+                    "prebid": {
+                      "type": "video"
+                    }
+                  }
+                }
+              ],
+              "seat": "tpmn"
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "test_bid_id",
+            "impid": "test-imp-id",
+            "price": 0.27543,
+            "adm": "<VAST version=\"3.0\"><Ad id=\"20001\" sequence=\"1\"><InLine><AdSystem version=\"4.0\"><![CDATA[iabtechlab]]></AdSystem><AdTitle><![CDATA[Inline Simple Ad]]></AdTitle><Impression><![CDATA[]]></Impression><Creatives><Creative id=\"5480\" sequence=\"1\"><Linear><Duration>00:01:00</Duration><MediaFiles><MediaFile id=\"5246\" delivery=\"progressive\" type=\"video/mp4\" bitrate=\"600\" width=\"640\" height=\"360\" minBitrate=\"500\" maxBitrate=\"700\" scalable=\"1\" maintainAspectRatio=\"1\" codec=\"0\"><![CDATA[https://s0.2mdn.net/4253510/google_ddm_animation_480P.mp4]]></MediaFile></MediaFiles><VideoClicks><ClickThrough id=\"blog\"><![CDATA[https://example.com]]></ClickThrough></VideoClicks></Linear></Creative></Creatives></InLine></Ad></VAST>",
+            "cid": "test_cid",
+            "crid": "test_crid",
+            "mtype": 2,
+            "dealid": "test_dealid",
+            "ext": {
+              "prebid": {
+                "type": "video"
+              }
+            }
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/tpmn/tpmntest/supplemental/bad-imp-ext.json b/adapters/tpmn/tpmntest/supplemental/bad-imp-ext.json
new file mode 100644
index 00000000..3bbb23f9
--- /dev/null
+++ b/adapters/tpmn/tpmntest/supplemental/bad-imp-ext.json
@@ -0,0 +1,79 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "tagid": "00000001",
+        "ext": {
+        }
+      }
+    ],
+    "app": {
+      "id": "1",
+      "bundle": "com.wls.testwlsapplication"
+    },
+    "device": {
+      "ip": "123.123.123.123",
+      "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx"
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://gat.tpmn.io/ortb/pbs_bidder",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  },
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              },
+              "tagid": "00000001",
+              "bidfloorcur": "USD",
+              "ext": {
+              }
+            }
+          ],
+          "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+          },
+          "device": {
+            "ip": "123.123.123.123",
+            "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 204,
+        "body": {
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+  ]
+}
diff --git a/adapters/tpmn/tpmntest/supplemental/bad_response.json b/adapters/tpmn/tpmntest/supplemental/bad_response.json
new file mode 100644
index 00000000..12c3a72d
--- /dev/null
+++ b/adapters/tpmn/tpmntest/supplemental/bad_response.json
@@ -0,0 +1,87 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "bidfloorcur": "USD",
+        "ext": {
+          "bidder": {
+            "inventoryId": 10000001
+          }
+        }
+      }
+    ],
+    "app": {
+      "id": "1",
+      "bundle": "com.wls.testwlsapplication"
+    },
+    "device": {
+      "ip": "123.123.123.123",
+      "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://gat.tpmn.io/ortb/pbs_bidder",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  },
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              },
+              "bidfloorcur": "USD",
+              "ext": {
+                "bidder": {
+                  "inventoryId": 10000001
+                }
+              }
+            }
+          ],
+          "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+          },
+          "device": {
+            "ip": "123.123.123.123",
+            "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": ""
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "bid response unmarshal: json: cannot unmarshal string into Go value of type openrtb2.BidResponse",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/tpmn/tpmntest/supplemental/no-imp-ext.json b/adapters/tpmn/tpmntest/supplemental/no-imp-ext.json
new file mode 100644
index 00000000..8bcbb93d
--- /dev/null
+++ b/adapters/tpmn/tpmntest/supplemental/no-imp-ext.json
@@ -0,0 +1,81 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "tagid": "16",
+        "ext": ""
+      }
+    ],
+    "app": {
+      "id": "1",
+      "bundle": "com.wls.testwlsapplication"
+    },
+    "device": {
+      "ip": "123.123.123.123",
+      "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx"
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://gat.tpmn.io/ortb/pbs_bidder",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  },
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              },
+              "tagid": "16",
+              "ext": "",
+              "bidfloorcur": "USD"
+            }
+          ],
+          "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+          },
+          "device": {
+            "ip": "123.123.123.123",
+            "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": ""
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "bid response unmarshal: json: cannot unmarshal string into Go value of type openrtb2.BidResponse",
+      "comparison": "literal"
+    }
+  ]
+}
+  
\ No newline at end of file
diff --git a/adapters/tpmn/tpmntest/supplemental/status-204.json b/adapters/tpmn/tpmntest/supplemental/status-204.json
new file mode 100644
index 00000000..fdcd3f7f
--- /dev/null
+++ b/adapters/tpmn/tpmntest/supplemental/status-204.json
@@ -0,0 +1,81 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "inventoryId": 10000001
+          }
+        }
+      }
+    ],
+    "app": {
+      "id": "1",
+      "bundle": "com.wls.testwlsapplication"
+    },
+    "device": {
+      "ip": "123.123.123.123",
+      "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://gat.tpmn.io/ortb/pbs_bidder",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  },
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              },
+              "bidfloorcur": "USD",
+              "ext": {
+                "bidder": {
+                  "inventoryId": 10000001
+                }
+              }
+            }
+          ],
+          "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+          },
+          "device": {
+            "ip": "123.123.123.123",
+            "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 204,
+        "body": {}
+      }
+    }
+  ],
+  "expectedBidResponses": []
+}
\ No newline at end of file
diff --git a/adapters/tpmn/tpmntest/supplemental/status-404.json b/adapters/tpmn/tpmntest/supplemental/status-404.json
new file mode 100644
index 00000000..74ced152
--- /dev/null
+++ b/adapters/tpmn/tpmntest/supplemental/status-404.json
@@ -0,0 +1,86 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "inventoryId": 10000001
+          }
+        }
+      }
+    ],
+    "app": {
+      "id": "1",
+      "bundle": "com.wls.testwlsapplication"
+    },
+    "device": {
+      "ip": "123.123.123.123",
+      "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://gat.tpmn.io/ortb/pbs_bidder",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  },
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              },
+              "bidfloorcur": "USD",
+              "ext": {
+                "bidder": {
+                  "inventoryId": 10000001
+                }
+              }
+            }
+          ],
+          "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+          },
+          "device": {
+            "ip": "123.123.123.123",
+            "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 404,
+        "body": {}
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Unexpected status code: 404. Run with request.debug = 1 for more info",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go
index a69a2cdb..e9d812b8 100644
--- a/endpoints/cookie_sync_test.go
+++ b/endpoints/cookie_sync_test.go
@@ -499,6 +499,7 @@ func TestCookieSyncParseRequest(t *testing.T) {
 		expectedPrivacy      privacy.Policies
 		expectedRequest      usersync.Request
 	}{
+
 		{
 			description: "Complete Request - includes GPP string with EU TCF V2",
 			givenBody: strings.NewReader(`{` +
@@ -973,6 +974,38 @@ func TestCookieSyncParseRequest(t *testing.T) {
 			expectedError:        errCookieSyncAccountBlocked.Error(),
 			givenAccountRequired: true,
 		},
+
+		{
+			description: "Account Defaults - Invalid Activities",
+			givenBody: strings.NewReader(`{` +
+				`"bidders":["a", "b"],` +
+				`"account":"ValidAccountInvalidActivities"` +
+				`}`),
+			givenGDPRConfig:  config.GDPR{Enabled: true, DefaultValue: "0"},
+			givenCCPAEnabled: true,
+			givenConfig: config.UserSync{
+				Cooperative: config.UserSyncCooperative{
+					EnabledByDefault: false,
+					PriorityGroups:   [][]string{{"a", "b", "c"}},
+				},
+			},
+			expectedPrivacy: privacy.Policies{},
+			expectedRequest: usersync.Request{
+				Bidders: []string{"a", "b"},
+				Cooperative: usersync.Cooperative{
+					Enabled:        false,
+					PriorityGroups: [][]string{{"a", "b", "c"}},
+				},
+				Limit: 0,
+				Privacy: usersyncPrivacy{
+					gdprPermissions: &fakePermissions{},
+				},
+				SyncTypeFilter: usersync.SyncTypeFilter{
+					IFrame:   usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
+					Redirect: usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude),
+				},
+			},
+		},
 	}
 
 	for _, test := range testCases {
@@ -997,8 +1030,9 @@ func TestCookieSyncParseRequest(t *testing.T) {
 				ccpaEnforce:            test.givenCCPAEnabled,
 			},
 			accountsFetcher: FakeAccountsFetcher{AccountData: map[string]json.RawMessage{
-				"TestAccount":     json.RawMessage(`{"cookie_sync": {"default_limit": 20, "max_limit": 30, "default_coop_sync": true}}`),
-				"DisabledAccount": json.RawMessage(`{"disabled":true}`),
+				"TestAccount":                   json.RawMessage(`{"cookie_sync": {"default_limit": 20, "max_limit": 30, "default_coop_sync": true}}`),
+				"DisabledAccount":               json.RawMessage(`{"disabled":true}`),
+				"ValidAccountInvalidActivities": json.RawMessage(`{"privacy":{"allowactivities":{"syncUser":{"rules":[{"condition":{"componentName": ["bidderA.bidderB.bidderC"]}}]}}}}`),
 			}},
 		}
 		assert.NoError(t, endpoint.config.MarshalAccountDefaults())
@@ -1871,6 +1905,41 @@ func TestUsersyncPrivacyCCPAAllowsBidderSync(t *testing.T) {
 	}
 }
 
+func TestCookieSyncActivityControlIntegration(t *testing.T) {
+	testCases := []struct {
+		name           string
+		bidderName     string
+		allow          bool
+		expectedResult bool
+	}{
+		{
+			name:           "activity_is_allowed",
+			bidderName:     "bidderA",
+			allow:          true,
+			expectedResult: true,
+		},
+		{
+			name:           "activity_is_denied",
+			bidderName:     "bidderA",
+			allow:          false,
+			expectedResult: false,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			privacyConfig := getDefaultActivityConfig(test.bidderName, test.allow)
+			activities, err := privacy.NewActivityControl(privacyConfig)
+			assert.NoError(t, err)
+			up := usersyncPrivacy{
+				activityControl: activities,
+			}
+			actualResult := up.ActivityAllowsUserSync(test.bidderName)
+			assert.Equal(t, test.expectedResult, actualResult)
+		})
+	}
+}
+
 func TestCombineErrors(t *testing.T) {
 	testCases := []struct {
 		description    string
@@ -2031,3 +2100,22 @@ func (p *fakePermissions) AuctionActivitiesAllowed(ctx context.Context, bidderCo
 		AllowBidRequest: true,
 	}, nil
 }
+
+func getDefaultActivityConfig(componentName string, allow bool) *config.AccountPrivacy {
+	return &config.AccountPrivacy{
+		AllowActivities: config.AllowActivities{
+			SyncUser: config.Activity{
+				Default: ptrutil.ToPtr(true),
+				Rules: []config.ActivityRule{
+					{
+						Allow: allow,
+						Condition: config.ActivityCondition{
+							ComponentName: []string{componentName},
+							ComponentType: []string{"bidder"},
+						},
+					},
+				},
+			},
+		},
+	}
+}
diff --git a/endpoints/setuid_test.go b/endpoints/setuid_test.go
index 65bc2655..05030cc7 100644
--- a/endpoints/setuid_test.go
+++ b/endpoints/setuid_test.go
@@ -277,6 +277,35 @@ func TestSetUIDEndpoint(t *testing.T) {
 			expectedBody:           "account is disabled, please reach out to the prebid server host",
 			description:            "Set uid for valid bidder with valid disabled account provided",
 		},
+		{
+			uri:                    "/setuid?bidder=pubmatic&uid=123&account=valid_acct_with_valid_activities_usersync_enabled",
+			syncersBidderNameToKey: map[string]string{"pubmatic": "pubmatic"},
+			existingSyncs:          nil,
+			gdprAllowsHostCookies:  true,
+			expectedSyncs:          map[string]string{"pubmatic": "123"},
+			expectedStatusCode:     http.StatusOK,
+			expectedHeaders:        map[string]string{"Content-Type": "text/html", "Content-Length": "0"},
+			description:            "Set uid for valid bidder with valid account provided with user sync allowed activity",
+		},
+		{
+			uri:                    "/setuid?bidder=pubmatic&uid=123&account=valid_acct_with_valid_activities_usersync_disabled",
+			syncersBidderNameToKey: map[string]string{"pubmatic": "pubmatic"},
+			existingSyncs:          nil,
+			gdprAllowsHostCookies:  true,
+			expectedSyncs:          nil,
+			expectedStatusCode:     http.StatusUnavailableForLegalReasons,
+			description:            "Set uid for valid bidder with valid account provided with user sync disallowed activity",
+		},
+		{
+			uri:                    "/setuid?bidder=pubmatic&uid=123&account=valid_acct_with_invalid_activities",
+			syncersBidderNameToKey: map[string]string{"pubmatic": "pubmatic"},
+			existingSyncs:          nil,
+			gdprAllowsHostCookies:  true,
+			expectedSyncs:          map[string]string{"pubmatic": "123"},
+			expectedStatusCode:     http.StatusOK,
+			expectedHeaders:        map[string]string{"Content-Type": "text/html", "Content-Length": "0"},
+			description:            "Set uid for valid bidder with valid account provided with invalid user sync activity",
+		},
 	}
 
 	analytics := analyticsConf.NewPBSAnalytics(&config.Analytics{})
@@ -1354,6 +1383,10 @@ func doRequest(req *http.Request, analytics analytics.PBSAnalyticsModule, metric
 		"disabled_acct":     json.RawMessage(`{"disabled":true}`),
 		"malformed_acct":    json.RawMessage(`{"disabled":"malformed"}`),
 		"invalid_json_acct": json.RawMessage(`{"}`),
+
+		"valid_acct_with_valid_activities_usersync_enabled":  json.RawMessage(`{"privacy":{"allowactivities":{"syncUser":{"default": true}}}}`),
+		"valid_acct_with_valid_activities_usersync_disabled": json.RawMessage(`{"privacy":{"allowactivities":{"syncUser":{"default": false}}}}`),
+		"valid_acct_with_invalid_activities":                 json.RawMessage(`{"privacy":{"allowactivities":{"syncUser":{"rules":[{"condition":{"componentName": ["bidderA.bidderB.bidderC"]}}]}}}}`),
 	}}
 
 	endpoint := NewSetUIDEndpoint(&cfg, syncersByBidder, gdprPermsBuilder, tcf2ConfigBuilder, analytics, fakeAccountsFetcher, metrics)
diff --git a/exchange/exchangetest/request-ext-prebid-filtering.json b/exchange/exchangetest/request-ext-prebid-filtering.json
index 499f779a..4355f517 100644
--- a/exchange/exchangetest/request-ext-prebid-filtering.json
+++ b/exchange/exchangetest/request-ext-prebid-filtering.json
@@ -60,6 +60,14 @@
                         "rubicon": {
                             "not": "permitted-for-appnexus"
                         }
+                    },
+                    "sdk": {
+                        "renderers": [
+                            {
+                                "name": "test-name",
+                                "version": "test-version"
+                            }
+                        ]
                     }
                 }
             }
@@ -108,6 +116,14 @@
                             "bidderparams": {
                                 "param1": 1,
                                 "paramA": "A"
+                            },
+                            "sdk": {
+                                "renderers": [
+                                    {
+                                        "name": "test-name",
+                                        "version": "test-version"
+                                    }
+                                ]
                             }
                         }
                     }
diff --git a/exchange/utils_test.go b/exchange/utils_test.go
index b62e9597..aff1b3c0 100644
--- a/exchange/utils_test.go
+++ b/exchange/utils_test.go
@@ -2287,6 +2287,7 @@ func TestCleanOpenRTBRequestsWithOpenRTBDowngrade(t *testing.T) {
 	bidReq.User.ID = ""
 	bidReq.User.BuyerUID = ""
 	bidReq.User.Yob = 0
+	bidReq.User.Geo = &openrtb2.Geo{Lat: 123.46}
 
 	downgradedRegs := *bidReq.Regs
 	downgradedUser := *bidReq.User
@@ -2383,14 +2384,14 @@ func TestBuildRequestExtForBidder(t *testing.T) {
 		{
 			description:  "Prebid - Allowed Fields Only",
 			bidderParams: nil,
-			requestExt:   json.RawMessage(`{"prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}}}`),
-			expectedJson: json.RawMessage(`{"prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}}}`),
+			requestExt:   json.RawMessage(`{"prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}, "sdk": {"renderers": [{"name": "r1"}]}}}`),
+			expectedJson: json.RawMessage(`{"prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}, "sdk": {"renderers": [{"name": "r1"}]}}}`),
 		},
 		{
 			description:  "Prebid - Allowed Fields + Bidder Params",
 			bidderParams: map[string]json.RawMessage{bidder: bidderParams},
-			requestExt:   json.RawMessage(`{"prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}}}`),
-			expectedJson: json.RawMessage(`{"prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}, "bidderparams":"bar"}}`),
+			requestExt:   json.RawMessage(`{"prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}, "sdk": {"renderers": [{"name": "r1"}]}}}`),
+			expectedJson: json.RawMessage(`{"prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}, "sdk": {"renderers": [{"name": "r1"}]}, "bidderparams":"bar"}}`),
 		},
 		{
 			description:  "Other",
@@ -2401,8 +2402,8 @@ func TestBuildRequestExtForBidder(t *testing.T) {
 		{
 			description:  "Prebid + Other + Bider Params",
 			bidderParams: map[string]json.RawMessage{bidder: bidderParams},
-			requestExt:   json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}}}`),
-			expectedJson: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}, "bidderparams":"bar"}}`),
+			requestExt:   json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}, "sdk": {"renderers": [{"name": "r1"}]}}}`),
+			expectedJson: json.RawMessage(`{"other":"foo","prebid":{"integration":"a","channel":{"name":"b","version":"c"},"debug":true,"currency":{"rates":{"FOO":{"BAR":42}},"usepbsrates":true}, "server": {"externalurl": "url", "gvlid": 1, "datacenter": "2"}, "sdk": {"renderers": [{"name": "r1"}]}, "bidderparams":"bar"}}`),
 		},
 		{
 			description:          "Prebid + AlternateBidderCodes in pbs config but current bidder not in AlternateBidderCodes config",
@@ -2601,6 +2602,7 @@ func newBidRequest(t *testing.T) *openrtb2.BidRequest {
 			BuyerUID: "their-id",
 			Yob:      1982,
 			Ext:      json.RawMessage(`{}`),
+			Geo:      &openrtb2.Geo{Lat: 123.456},
 		},
 		Imp: []openrtb2.Imp{{
 			ID: "some-imp-id",
@@ -4266,33 +4268,76 @@ func TestGetMediaTypeForBid(t *testing.T) {
 	}
 }
 
-func TemporarilyDisabledTestCleanOpenRTBRequestsActivitiesFetchBids(t *testing.T) {
+func TestCleanOpenRTBRequestsActivitiesFetchBids(t *testing.T) {
 	testCases := []struct {
-		name              string
-		req               *openrtb2.BidRequest
-		componentName     string
-		allow             bool
-		expectedReqNumber int
+		name                 string
+		req                  *openrtb2.BidRequest
+		privacyConfig        *config.AccountPrivacy
+		componentName        string
+		allow                bool
+		expectedReqNumber    int
+		expectedUserYOB      int64
+		expectedUserLat      float64
+		expectedDeviceDIDMD5 string
 	}{
 		{
-			name:              "request_with_one_bidder_allowed",
-			req:               newBidRequest(t),
-			componentName:     "appnexus",
-			allow:             true,
-			expectedReqNumber: 1,
-		},
-		{
-			name:              "request_with_one_bidder_not_allowed",
-			req:               newBidRequest(t),
-			componentName:     "appnexus",
-			allow:             false,
-			expectedReqNumber: 0,
+			name:                 "fetch_bids_request_with_one_bidder_allowed",
+			req:                  newBidRequest(t),
+			privacyConfig:        getFetchBidsActivityConfig("appnexus", true),
+			expectedReqNumber:    1,
+			expectedUserYOB:      1982,
+			expectedUserLat:      123.456,
+			expectedDeviceDIDMD5: "some device ID hash",
+		},
+		{
+			name:                 "fetch_bids_request_with_one_bidder_not_allowed",
+			req:                  newBidRequest(t),
+			privacyConfig:        getFetchBidsActivityConfig("appnexus", false),
+			expectedReqNumber:    0,
+			expectedUserYOB:      1982,
+			expectedUserLat:      123.456,
+			expectedDeviceDIDMD5: "some device ID hash",
+		},
+		{
+			name:                 "transmit_ufpd_allowed",
+			req:                  newBidRequest(t),
+			privacyConfig:        getTransmitUFPDActivityConfig("appnexus", true),
+			expectedReqNumber:    1,
+			expectedUserYOB:      1982,
+			expectedUserLat:      123.456,
+			expectedDeviceDIDMD5: "some device ID hash",
+		},
+		{
+			name:                 "transmit_ufpd_deny",
+			req:                  newBidRequest(t),
+			privacyConfig:        getTransmitUFPDActivityConfig("appnexus", false),
+			expectedReqNumber:    1,
+			expectedUserYOB:      0,
+			expectedUserLat:      123.456,
+			expectedDeviceDIDMD5: "",
+		},
+		{
+			name:                 "transmit_precise_geo_allowed",
+			req:                  newBidRequest(t),
+			privacyConfig:        getTransmitPreciseGeoActivityConfig("appnexus", true),
+			expectedReqNumber:    1,
+			expectedUserYOB:      1982,
+			expectedUserLat:      123.456,
+			expectedDeviceDIDMD5: "some device ID hash",
+		},
+		{
+			name:                 "transmit_precise_geo_deny",
+			req:                  newBidRequest(t),
+			privacyConfig:        getTransmitPreciseGeoActivityConfig("appnexus", false),
+			expectedReqNumber:    1,
+			expectedUserYOB:      1982,
+			expectedUserLat:      123.46,
+			expectedDeviceDIDMD5: "some device ID hash",
 		},
 	}
 
 	for _, test := range testCases {
-		privacyConfig := getDefaultActivityConfig(test.componentName, test.allow)
-		activities, err := privacy.NewActivityControl(privacyConfig)
+		activities, err := privacy.NewActivityControl(test.privacyConfig)
 		assert.NoError(t, err, "")
 		auctionReq := AuctionRequest{
 			BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: test.req},
@@ -4312,25 +4357,51 @@ func TemporarilyDisabledTestCleanOpenRTBRequestsActivitiesFetchBids(t *testing.T
 			bidderRequests, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo)
 			assert.Empty(t, errs)
 			assert.Len(t, bidderRequests, test.expectedReqNumber)
+
+			if test.expectedReqNumber == 1 {
+				assert.Equal(t, test.expectedUserYOB, bidderRequests[0].BidRequest.User.Yob)
+				assert.Equal(t, test.expectedUserLat, bidderRequests[0].BidRequest.User.Geo.Lat)
+				assert.Equal(t, test.expectedDeviceDIDMD5, bidderRequests[0].BidRequest.Device.DIDMD5)
+			}
 		})
 	}
 }
 
-func getDefaultActivityConfig(componentName string, allow bool) *config.AccountPrivacy {
-	return &config.AccountPrivacy{
-		AllowActivities: config.AllowActivities{
-			FetchBids: config.Activity{
-				Default: ptrutil.ToPtr(true),
-				Rules: []config.ActivityRule{
-					{
-						Allow: allow,
-						Condition: config.ActivityCondition{
-							ComponentName: []string{componentName},
-							ComponentType: []string{"bidder"},
-						},
-					},
+func buildDefaultActivityConfig(componentName string, allow bool) config.Activity {
+	return config.Activity{
+		Default: ptrutil.ToPtr(true),
+		Rules: []config.ActivityRule{
+			{
+				Allow: allow,
+				Condition: config.ActivityCondition{
+					ComponentName: []string{componentName},
+					ComponentType: []string{"bidder"},
 				},
 			},
 		},
 	}
 }
+
+func getFetchBidsActivityConfig(componentName string, allow bool) *config.AccountPrivacy {
+	return &config.AccountPrivacy{
+		AllowActivities: config.AllowActivities{
+			FetchBids: buildDefaultActivityConfig(componentName, allow),
+		},
+	}
+}
+
+func getTransmitUFPDActivityConfig(componentName string, allow bool) *config.AccountPrivacy {
+	return &config.AccountPrivacy{
+		AllowActivities: config.AllowActivities{
+			TransmitUserFPD: buildDefaultActivityConfig(componentName, allow),
+		},
+	}
+}
+
+func getTransmitPreciseGeoActivityConfig(componentName string, allow bool) *config.AccountPrivacy {
+	return &config.AccountPrivacy{
+		AllowActivities: config.AllowActivities{
+			TransmitPreciseGeo: buildDefaultActivityConfig(componentName, allow),
+		},
+	}
+}
diff --git a/privacy/enforcement_test.go b/privacy/enforcement_test.go
index a9d7ca92..be1ab0dc 100644
--- a/privacy/enforcement_test.go
+++ b/privacy/enforcement_test.go
@@ -8,7 +8,7 @@ import (
 	"github.com/stretchr/testify/mock"
 )
 
-func TestAny(t *testing.T) {
+func TestAnyLegacy(t *testing.T) {
 	testCases := []struct {
 		enforcement Enforcement
 		expected    bool
@@ -50,12 +50,12 @@ func TestAny(t *testing.T) {
 	}
 
 	for _, test := range testCases {
-		result := test.enforcement.Any()
+		result := test.enforcement.AnyLegacy()
 		assert.Equal(t, test.expected, result, test.description)
 	}
 }
 
-func TestApply(t *testing.T) {
+func TestApplyGDPR(t *testing.T) {
 	testCases := []struct {
 		description        string
 		enforcement        Enforcement
@@ -216,6 +216,130 @@ func TestApply(t *testing.T) {
 	}
 }
 
+func TestApplyToggle(t *testing.T) {
+	testCases := []struct {
+		description                  string
+		enforcement                  Enforcement
+		expectedScrubRequestExecuted bool
+		expectedScrubUserExecuted    bool
+		expectedScrubDeviceExecuted  bool
+	}{
+		{
+			description: "All enforced - only ScrubRequest execution expected",
+			enforcement: Enforcement{
+				CCPA:       true,
+				COPPA:      true,
+				GDPRGeo:    true,
+				GDPRID:     true,
+				LMT:        true,
+				UFPD:       true,
+				Eids:       true,
+				PreciseGeo: true,
+				TID:        true,
+			},
+			expectedScrubRequestExecuted: true,
+			expectedScrubUserExecuted:    false,
+			expectedScrubDeviceExecuted:  false,
+		},
+		{
+			description: "All Legacy and no activities - ScrubUser and ScrubDevice execution expected",
+			enforcement: Enforcement{
+				CCPA:       true,
+				COPPA:      true,
+				GDPRGeo:    true,
+				GDPRID:     true,
+				LMT:        true,
+				UFPD:       false,
+				Eids:       false,
+				PreciseGeo: false,
+				TID:        false,
+			},
+			expectedScrubRequestExecuted: false,
+			expectedScrubUserExecuted:    true,
+			expectedScrubDeviceExecuted:  true,
+		},
+		{
+			description: "Some Legacy and some activities - ScrubRequest, ScrubUser and ScrubDevice execution expected",
+			enforcement: Enforcement{
+				CCPA:       true,
+				COPPA:      true,
+				GDPRGeo:    true,
+				GDPRID:     true,
+				LMT:        true,
+				UFPD:       true,
+				Eids:       false,
+				PreciseGeo: false,
+				TID:        false,
+			},
+			expectedScrubRequestExecuted: true,
+			expectedScrubUserExecuted:    true,
+			expectedScrubDeviceExecuted:  true,
+		},
+		{
+			description: "Some Legacy and some activities - ScrubRequest execution expected",
+			enforcement: Enforcement{
+				CCPA:       true,
+				COPPA:      true,
+				GDPRGeo:    true,
+				GDPRID:     true,
+				LMT:        true,
+				UFPD:       true,
+				Eids:       true,
+				PreciseGeo: true,
+				TID:        false,
+			},
+			expectedScrubRequestExecuted: true,
+			expectedScrubUserExecuted:    false,
+			expectedScrubDeviceExecuted:  false,
+		},
+		{
+			description: "Some Legacy and some activities overlap - ScrubRequest and ScrubUser execution expected",
+			enforcement: Enforcement{
+				CCPA:       true,
+				COPPA:      true,
+				GDPRGeo:    true,
+				GDPRID:     true,
+				LMT:        true,
+				UFPD:       true,
+				Eids:       false,
+				PreciseGeo: true,
+				TID:        false,
+			},
+			expectedScrubRequestExecuted: true,
+			expectedScrubUserExecuted:    true,
+			expectedScrubDeviceExecuted:  false,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.description, func(t *testing.T) {
+			req := &openrtb2.BidRequest{
+				Device: &openrtb2.Device{},
+				User:   &openrtb2.User{},
+			}
+			replacedDevice := &openrtb2.Device{}
+			replacedUser := &openrtb2.User{}
+
+			m := &mockScrubber{}
+
+			if test.expectedScrubRequestExecuted {
+				m.On("ScrubRequest", req, test.enforcement).Return(req).Once()
+			}
+			if test.expectedScrubUserExecuted {
+				m.On("ScrubUser", req.User, ScrubStrategyUserIDAndDemographic, ScrubStrategyGeoFull).Return(replacedUser).Once()
+			}
+			if test.expectedScrubDeviceExecuted {
+				m.On("ScrubDevice", req.Device, ScrubStrategyDeviceIDAll, ScrubStrategyIPV4Lowest8, ScrubStrategyIPV6Lowest32, ScrubStrategyGeoFull).Return(replacedDevice).Once()
+			}
+
+			test.enforcement.apply(req, m)
+
+			m.AssertExpectations(t)
+
+		})
+	}
+}
+
 func TestApplyNoneApplicable(t *testing.T) {
 	req := &openrtb2.BidRequest{}
 
@@ -227,6 +351,11 @@ func TestApplyNoneApplicable(t *testing.T) {
 		GDPRGeo: false,
 		GDPRID:  false,
 		LMT:     false,
+
+		UFPD:       false,
+		PreciseGeo: false,
+		TID:        false,
+		Eids:       false,
 	}
 	enforcement.apply(req, m)
 
@@ -248,6 +377,11 @@ type mockScrubber struct {
 	mock.Mock
 }
 
+func (m *mockScrubber) ScrubRequest(bidRequest *openrtb2.BidRequest, enforcement Enforcement) *openrtb2.BidRequest {
+	args := m.Called(bidRequest, enforcement)
+	return args.Get(0).(*openrtb2.BidRequest)
+}
+
 func (m *mockScrubber) ScrubDevice(device *openrtb2.Device, id ScrubStrategyDeviceID, ipv4 ScrubStrategyIPV4, ipv6 ScrubStrategyIPV6, geo ScrubStrategyGeo) *openrtb2.Device {
 	args := m.Called(device, id, ipv4, ipv6, geo)
 	return args.Get(0).(*openrtb2.Device)
diff --git a/privacy/enforcer_test.go b/privacy/enforcer_test.go
index e87a9eb2..2b9715f8 100644
--- a/privacy/enforcer_test.go
+++ b/privacy/enforcer_test.go
@@ -8,7 +8,7 @@ import (
 	"testing"
 )
 
-func TemporarilyDisabledTestNewActivityControl(t *testing.T) {
+func TestNewActivityControl(t *testing.T) {
 
 	testCases := []struct {
 		name            string
diff --git a/privacy/scrubber_test.go b/privacy/scrubber_test.go
index 7b4afac2..0691b2c7 100644
--- a/privacy/scrubber_test.go
+++ b/privacy/scrubber_test.go
@@ -9,24 +9,7 @@ import (
 )
 
 func TestScrubDevice(t *testing.T) {
-	device := &openrtb2.Device{
-		DIDMD5:   "anyDIDMD5",
-		DIDSHA1:  "anyDIDSHA1",
-		DPIDMD5:  "anyDPIDMD5",
-		DPIDSHA1: "anyDPIDSHA1",
-		MACSHA1:  "anyMACSHA1",
-		MACMD5:   "anyMACMD5",
-		IFA:      "anyIFA",
-		IP:       "1.2.3.4",
-		IPv6:     "2001:0db8:0000:0000:0000:ff00:0042:8329",
-		Geo: &openrtb2.Geo{
-			Lat:   123.456,
-			Lon:   678.89,
-			Metro: "some metro",
-			City:  "some city",
-			ZIP:   "some zip",
-		},
-	}
+	device := getTestDevice()
 
 	testCases := []struct {
 		description string
@@ -197,20 +180,7 @@ func TestScrubDeviceNil(t *testing.T) {
 }
 
 func TestScrubUser(t *testing.T) {
-	user := &openrtb2.User{
-		ID:       "anyID",
-		BuyerUID: "anyBuyerUID",
-		Yob:      42,
-		Gender:   "anyGender",
-		Ext:      json.RawMessage(`{}`),
-		Geo: &openrtb2.Geo{
-			Lat:   123.456,
-			Lon:   678.89,
-			Metro: "some metro",
-			City:  "some city",
-			ZIP:   "some zip",
-		},
-	}
+	user := getTestUser()
 
 	testCases := []struct {
 		description string
@@ -333,6 +303,171 @@ func TestScrubUserNil(t *testing.T) {
 	assert.Nil(t, result)
 }
 
+func TestScrubRequest(t *testing.T) {
+
+	imps := []openrtb2.Imp{
+		{ID: "testId", Ext: json.RawMessage(`{"test": 1, "tid": 2}`)},
+	}
+	source := &openrtb2.Source{
+		TID: "testTid",
+	}
+	device := getTestDevice()
+	user := getTestUser()
+	user.Ext = json.RawMessage(`{"data": 1, "eids": 2}`)
+	user.EIDs = []openrtb2.EID{{Source: "test"}}
+
+	testCases := []struct {
+		description    string
+		enforcement    Enforcement
+		userExtPresent bool
+		expected       *openrtb2.BidRequest
+	}{
+		{
+			description:    "enforce transmitUFPD with user.ext",
+			enforcement:    Enforcement{UFPD: true},
+			userExtPresent: true,
+			expected: &openrtb2.BidRequest{
+				Imp:    imps,
+				Source: source,
+				User: &openrtb2.User{
+					EIDs: []openrtb2.EID{{Source: "test"}},
+					Geo:  user.Geo,
+					Ext:  json.RawMessage(`{"eids":2}`),
+				},
+				Device: &openrtb2.Device{
+					IP:   "1.2.3.4",
+					IPv6: "2001:0db8:0000:0000:0000:ff00:0042:8329",
+					Geo:  device.Geo,
+				},
+			},
+		},
+		{
+			description:    "enforce transmitUFPD without user.ext",
+			enforcement:    Enforcement{UFPD: true},
+			userExtPresent: false,
+			expected: &openrtb2.BidRequest{
+				Imp:    imps,
+				Source: source,
+				User: &openrtb2.User{
+					EIDs: []openrtb2.EID{{Source: "test"}},
+					Geo:  user.Geo,
+				},
+				Device: &openrtb2.Device{
+					IP:   "1.2.3.4",
+					IPv6: "2001:0db8:0000:0000:0000:ff00:0042:8329",
+					Geo:  device.Geo,
+				},
+			},
+		},
+		{
+			description:    "enforce transmitEids",
+			enforcement:    Enforcement{Eids: true},
+			userExtPresent: true,
+			expected: &openrtb2.BidRequest{
+				Imp:    imps,
+				Source: source,
+				Device: device,
+				User: &openrtb2.User{
+					ID:       "anyID",
+					BuyerUID: "anyBuyerUID",
+					Yob:      42,
+					Gender:   "anyGender",
+					Geo:      user.Geo,
+					EIDs:     nil,
+					Ext:      json.RawMessage(`{"data":1}`),
+				},
+			},
+		},
+		{
+			description:    "enforce transmitTid",
+			enforcement:    Enforcement{TID: true},
+			userExtPresent: true,
+			expected: &openrtb2.BidRequest{
+				Imp: []openrtb2.Imp{
+					{ID: "testId", Ext: json.RawMessage(`{"test":1}`)},
+				},
+				Source: &openrtb2.Source{
+					TID: "",
+				},
+				Device: device,
+				User: &openrtb2.User{
+					ID:       "anyID",
+					BuyerUID: "anyBuyerUID",
+					Yob:      42,
+					Gender:   "anyGender",
+					Geo:      user.Geo,
+					EIDs:     []openrtb2.EID{{Source: "test"}},
+					Ext:      json.RawMessage(`{"data": 1, "eids": 2}`),
+				},
+			},
+		},
+		{
+			description:    "enforce precise Geo",
+			enforcement:    Enforcement{PreciseGeo: true},
+			userExtPresent: true,
+			expected: &openrtb2.BidRequest{
+				Imp:    imps,
+				Source: source,
+				User: &openrtb2.User{
+					ID:       "anyID",
+					BuyerUID: "anyBuyerUID",
+					Yob:      42,
+					Gender:   "anyGender",
+					Geo: &openrtb2.Geo{
+						Lat: 123.46, Lon: 678.89,
+						Metro: "some metro",
+						City:  "some city",
+						ZIP:   "some zip",
+					},
+					EIDs: []openrtb2.EID{{Source: "test"}},
+					Ext:  json.RawMessage(`{"data": 1, "eids": 2}`),
+				},
+				Device: &openrtb2.Device{
+					IFA:      "anyIFA",
+					DIDSHA1:  "anyDIDSHA1",
+					DIDMD5:   "anyDIDMD5",
+					DPIDSHA1: "anyDPIDSHA1",
+					DPIDMD5:  "anyDPIDMD5",
+					MACSHA1:  "anyMACSHA1",
+					MACMD5:   "anyMACMD5",
+					IP:       "1.2.3.0",
+					IPv6:     "2001:0db8:0000:0000:0000:ff00:0:0",
+					Geo: &openrtb2.Geo{
+						Lat: 123.46, Lon: 678.89,
+						Metro: "some metro",
+						City:  "some city",
+						ZIP:   "some zip",
+					},
+				},
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.description, func(t *testing.T) {
+			bidRequest := &openrtb2.BidRequest{
+				Imp: []openrtb2.Imp{
+					{ID: "testId", Ext: json.RawMessage(`{"test": 1, "tid": 2}`)},
+				},
+				Source: &openrtb2.Source{
+					TID: "testTid",
+				},
+				User:   getTestUser(),
+				Device: getTestDevice(),
+			}
+			if test.userExtPresent {
+				bidRequest.User.Ext = json.RawMessage(`{"data": 1, "eids": 2}`)
+			} else {
+				bidRequest.User.Ext = nil
+			}
+			bidRequest.User.EIDs = []openrtb2.EID{{Source: "test"}}
+
+			result := NewScrubber().ScrubRequest(bidRequest, test.enforcement)
+			assert.Equal(t, test.expected, result, test.description)
+		})
+	}
+}
+
 func TestScrubIPV4(t *testing.T) {
 	testCases := []struct {
 		IP          string
@@ -572,7 +707,46 @@ func TestScrubUserExtIDs(t *testing.T) {
 	}
 
 	for _, test := range testCases {
-		result := scrubUserExtIDs(test.userExt)
+		result := scrubExtIDs(test.userExt, "eids")
 		assert.Equal(t, test.expected, result, test.description)
 	}
 }
+
+func getTestUser() *openrtb2.User {
+	return &openrtb2.User{
+		ID:       "anyID",
+		BuyerUID: "anyBuyerUID",
+		Yob:      42,
+		Gender:   "anyGender",
+		Ext:      json.RawMessage(`{}`),
+		Geo: &openrtb2.Geo{
+			Lat:   123.456,
+			Lon:   678.89,
+			Metro: "some metro",
+			City:  "some city",
+			ZIP:   "some zip",
+		},
+	}
+}
+
+func getTestDevice() *openrtb2.Device {
+	return &openrtb2.Device{
+		DIDMD5:   "anyDIDMD5",
+		DIDSHA1:  "anyDIDSHA1",
+		DPIDMD5:  "anyDPIDMD5",
+		DPIDSHA1: "anyDPIDSHA1",
+		MACSHA1:  "anyMACSHA1",
+		MACMD5:   "anyMACMD5",
+		IFA:      "anyIFA",
+		IP:       "1.2.3.4",
+		IPv6:     "2001:0db8:0000:0000:0000:ff00:0042:8329",
+		Geo: &openrtb2.Geo{
+			Lat:   123.456,
+			Lon:   678.89,
+			Metro: "some metro",
+			City:  "some city",
+			ZIP:   "some zip",
+		},
+	}
+
+}
diff --git a/usersync/chooser_test.go b/usersync/chooser_test.go
index 0071a2e3..adc217ae 100644
--- a/usersync/chooser_test.go
+++ b/usersync/chooser_test.go
@@ -1,12 +1,11 @@
 package usersync
 
 import (
-	"testing"
-	"time"
-
 	"github.com/prebid/prebid-server/privacy"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
+	"testing"
+	"time"
 )
 
 func TestNewChooser(t *testing.T) {
@@ -66,7 +65,7 @@ func TestChooserChoose(t *testing.T) {
 		{
 			description: "Cookie Opt Out",
 			givenRequest: Request{
-				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
+				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
 				Limit:   0,
 			},
 			givenChosenBidders: []string{"a"},
@@ -80,7 +79,7 @@ func TestChooserChoose(t *testing.T) {
 		{
 			description: "GDPR Host Cookie Not Allowed",
 			givenRequest: Request{
-				Privacy: fakePrivacy{gdprAllowsHostCookie: false, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
+				Privacy: fakePrivacy{gdprAllowsHostCookie: false, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
 				Limit:   0,
 			},
 			givenChosenBidders: []string{"a"},
@@ -94,7 +93,7 @@ func TestChooserChoose(t *testing.T) {
 		{
 			description: "No Bidders",
 			givenRequest: Request{
-				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
+				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
 				Limit:   0,
 			},
 			givenChosenBidders: []string{},
@@ -108,7 +107,7 @@ func TestChooserChoose(t *testing.T) {
 		{
 			description: "One Bidder - Sync",
 			givenRequest: Request{
-				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
+				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
 				Limit:   0,
 			},
 			givenChosenBidders: []string{"a"},
@@ -122,7 +121,7 @@ func TestChooserChoose(t *testing.T) {
 		{
 			description: "One Bidder - No Sync",
 			givenRequest: Request{
-				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
+				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
 				Limit:   0,
 			},
 			givenChosenBidders: []string{"c"},
@@ -136,7 +135,7 @@ func TestChooserChoose(t *testing.T) {
 		{
 			description: "Many Bidders - All Sync - Limit Disabled With 0",
 			givenRequest: Request{
-				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
+				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
 				Limit:   0,
 			},
 			givenChosenBidders: []string{"a", "b"},
@@ -150,7 +149,7 @@ func TestChooserChoose(t *testing.T) {
 		{
 			description: "Many Bidders - All Sync - Limit Disabled With Negative Value",
 			givenRequest: Request{
-				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
+				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
 				Limit:   -1,
 			},
 			givenChosenBidders: []string{"a", "b"},
@@ -164,7 +163,7 @@ func TestChooserChoose(t *testing.T) {
 		{
 			description: "Many Bidders - Limited Sync",
 			givenRequest: Request{
-				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
+				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
 				Limit:   1,
 			},
 			givenChosenBidders: []string{"a", "b"},
@@ -178,7 +177,7 @@ func TestChooserChoose(t *testing.T) {
 		{
 			description: "Many Bidders - Limited Sync - Disqualified Syncers Don't Count Towards Limit",
 			givenRequest: Request{
-				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
+				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
 				Limit:   1,
 			},
 			givenChosenBidders: []string{"c", "a", "b"},
@@ -192,7 +191,7 @@ func TestChooserChoose(t *testing.T) {
 		{
 			description: "Many Bidders - Some Sync, Some Don't",
 			givenRequest: Request{
-				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
+				Privacy: fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
 				Limit:   0,
 			},
 			givenChosenBidders: []string{"a", "c"},
@@ -254,7 +253,7 @@ func TestChooserEvaluate(t *testing.T) {
 			description:        "Valid",
 			givenBidder:        "a",
 			givenSyncersSeen:   map[string]struct{}{},
-			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
+			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
 			givenCookie:        cookieNeedsSync,
 			expectedSyncer:     fakeSyncerA,
 			expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusOK},
@@ -263,7 +262,7 @@ func TestChooserEvaluate(t *testing.T) {
 			description:        "Unknown Bidder",
 			givenBidder:        "unknown",
 			givenSyncersSeen:   map[string]struct{}{},
-			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
+			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
 			givenCookie:        cookieNeedsSync,
 			expectedSyncer:     nil,
 			expectedEvaluation: BidderEvaluation{Bidder: "unknown", Status: StatusUnknownBidder},
@@ -272,7 +271,7 @@ func TestChooserEvaluate(t *testing.T) {
 			description:        "Duplicate Syncer",
 			givenBidder:        "a",
 			givenSyncersSeen:   map[string]struct{}{"keyA": {}},
-			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
+			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
 			givenCookie:        cookieNeedsSync,
 			expectedSyncer:     nil,
 			expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusDuplicate},
@@ -281,7 +280,7 @@ func TestChooserEvaluate(t *testing.T) {
 			description:        "Incompatible Kind",
 			givenBidder:        "b",
 			givenSyncersSeen:   map[string]struct{}{},
-			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
+			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
 			givenCookie:        cookieNeedsSync,
 			expectedSyncer:     nil,
 			expectedEvaluation: BidderEvaluation{Bidder: "b", SyncerKey: "keyB", Status: StatusTypeNotSupported},
@@ -290,7 +289,7 @@ func TestChooserEvaluate(t *testing.T) {
 			description:        "Already Synced",
 			givenBidder:        "a",
 			givenSyncersSeen:   map[string]struct{}{},
-			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
+			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
 			givenCookie:        cookieAlreadyHasSyncForA,
 			expectedSyncer:     nil,
 			expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusAlreadySynced},
@@ -299,7 +298,7 @@ func TestChooserEvaluate(t *testing.T) {
 			description:        "Different Bidder Already Synced",
 			givenBidder:        "a",
 			givenSyncersSeen:   map[string]struct{}{},
-			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true},
+			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
 			givenCookie:        cookieAlreadyHasSyncForB,
 			expectedSyncer:     fakeSyncerA,
 			expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusOK},
@@ -308,7 +307,7 @@ func TestChooserEvaluate(t *testing.T) {
 			description:        "Blocked By GDPR",
 			givenBidder:        "a",
 			givenSyncersSeen:   map[string]struct{}{},
-			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: false, ccpaAllowsBidderSync: true},
+			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: false, ccpaAllowsBidderSync: true, activityAllowUserSync: true},
 			givenCookie:        cookieNeedsSync,
 			expectedSyncer:     nil,
 			expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByGDPR},
@@ -317,11 +316,20 @@ func TestChooserEvaluate(t *testing.T) {
 			description:        "Blocked By CCPA",
 			givenBidder:        "a",
 			givenSyncersSeen:   map[string]struct{}{},
-			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: false},
+			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: false, activityAllowUserSync: true},
 			givenCookie:        cookieNeedsSync,
 			expectedSyncer:     nil,
 			expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByCCPA},
 		},
+		{
+			description:        "Blocked By activity control",
+			givenBidder:        "a",
+			givenSyncersSeen:   map[string]struct{}{},
+			givenPrivacy:       fakePrivacy{gdprAllowsHostCookie: true, gdprAllowsBidderSync: true, ccpaAllowsBidderSync: true, activityAllowUserSync: false},
+			givenCookie:        cookieNeedsSync,
+			expectedSyncer:     nil,
+			expectedEvaluation: BidderEvaluation{Bidder: "a", SyncerKey: "keyA", Status: StatusBlockedByPrivacy},
+		},
 	}
 
 	for _, test := range testCases {
@@ -373,9 +381,10 @@ func (fakeSyncer) GetSync(syncTypes []SyncType, privacyPolicies privacy.Policies
 }
 
 type fakePrivacy struct {
-	gdprAllowsHostCookie bool
-	gdprAllowsBidderSync bool
-	ccpaAllowsBidderSync bool
+	gdprAllowsHostCookie  bool
+	gdprAllowsBidderSync  bool
+	ccpaAllowsBidderSync  bool
+	activityAllowUserSync bool
 }
 
 func (p fakePrivacy) GDPRAllowsHostCookie() bool {
@@ -389,3 +398,7 @@ func (p fakePrivacy) GDPRAllowsBidderSync(bidder string) bool {
 func (p fakePrivacy) CCPAAllowsBidderSync(bidder string) bool {
 	return p.ccpaAllowsBidderSync
 }
+
+func (p fakePrivacy) ActivityAllowsUserSync(bidder string) bool {
+	return p.activityAllowUserSync
+}