Repository: prebid/prebid-server
Pull request #3540 (2024-02-25 16:53:13)

DSA: Inject default in bid requests if not present
Implements #3424


Patch:
diff --git a/account/account.go b/account/account.go
index 37d36761..b40d9a88 100644
--- a/account/account.go
+++ b/account/account.go
@@ -49,6 +49,11 @@ func GetAccount(ctx context.Context, cfg *config.Configuration, fetcher stored_r
 				Message: fmt.Sprintf("The prebid-server account config for account id \"%s\" is malformed. Please reach out to the prebid server host.", accountID),
 			}}
 		}
+		if err := config.UnpackDSADefault(account.Privacy.DSA); err != nil {
+			return nil, []error{&errortypes.MalformedAcct{
+				Message: fmt.Sprintf("The prebid-server account config DSA for account id \"%s\" is malformed. Please reach out to the prebid server host.", accountID),
+			}}
+		}
 
 		// Fill in ID if needed, so it can be left out of account definition
 		if len(account.ID) == 0 {
diff --git a/adapters/smrtconnect/smrtconnect.go b/adapters/smrtconnect/smrtconnect.go
new file mode 100644
index 00000000..dc7bf254
--- /dev/null
+++ b/adapters/smrtconnect/smrtconnect.go
@@ -0,0 +1,144 @@
+package smrtconnect
+
+import (
+	"encoding/json"
+	"fmt"
+	"text/template"
+
+	"github.com/prebid/openrtb/v20/openrtb2"
+	"github.com/prebid/prebid-server/v2/adapters"
+	"github.com/prebid/prebid-server/v2/config"
+	"github.com/prebid/prebid-server/v2/errortypes"
+	"github.com/prebid/prebid-server/v2/macros"
+	"github.com/prebid/prebid-server/v2/openrtb_ext"
+)
+
+type adapter struct {
+	endpoint *template.Template
+}
+
+// Builder builds a new instance of the Smrtconnect adapter for the given bidder with the given config.
+func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
+	template, err := template.New("endpointTemplate").Parse(config.Endpoint)
+	if err != nil {
+		return nil, fmt.Errorf("unable to parse endpoint url template: %v", err)
+	}
+
+	bidder := &adapter{
+		endpoint: template,
+	}
+	return bidder, nil
+}
+
+func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	var requests []*adapters.RequestData
+
+	requestCopy := *request
+	for _, imp := range request.Imp {
+		smrtconnectExt, err := getImpressionExt(&imp)
+		if err != nil {
+			return nil, []error{err}
+		}
+
+		url, err := a.buildEndpointURL(smrtconnectExt)
+		if err != nil {
+			return nil, []error{err}
+		}
+
+		requestCopy.Imp = []openrtb2.Imp{imp}
+		requestJSON, err := json.Marshal(requestCopy)
+		if err != nil {
+			return nil, []error{err}
+		}
+
+		requestData := &adapters.RequestData{
+			Method: "POST",
+			Uri:    url,
+			Body:   requestJSON,
+		}
+		requests = append(requests, requestData)
+	}
+	return requests, nil
+}
+
+func getImpressionExt(imp *openrtb2.Imp) (*openrtb_ext.ExtSmrtconnect, error) {
+	var bidderExt adapters.ExtImpBidder
+	if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
+		return nil, &errortypes.BadInput{
+			Message: "ext.bidder not provided",
+		}
+	}
+	var smrtconnectExt openrtb_ext.ExtSmrtconnect
+	if err := json.Unmarshal(bidderExt.Bidder, &smrtconnectExt); err != nil {
+		return nil, &errortypes.BadInput{
+			Message: "ext.bidder not provided",
+		}
+	}
+	imp.Ext = nil
+	return &smrtconnectExt, nil
+}
+
+func (a *adapter) buildEndpointURL(params *openrtb_ext.ExtSmrtconnect) (string, error) {
+	endpointParams := macros.EndpointTemplateParams{SupplyId: params.SupplyId}
+	return macros.ResolveMacros(a.endpoint, endpointParams)
+}
+
+func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *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{&errortypes.BadServerResponse{
+			Message: "Bad Server Response",
+		}}
+	}
+
+	if len(response.SeatBid) == 0 {
+		return nil, []error{&errortypes.BadServerResponse{
+			Message: "Empty SeatBid array",
+		}}
+	}
+
+	var bidErrs []error
+	bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
+	bidResponse.Currency = response.Cur
+	for _, seatBid := range response.SeatBid {
+		for i := range seatBid.Bid {
+			bidType, err := getBidType(seatBid.Bid[i])
+			if err != nil {
+				// could not determinate media type, append an error and continue with the next bid.
+				bidErrs = append(bidErrs, err)
+				continue
+			}
+			bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
+				Bid:     &seatBid.Bid[i],
+				BidType: bidType,
+			})
+		}
+	}
+	return bidResponse, bidErrs
+}
+
+func getBidType(bid openrtb2.Bid) (openrtb_ext.BidType, error) {
+	// determinate media type by bid response field mtype
+	switch bid.MType {
+	case openrtb2.MarkupBanner:
+		return openrtb_ext.BidTypeBanner, nil
+	case openrtb2.MarkupVideo:
+		return openrtb_ext.BidTypeVideo, nil
+	case openrtb2.MarkupAudio:
+		return openrtb_ext.BidTypeAudio, nil
+	case openrtb2.MarkupNative:
+		return openrtb_ext.BidTypeNative, nil
+	}
+
+	return "", &errortypes.BadInput{
+		Message: fmt.Sprintf("Could not define media type for impression: %s", bid.ImpID),
+	}
+}
diff --git a/config/account.go b/config/account.go
index 72b6c07a..80ce6d41 100644
--- a/config/account.go
+++ b/config/account.go
@@ -335,12 +335,14 @@ func (m AccountModules) ModuleConfig(id string) (json.RawMessage, error) {
 
 type AccountPrivacy struct {
 	AllowActivities *AllowActivities `mapstructure:"allowactivities" json:"allowactivities"`
+	DSA             *AccountDSA      `mapstructure:"dsa" json:"dsa"`
 	IPv6Config      IPv6             `mapstructure:"ipv6" json:"ipv6"`
 	IPv4Config      IPv4             `mapstructure:"ipv4" json:"ipv4"`
 	PrivacySandbox  PrivacySandbox   `mapstructure:"privacysandbox" json:"privacysandbox"`
 }
 
 type PrivacySandbox struct {
+	TopicsDomain      string            `mapstructure:"topicsdomain"`
 	CookieDeprecation CookieDeprecation `mapstructure:"cookiedeprecation"`
 }
 
@@ -349,6 +351,13 @@ type CookieDeprecation struct {
 	TTLSec  int  `mapstructure:"ttl_sec"`
 }
 
+// AccountDSA represents DSA configuration
+type AccountDSA struct {
+	Default         string `mapstructure:"default" json:"default"`
+	DefaultUnpacked *openrtb_ext.ExtRegsDSA
+	GDPROnly        bool `mapstructure:"gdpr_only" json:"gdpr_only"`
+}
+
 type IPv6 struct {
 	AnonKeepBits int `mapstructure:"anon_keep_bits" json:"anon_keep_bits"`
 }
diff --git a/config/config.go b/config/config.go
index b2204053..86521c4c 100644
--- a/config/config.go
+++ b/config/config.go
@@ -721,6 +721,10 @@ func New(v *viper.Viper, bidderInfos BidderInfos, normalizeBidderName func(strin
 		return nil, err
 	}
 
+	if err := UnpackDSADefault(c.AccountDefaults.Privacy.DSA); err != nil {
+		return nil, fmt.Errorf("invalid default account DSA: %v", err)
+	}
+
 	// Update account defaults and generate base json for patch
 	c.AccountDefaults.CacheTTL = c.CacheURL.DefaultTTLs // comment this out to set explicitly in config
 
@@ -851,6 +855,14 @@ func (cfg *Configuration) MarshalAccountDefaults() error {
 	return err
 }
 
+// UnpackDSADefault validates the JSON DSA default object string by unmarshaling and maps it to a struct
+func UnpackDSADefault(dsa *AccountDSA) error {
+	if dsa == nil || len(dsa.Default) == 0 {
+		return nil
+	}
+	return jsonutil.Unmarshal([]byte(dsa.Default), &dsa.DefaultUnpacked)
+}
+
 // AccountDefaultsJSON returns the precompiled JSON form of account_defaults
 func (cfg *Configuration) AccountDefaultsJSON() json.RawMessage {
 	return cfg.accountDefaultsJSON
@@ -1146,10 +1158,13 @@ func SetupViper(v *viper.Viper, filename string, bidderInfos BidderInfos) {
 	v.SetDefault("account_defaults.price_floors.fetch.max_age_sec", 86400)
 	v.SetDefault("account_defaults.price_floors.fetch.period_sec", 3600)
 	v.SetDefault("account_defaults.price_floors.fetch.max_schema_dims", 0)
+	v.SetDefault("account_defaults.privacy.privacysandbox.topicsdomain", "")
 	v.SetDefault("account_defaults.privacy.privacysandbox.cookiedeprecation.enabled", false)
 	v.SetDefault("account_defaults.privacy.privacysandbox.cookiedeprecation.ttl_sec", 604800)
 
 	v.SetDefault("account_defaults.events_enabled", false)
+	v.BindEnv("account_defaults.privacy.dsa.default")
+	v.BindEnv("account_defaults.privacy.dsa.gdpr_only")
 	v.SetDefault("account_defaults.privacy.ipv6.anon_keep_bits", 56)
 	v.SetDefault("account_defaults.privacy.ipv4.anon_keep_bits", 24)
 
diff --git a/dsa/writer.go b/dsa/writer.go
new file mode 100644
index 00000000..9cf276de
--- /dev/null
+++ b/dsa/writer.go
@@ -0,0 +1,33 @@
+package dsa
+
+import (
+	"github.com/prebid/prebid-server/v2/config"
+	"github.com/prebid/prebid-server/v2/openrtb_ext"
+)
+
+// Writer is used to write the default DSA to the request (req.regs.ext.dsa)
+type Writer struct {
+	Config      *config.AccountDSA
+	GDPRInScope bool
+}
+
+// Write sets the default DSA object on the request at regs.ext.dsa if it is
+// defined in the account config and it is not already present on the request
+func (dw Writer) Write(req *openrtb_ext.RequestWrapper) error {
+	if req == nil || getReqDSA(req) != nil {
+		return nil
+	}
+	if dw.Config == nil || dw.Config.DefaultUnpacked == nil {
+		return nil
+	}
+	if dw.Config.GDPROnly && !dw.GDPRInScope {
+		return nil
+	}
+	regExt, err := req.GetRegExt()
+	if err != nil {
+		return err
+	}
+	clonedDefaultUnpacked := dw.Config.DefaultUnpacked.Clone()
+	regExt.SetDSA(clonedDefaultUnpacked)
+	return nil
+}
diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go
index 1b53b182..4a6574e8 100644
--- a/endpoints/openrtb2/amp_auction.go
+++ b/endpoints/openrtb2/amp_auction.go
@@ -156,6 +156,7 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
 	w.Header().Set("AMP-Access-Control-Allow-Source-Origin", origin)
 	w.Header().Set("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin")
 	w.Header().Set("X-Prebid", version.BuildXPrebidHeader(version.Ver))
+	setBrowsingTopicsHeader(w, r)
 
 	// There is no body for AMP requests, so we pass a nil body and ignore the return value.
 	_, rejectErr := hookExecutor.ExecuteEntrypointStage(r, nilBody)
@@ -230,6 +231,11 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
 		return
 	}
 
+	// Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers).
+	if errs := deps.setFieldsImplicitly(r, reqWrapper, account); len(errs) > 0 {
+		errL = append(errL, errs...)
+	}
+
 	hasStoredResponses := len(storedAuctionResponses) > 0
 	errs := deps.validateRequest(account, r, reqWrapper, true, hasStoredResponses, storedBidResponses, false)
 	errL = append(errL, errs...)
@@ -441,6 +447,9 @@ func getExtBidResponse(
 		warnings = make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage)
 	}
 	for _, v := range errortypes.WarningOnly(errs) {
+		if errortypes.ReadScope(v) == errortypes.ScopeDebug && !(reqWrapper != nil && reqWrapper.Test == 1) {
+			continue
+		}
 		bidderErr := openrtb_ext.ExtBidderMessage{
 			Code:    errortypes.ReadCode(v),
 			Message: v.Error(),
@@ -501,9 +510,6 @@ func (deps *endpointDeps) parseAmpRequest(httpRequest *http.Request) (req *openr
 	// move to using the request wrapper
 	req = &openrtb_ext.RequestWrapper{BidRequest: reqNormal}
 
-	// Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers).
-	deps.setFieldsImplicitly(httpRequest, req)
-
 	// Need to ensure cache and targeting are turned on
 	e = initAmpTargetingAndCache(req)
 	if errs = append(errs, e...); errortypes.ContainsFatalError(errs) {
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index 9accae2e..8c178ff3 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -29,6 +29,7 @@ import (
 	"github.com/prebid/prebid-server/v2/hooks"
 	"github.com/prebid/prebid-server/v2/ortb"
 	"github.com/prebid/prebid-server/v2/privacy"
+	"github.com/prebid/prebid-server/v2/privacysandbox"
 	"golang.org/x/net/publicsuffix"
 	jsonpatch "gopkg.in/evanphx/json-patch.v4"
 
@@ -61,6 +62,9 @@ const storedRequestTimeoutMillis = 50
 const ampChannel = "amp"
 const appChannel = "app"
 const secCookieDeprecation = "Sec-Cookie-Deprecation"
+const secBrowsingTopics = "Sec-Browsing-Topics"
+const observeBrowsingTopics = "Observe-Browsing-Topics"
+const observeBrowsingTopicsValue = "?1"
 
 var (
 	dntKey      string = http.CanonicalHeaderKey("DNT")
@@ -190,6 +194,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 	}()
 
 	w.Header().Set("X-Prebid", version.BuildXPrebidHeader(version.Ver))
+	setBrowsingTopicsHeader(w, r)
 
 	req, impExtInfoMap, storedAuctionResponses, storedBidResponses, bidderImpReplaceImp, account, errL := deps.parseRequest(r, &labels, hookExecutor)
 	if errortypes.ContainsFatalError(errL) && writeError(errL, w, &labels) {
@@ -393,6 +398,13 @@ func sendAuctionResponse(
 	return labels, ao
 }
 
+// setBrowsingTopicsHeader always set the Observe-Browsing-Topics header to a value of ?1 if the Sec-Browsing-Topics is present in request
+func setBrowsingTopicsHeader(w http.ResponseWriter, r *http.Request) {
+	if value := r.Header.Get(secBrowsingTopics); value != "" {
+		w.Header().Set(observeBrowsingTopics, observeBrowsingTopicsValue)
+	}
+}
+
 // parseRequest turns the HTTP request into an OpenRTB request. This is guaranteed to return:
 //
 //   - A context which times out appropriately, given the request.
@@ -406,6 +418,7 @@ func sendAuctionResponse(
 func (deps *endpointDeps) parseRequest(httpRequest *http.Request, labels *metrics.Labels, hookExecutor hookexecution.HookStageExecutor) (req *openrtb_ext.RequestWrapper, impExtInfoMap map[string]exchange.ImpExtInfo, storedAuctionResponses stored_responses.ImpsWithBidResponses, storedBidResponses stored_responses.ImpBidderStoredResp, bidderImpReplaceImpId stored_responses.BidderImpReplaceImpID, account *config.Account, errs []error) {
 	errs = nil
 	var err error
+	var errL []error
 	var r io.ReadCloser = httpRequest.Body
 	reqContentEncoding := httputil.ContentEncoding(httpRequest.Header.Get("Content-Encoding"))
 	if reqContentEncoding != "" {
@@ -532,7 +545,9 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request, labels *metric
 	}
 
 	// Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers).
-	deps.setFieldsImplicitly(httpRequest, req)
+	if errsL := deps.setFieldsImplicitly(httpRequest, req, account); len(errsL) > 0 {
+		errs = append(errs, errsL...)
+	}
 
 	if err := ortb.SetDefaults(req); err != nil {
 		errs = []error{err}
@@ -547,13 +562,14 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request, labels *metric
 	lmt.ModifyForIOS(req.BidRequest)
 
 	//Stored auction responses should be processed after stored requests due to possible impression modification
-	storedAuctionResponses, storedBidResponses, bidderImpReplaceImpId, errs = stored_responses.ProcessStoredResponses(ctx, req, deps.storedRespFetcher)
-	if len(errs) > 0 {
+	storedAuctionResponses, storedBidResponses, bidderImpReplaceImpId, errL = stored_responses.ProcessStoredResponses(ctx, req, deps.storedRespFetcher)
+	if len(errL) > 0 {
+		errs = append(errs, errL...)
 		return nil, nil, nil, nil, nil, nil, errs
 	}
 
 	hasStoredResponses := len(storedAuctionResponses) > 0
-	errL := deps.validateRequest(account, httpRequest, req, false, hasStoredResponses, storedBidResponses, hasStoredBidRequest)
+	errL = deps.validateRequest(account, httpRequest, req, false, hasStoredResponses, storedBidResponses, hasStoredBidRequest)
 	if len(errL) > 0 {
 		errs = append(errs, errL...)
 	}
@@ -876,7 +892,7 @@ func (deps *endpointDeps) validateRequest(account *config.Account, httpReq *http
 		return append(errL, err)
 	}
 
-	if err := validateOrFillCDep(httpReq, req, account); err != nil {
+	if err := validateOrFillCookieDeprecation(httpReq, req, account); err != nil {
 		errL = append(errL, err)
 	}
 
@@ -1919,7 +1935,7 @@ func validateDevice(device *openrtb2.Device) error {
 	return nil
 }
 
-func validateOrFillCDep(httpReq *http.Request, req *openrtb_ext.RequestWrapper, account *config.Account) error {
+func validateOrFillCookieDeprecation(httpReq *http.Request, req *openrtb_ext.RequestWrapper, account *config.Account) error {
 	if account == nil || !account.Privacy.PrivacySandbox.CookieDeprecation.Enabled {
 		return nil
 	}
@@ -2029,7 +2045,7 @@ func sanitizeRequest(r *openrtb_ext.RequestWrapper, ipValidator iputil.IPValidat
 // OpenRTB properties from the headers and other implicit info.
 //
 // This function _should not_ override any fields which were defined explicitly by the caller in the request.
-func (deps *endpointDeps) setFieldsImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper) {
+func (deps *endpointDeps) setFieldsImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper, account *config.Account) []error {
 	sanitizeRequest(r, deps.privateNetworkIPValidator)
 
 	setDeviceImplicitly(httpReq, r, deps.privateNetworkIPValidator)
@@ -2041,6 +2057,9 @@ func (deps *endpointDeps) setFieldsImplicitly(httpReq *http.Request, r *openrtb_
 	}
 
 	setAuctionTypeImplicitly(r)
+
+	errs := setSecBrowsingTopicsImplicitly(httpReq, r, account)
+	return errs
 }
 
 // setDeviceImplicitly uses implicit info from httpReq to populate bidReq.Device
@@ -2048,7 +2067,6 @@ func setDeviceImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper, i
 	setIPImplicitly(httpReq, r, ipValidtor)
 	setUAImplicitly(httpReq, r)
 	setDoNotTrackImplicitly(httpReq, r)
-
 }
 
 // setAuctionTypeImplicitly sets the auction type to 1 if it wasn't on the request,
@@ -2059,6 +2077,31 @@ func setAuctionTypeImplicitly(r *openrtb_ext.RequestWrapper) {
 	}
 }
 
+// setSecBrowsingTopicsImplicitly updates user.data with data from request header 'Sec-Browsing-Topics'
+func setSecBrowsingTopicsImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper, account *config.Account) []error {
+	secBrowsingTopics := httpReq.Header.Get(secBrowsingTopics)
+	if secBrowsingTopics == "" {
+		return nil
+	}
+
+	// host must configure privacy sandbox
+	if account == nil || account.Privacy.PrivacySandbox.TopicsDomain == "" {
+		return nil
+	}
+
+	topics, errs := privacysandbox.ParseTopicsFromHeader(secBrowsingTopics)
+	if len(topics) == 0 {
+		return errs
+	}
+
+	if r.User == nil {
+		r.User = &openrtb2.User{}
+	}
+
+	r.User.Data = privacysandbox.UpdateUserDataWithTopics(r.User.Data, topics, account.Privacy.PrivacySandbox.TopicsDomain)
+	return errs
+}
+
 func setSiteImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper) {
 	if r.Site == nil {
 		r.Site = &openrtb2.Site{}
diff --git a/endpoints/openrtb2/sample-requests/video/video_invalid_sample_negative_tmax.json b/endpoints/openrtb2/sample-requests/video/video_invalid_sample_negative_tmax.json
new file mode 100644
index 00000000..e1dc0231
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/video/video_invalid_sample_negative_tmax.json
@@ -0,0 +1,87 @@
+{
+    "description": "video request with negative tmax value. Expect error",
+  
+    "requestPayload": {
+      "tmax": -2,
+      "storedrequestid": "80ce30c53c16e6ede735f123ef6e32361bfc7b22",
+      "podconfig": {
+        "durationrangesec": [
+          30
+        ],
+        "requireexactduration": true,
+        "pods": [{
+            "podid": 1,
+            "adpoddurationsec": 180,
+            "configid": "fba10607-0c12-43d1-ad07-b8a513bc75d6"
+          },
+          {
+            "podid": 2,
+            "adpoddurationsec": 150,
+            "configid": "8b452b41-2681-4a20-9086-6f16ffad7773"
+          }
+        ]
+      },
+      "site": {
+        "page": "prebid.com"
+      },
+      "regs": {
+        "ext": {
+          "gdpr": 0
+        }
+      },
+      "user": {
+        "yob": 1991,
+        "gender": "F",
+        "keywords": "Hotels, Travelling",
+        "ext": {
+          "prebid": {
+            "buyeruids": {
+              "appnexus": "unique_id_an",
+              "rubicon": "unique_id_rubi"
+            }
+          }
+        }
+      },
+      "device": {
+        "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2",
+        "ip": "123.145.167.10",
+        "devicetype": 1,
+        "ifa": "AA000DFE74168477C70D291f574D344790E0BB11",
+        "lmt": 44,
+        "os": "mac os",
+        "w": 640,
+        "h": 480,
+        "didsha1": "didsha1",
+        "didmd5": "didmd5",
+        "dpidsha1": "dpidsha1",
+        "dpidmd5": "dpidmd5",
+        "macsha1": "macsha1",
+        "macmd5": "macmd5"
+      },
+      "includebrandcategory": {
+        "primaryadserver": 1,
+        "publisher": ""
+      },
+      "video": {
+        "w": 640,
+        "h": 480,
+        "mimes": [
+          "video/mp4"
+        ],
+        "protocols": [
+          2, 3, 5, 6
+        ]
+      },
+      "content": {
+        "episode": 6,
+        "title": "episodeName",
+        "series": "TvName",
+        "season": "season3",
+        "len": 900,
+        "livestream": 0
+      },
+      "cacheconfig": {
+        "ttl": 42
+      }
+    }
+  }
\ No newline at end of file
diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go
index 3ddfc31a..f74a4a22 100644
--- a/endpoints/openrtb2/video_auction.go
+++ b/endpoints/openrtb2/video_auction.go
@@ -165,6 +165,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 	}()
 
 	w.Header().Set("X-Prebid", version.BuildXPrebidHeader(version.Ver))
+	setBrowsingTopicsHeader(w, r)
 
 	lr := &io.LimitedReader{
 		R: r.Body,
@@ -259,9 +260,6 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 	// all code after this line should use the bidReqWrapper instead of bidReq directly
 	bidReqWrapper := &openrtb_ext.RequestWrapper{BidRequest: bidReq}
 
-	// Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers).
-	deps.setFieldsImplicitly(r, bidReqWrapper)
-
 	if err := ortb.SetDefaults(bidReqWrapper); err != nil {
 		handleError(&labels, w, errL, &vo, &debugLog)
 		return
@@ -300,7 +298,13 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 		return
 	}
 
-	errL = deps.validateRequest(account, r, bidReqWrapper, false, false, nil, false)
+	// Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers).
+	if errs := deps.setFieldsImplicitly(r, bidReqWrapper, account); len(errs) > 0 {
+		errL = append(errL, errs...)
+	}
+
+	errs := deps.validateRequest(account, r, bidReqWrapper, false, false, nil, false)
+	errL = append(errL, errs...)
 	if errortypes.ContainsFatalError(errL) {
 		handleError(&labels, w, errL, &vo, &debugLog)
 		return
@@ -308,6 +312,8 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 
 	activityControl = privacy.NewActivityControl(&account.Privacy)
 
+	warnings := errortypes.WarningOnly(errL)
+
 	secGPC := r.Header.Get("Sec-GPC")
 	auctionRequest := &exchange.AuctionRequest{
 		BidRequestWrapper:          bidReqWrapper,
@@ -316,6 +322,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 		RequestType:                labels.RType,
 		StartTime:                  start,
 		LegacyLabels:               labels,
+		Warnings:                   warnings,
 		GlobalPrivacyControlHeader: secGPC,
 		PubID:                      labels.PubID,
 		HookExecutor:               hookexecution.EmptyHookExecutor{},
diff --git a/errortypes/code.go b/errortypes/code.go
index a30bb8e4..a56df67c 100644
--- a/errortypes/code.go
+++ b/errortypes/code.go
@@ -33,6 +33,7 @@ const (
 	FloorBidRejectionWarningCode
 	InvalidBidResponseDSAWarningCode
 	SecCookieDeprecationLenWarningCode
+	SecBrowsingTopicsWarningCode
 )
 
 // Coder provides an error or warning code with severity.
diff --git a/errortypes/errortypes.go b/errortypes/errortypes.go
index d31c4166..7ca5668c 100644
--- a/errortypes/errortypes.go
+++ b/errortypes/errortypes.go
@@ -251,3 +251,26 @@ func (err *FailedToMarshal) Code() int {
 func (err *FailedToMarshal) Severity() Severity {
 	return SeverityFatal
 }
+
+// DebugWarning is a generic non-fatal error used in debug mode. Throughout the codebase, an error can
+// only be a warning if it's of the type defined below
+type DebugWarning struct {
+	Message     string
+	WarningCode int
+}
+
+func (err *DebugWarning) Error() string {
+	return err.Message
+}
+
+func (err *DebugWarning) Code() int {
+	return err.WarningCode
+}
+
+func (err *DebugWarning) Severity() Severity {
+	return SeverityWarning
+}
+
+func (err *DebugWarning) Scope() Scope {
+	return ScopeDebug
+}
diff --git a/errortypes/scope.go b/errortypes/scope.go
new file mode 100644
index 00000000..b9728435
--- /dev/null
+++ b/errortypes/scope.go
@@ -0,0 +1,19 @@
+package errortypes
+
+type Scope int
+
+const (
+	ScopeAny Scope = iota
+	ScopeDebug
+)
+
+type Scoped interface {
+	Scope() Scope
+}
+
+func ReadScope(err error) Scope {
+	if e, ok := err.(Scoped); ok {
+		return e.Scope()
+	}
+	return ScopeAny
+}
diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go
index a8551666..416bd164 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -164,6 +164,7 @@ import (
 	"github.com/prebid/prebid-server/v2/adapters/smartx"
 	"github.com/prebid/prebid-server/v2/adapters/smartyads"
 	"github.com/prebid/prebid-server/v2/adapters/smilewanted"
+	"github.com/prebid/prebid-server/v2/adapters/smrtconnect"
 	"github.com/prebid/prebid-server/v2/adapters/sonobi"
 	"github.com/prebid/prebid-server/v2/adapters/sovrn"
 	"github.com/prebid/prebid-server/v2/adapters/sovrnXsp"
@@ -371,6 +372,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderSmartx:            smartx.Builder,
 		openrtb_ext.BidderSmartyAds:         smartyads.Builder,
 		openrtb_ext.BidderSmileWanted:       smilewanted.Builder,
+		openrtb_ext.BidderSmrtconnect:       smrtconnect.Builder,
 		openrtb_ext.BidderSonobi:            sonobi.Builder,
 		openrtb_ext.BidderSovrn:             sovrn.Builder,
 		openrtb_ext.BidderSovrnXsp:          sovrnXsp.Builder,
diff --git a/exchange/exchange.go b/exchange/exchange.go
index c73eea9f..e688b3e0 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -488,6 +488,9 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog
 	}
 
 	for _, warning := range r.Warnings {
+		if errortypes.ReadScope(warning) == errortypes.ScopeDebug && !responseDebugAllow {
+			continue
+		}
 		generalWarning := openrtb_ext.ExtBidderMessage{
 			Code:    errortypes.ReadCode(warning),
 			Message: warning.Error(),
diff --git a/exchange/gdpr.go b/exchange/gdpr.go
index 52fb860f..2f94eefd 100644
--- a/exchange/gdpr.go
+++ b/exchange/gdpr.go
@@ -35,3 +35,9 @@ func getConsent(req *openrtb_ext.RequestWrapper, gpp gpplib.GppContainer) (conse
 	}
 	return *ue.GetConsent(), nil
 }
+
+// enforceGDPR determines if GDPR should be enforced based on the request signal and whether the channel is enabled
+func enforceGDPR(signal gdpr.Signal, defaultValue gdpr.Signal, channelEnabled bool) bool {
+	gdprApplies := signal == gdpr.SignalYes || (signal == gdpr.SignalAmbiguous && defaultValue == gdpr.SignalYes)
+	return gdprApplies && channelEnabled
+}
diff --git a/exchange/utils.go b/exchange/utils.go
index 676c015a..671e2ff8 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -18,6 +18,7 @@ import (
 	"github.com/prebid/openrtb/v20/openrtb2"
 
 	"github.com/prebid/prebid-server/v2/config"
+	"github.com/prebid/prebid-server/v2/dsa"
 	"github.com/prebid/prebid-server/v2/errortypes"
 	"github.com/prebid/prebid-server/v2/firstpartydata"
 	"github.com/prebid/prebid-server/v2/gdpr"
@@ -82,8 +83,30 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context,
 		return
 	}
 
+	gdprSignal, err := getGDPR(req)
+	if err != nil {
+		errs = append(errs, err)
+	}
+	channelEnabled := auctionReq.TCF2Config.ChannelEnabled(channelTypeMap[auctionReq.LegacyLabels.RType])
+	gdprEnforced := enforceGDPR(gdprSignal, gdprDefaultValue, channelEnabled)
+	dsaWriter := dsa.Writer{
+		Config:      auctionReq.Account.Privacy.DSA,
+		GDPRInScope: gdprEnforced,
+	}
+	if err := dsaWriter.Write(req); err != nil {
+		errs = append(errs, err)
+	}
+	if err := req.RebuildRequest(); err != nil {
+		errs = append(errs, err)
+		return
+	}
+
 	var allBidderRequests []BidderRequest
-	allBidderRequests, errs = getAuctionBidderRequests(auctionReq, requestExt, rs.bidderToSyncerKey, impsByBidder, aliases, rs.hostSChainNode)
+	var allBidderRequestErrs []error
+	allBidderRequests, allBidderRequestErrs = getAuctionBidderRequests(auctionReq, requestExt, rs.bidderToSyncerKey, impsByBidder, aliases, rs.hostSChainNode)
+	if allBidderRequestErrs != nil {
+		errs = append(errs, allBidderRequestErrs...)
+	}
 
 	bidderNameToBidderReq := buildBidResponseRequest(req.BidRequest, bidderImpWithBidResp, aliases, auctionReq.BidderImpReplaceImpID)
 	//this function should be executed after getAuctionBidderRequests
@@ -103,16 +126,10 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context,
 		applyBidAdjustmentToFloor(allBidderRequests, bidAdjustmentFactors)
 	}
 
-	gdprSignal, err := getGDPR(req)
-	if err != nil {
-		errs = append(errs, err)
-	}
-
 	consent, err := getConsent(req, gpp)
 	if err != nil {
 		errs = append(errs, err)
 	}
-	gdprApplies := gdprSignal == gdpr.SignalYes || (gdprSignal == gdpr.SignalAmbiguous && gdprDefaultValue == gdpr.SignalYes)
 
 	ccpaEnforcer, err := extractCCPA(req.BidRequest, rs.privacyConfig, &auctionReq.Account, aliases, channelTypeMap[auctionReq.LegacyLabels.RType], gpp)
 	if err != nil {
@@ -130,13 +147,8 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context,
 	privacyLabels.COPPAEnforced = coppa
 	privacyLabels.LMTEnforced = lmt
 
-	var gdprEnforced bool
 	var gdprPerms gdpr.Permissions = &gdpr.AlwaysAllow{}
 
-	if gdprApplies {
-		gdprEnforced = auctionReq.TCF2Config.ChannelEnabled(channelTypeMap[auctionReq.LegacyLabels.RType])
-	}
-
 	if gdprEnforced {
 		privacyLabels.GDPREnforced = true
 		parsedConsent, err := vendorconsent.ParseString(consent)
@@ -227,7 +239,10 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context,
 			privacy.ScrubTID(reqWrapper)
 		}
 
-		reqWrapper.RebuildRequest()
+		err := reqWrapper.RebuildRequest()
+		if err != nil {
+			errs = append(errs, err)
+		}
 		bidderRequest.BidRequest = reqWrapper.BidRequest
 
 		allowedBidderRequests = append(allowedBidderRequests, bidderRequest)
diff --git a/floors/validate.go b/floors/validate.go
index 5dd843b1..245bf993 100644
--- a/floors/validate.go
+++ b/floors/validate.go
@@ -55,7 +55,7 @@ func validateFloorRulesAndLowerValidRuleKey(schema openrtb_ext.PriceFloorSchema,
 
 // validateFloorParams validates SchemaVersion, SkipRate and FloorMin
 func validateFloorParams(extFloorRules *openrtb_ext.PriceFloorRules) error {
-	if extFloorRules.Data != nil && len(extFloorRules.Data.FloorsSchemaVersion) > 0 && extFloorRules.Data.FloorsSchemaVersion != "2" {
+	if extFloorRules.Data != nil && extFloorRules.Data.FloorsSchemaVersion != 0 && extFloorRules.Data.FloorsSchemaVersion != 2 {
 		return fmt.Errorf("Invalid FloorsSchemaVersion = '%v', supported version 2", extFloorRules.Data.FloorsSchemaVersion)
 	}
 
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 71bed294..ea74cf94 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -182,6 +182,7 @@ var coreBidderNames []BidderName = []BidderName{
 	BidderSmartx,
 	BidderSmartyAds,
 	BidderSmileWanted,
+	BidderSmrtconnect,
 	BidderSonobi,
 	BidderSovrn,
 	BidderSovrnXsp,
@@ -465,6 +466,7 @@ const (
 	BidderSmartx            BidderName = "smartx"
 	BidderSmartyAds         BidderName = "smartyads"
 	BidderSmileWanted       BidderName = "smilewanted"
+	BidderSmrtconnect       BidderName = "smrtconnect"
 	BidderSonobi            BidderName = "sonobi"
 	BidderSovrn             BidderName = "sovrn"
 	BidderSovrnXsp          BidderName = "sovrnXsp"
diff --git a/openrtb_ext/floors.go b/openrtb_ext/floors.go
index 92dab2ac..12c04c8a 100644
--- a/openrtb_ext/floors.go
+++ b/openrtb_ext/floors.go
@@ -84,7 +84,7 @@ type PriceFloorEndpoint struct {
 type PriceFloorData struct {
 	Currency            string                 `json:"currency,omitempty"`
 	SkipRate            int                    `json:"skiprate,omitempty"`
-	FloorsSchemaVersion string                 `json:"floorsschemaversion,omitempty"`
+	FloorsSchemaVersion int                    `json:"floorsschemaversion,omitempty"`
 	ModelTimestamp      int                    `json:"modeltimestamp,omitempty"`
 	ModelGroups         []PriceFloorModelGroup `json:"modelgroups,omitempty"`
 	FloorProvider       string                 `json:"floorprovider,omitempty"`
diff --git a/openrtb_ext/imp_smrtconnect.go b/openrtb_ext/imp_smrtconnect.go
new file mode 100644
index 00000000..c3e6b0cd
--- /dev/null
+++ b/openrtb_ext/imp_smrtconnect.go
@@ -0,0 +1,5 @@
+package openrtb_ext
+
+type ExtSmrtconnect struct {
+	SupplyId string `json:"supply_id"`
+}
diff --git a/openrtb_ext/regs.go b/openrtb_ext/regs.go
index eca5ff98..7277bb86 100644
--- a/openrtb_ext/regs.go
+++ b/openrtb_ext/regs.go
@@ -1,5 +1,9 @@
 package openrtb_ext
 
+import (
+	"github.com/prebid/prebid-server/v2/util/sliceutil"
+)
+
 // ExtRegs defines the contract for bidrequest.regs.ext
 type ExtRegs struct {
 	// DSA is an object containing DSA transparency information, see https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/dsa_transparency.md
@@ -15,8 +19,45 @@ type ExtRegs struct {
 
 // ExtRegsDSA defines the contract for bidrequest.regs.ext.dsa
 type ExtRegsDSA struct {
-	// Required should be a between 0 and 3 inclusive, see https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/dsa_transparency.md
-	Required *int8 `json:"dsarequired,omitempty"`
-	// PubRender should be between 0 and 2 inclusive, see https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/dsa_transparency.md
-	PubRender *int8 `json:"pubrender,omitempty"`
+	Required     *int8                   `json:"dsarequired,omitempty"`
+	PubRender    *int8                   `json:"pubrender,omitempty"`
+	DataToPub    *int8                   `json:"datatopub,omitempty"`
+	Transparency []ExtBidDSATransparency `json:"transparency,omitempty"`
+}
+
+// Clone creates a deep copy of ExtRegsDSA
+func (erd *ExtRegsDSA) Clone() *ExtRegsDSA {
+	if erd == nil {
+		return nil
+	}
+	clone := *erd
+
+	if erd.Required != nil {
+		clonedRequired := *erd.Required
+		clone.Required = &clonedRequired
+	}
+	if erd.PubRender != nil {
+		clonedPubRender := *erd.PubRender
+		clone.PubRender = &clonedPubRender
+	}
+	if erd.DataToPub != nil {
+		clonedDataToPub := *erd.DataToPub
+		clone.DataToPub = &clonedDataToPub
+	}
+	if erd.Transparency != nil {
+		clonedTransparency := make([]ExtBidDSATransparency, len(erd.Transparency))
+		for i, transparency := range erd.Transparency {
+			newTransparency := transparency
+			newTransparency.Params = sliceutil.Clone(transparency.Params)
+			clonedTransparency[i] = newTransparency
+		}
+		clone.Transparency = clonedTransparency
+	}
+	return &clone
+}
+
+// ExtBidDSATransparency defines the contract for bidrequest.regs.ext.dsa.transparency
+type ExtBidDSATransparency struct {
+	Domain string `json:"domain,omitempty"`
+	Params []int  `json:"dsaparams,omitempty"`
 }
diff --git a/privacysandbox/topics.go b/privacysandbox/topics.go
new file mode 100644
index 00000000..4c129d0a
--- /dev/null
+++ b/privacysandbox/topics.go
@@ -0,0 +1,228 @@
+package privacysandbox
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/prebid/openrtb/v20/openrtb2"
+	"github.com/prebid/prebid-server/v2/errortypes"
+	"github.com/prebid/prebid-server/v2/util/jsonutil"
+)
+
+type Topic struct {
+	SegTax   int    `json:"segtax,omitempty"`
+	SegClass string `json:"segclass,omitempty"`
+	SegIDs   []int  `json:"segids,omitempty"`
+}
+
+// ParseTopicsFromHeader parses the Sec-Browsing-Topics header data into Topics object
+func ParseTopicsFromHeader(secBrowsingTopics string) ([]Topic, []error) {
+	topics := make([]Topic, 0, 10)
+	var warnings []error
+
+	for _, field := range strings.Split(secBrowsingTopics, ",") {
+		field = strings.TrimSpace(field)
+		if field == "" || strings.HasPrefix(field, "();p=") {
+			continue
+		}
+
+		if len(topics) < 10 {
+			if topic, ok := parseTopicSegment(field); ok {
+				topics = append(topics, topic)
+			} else {
+				warnings = append(warnings, formatWarning(field))
+			}
+		} else {
+			warnings = append(warnings, formatWarning(field+" discarded due to limit reached."))
+		}
+	}
+
+	return topics, warnings
+}
+
+// parseTopicSegment parses a single topic segment from the header into Topics object
+func parseTopicSegment(field string) (Topic, bool) {
+	segment := strings.Split(field, ";")
+	if len(segment) != 2 {
+		return Topic{}, false
+	}
+
+	segmentsIDs := strings.TrimSpace(segment[0])
+	if len(segmentsIDs) < 3 || segmentsIDs[0] != '(' || segmentsIDs[len(segmentsIDs)-1] != ')' {
+		return Topic{}, false
+	}
+
+	segtax, segclass := parseSegTaxSegClass(segment[1])
+	if segtax == 0 || segclass == "" {
+		return Topic{}, false
+	}
+
+	segIDs, err := parseSegmentIDs(segmentsIDs[1 : len(segmentsIDs)-1])
+	if err != nil {
+		return Topic{}, false
+	}
+
+	return Topic{
+		SegTax:   segtax,
+		SegClass: segclass,
+		SegIDs:   segIDs,
+	}, true
+}
+
+func parseSegTaxSegClass(seg string) (int, string) {
+	taxanomyModel := strings.Split(seg, ":")
+	if len(taxanomyModel) != 3 {
+		return 0, ""
+	}
+
+	// taxanomyModel[0] is v=browser_version, we don't need it
+	taxanomyVer := strings.TrimSpace(taxanomyModel[1])
+	taxanomy, err := strconv.Atoi(taxanomyVer)
+	if err != nil || taxanomy < 1 || taxanomy > 10 {
+		return 0, ""
+	}
+
+	segtax := 600 + (taxanomy - 1)
+	segclass := strings.TrimSpace(taxanomyModel[2])
+	return segtax, segclass
+}
+
+// parseSegmentIDs parses the segment ids from the header string into int array
+func parseSegmentIDs(segmentsIDs string) ([]int, error) {
+	var selectedSegmentIDs []int
+	for _, segmentID := range strings.Fields(segmentsIDs) {
+		segmentID = strings.TrimSpace(segmentID)
+		selectedSegmentID, err := strconv.Atoi(segmentID)
+		if err != nil || selectedSegmentID <= 0 {
+			return selectedSegmentIDs, errors.New("invalid segment id")
+		}
+		selectedSegmentIDs = append(selectedSegmentIDs, selectedSegmentID)
+	}
+
+	return selectedSegmentIDs, nil
+}
+
+func UpdateUserDataWithTopics(userData []openrtb2.Data, headerData []Topic, topicsDomain string) []openrtb2.Data {
+	if topicsDomain == "" {
+		return userData
+	}
+
+	// headerDataMap groups segIDs by segtax and segclass for faster lookup and tracking of new segIDs yet to be added to user.data
+	// tracking is done by removing segIDs from segIDsMap once they are added to user.data, ensuring that headerDataMap will always have unique segtax-segclass-segIDs
+	// the only drawback of tracking via deleting segtax-segclass from headerDataMap is that this would not track duplicate entries within user.data which is fine because we are only merging header data with the provided user.data
+	headerDataMap := createHeaderDataMap(headerData)
+
+	for i, data := range userData {
+		ext := &Topic{}
+		err := json.Unmarshal(data.Ext, ext)
+		if err != nil {
+			continue
+		}
+
+		if ext.SegTax == 0 || ext.SegClass == "" {
+			continue
+		}
+
+		if newSegIDs := findNewSegIDs(data.Name, topicsDomain, *ext, data.Segment, headerDataMap); newSegIDs != nil {
+			for _, segID := range newSegIDs {
+				userData[i].Segment = append(userData[i].Segment, openrtb2.Segment{ID: strconv.Itoa(segID)})
+			}
+
+			delete(headerDataMap[ext.SegTax], ext.SegClass)
+		}
+	}
+
+	for segTax, segClassMap := range headerDataMap {
+		for segClass, segIDs := range segClassMap {
+			if len(segIDs) != 0 {
+				data := openrtb2.Data{
+					Name: topicsDomain,
+				}
+
+				var err error
+				data.Ext, err = jsonutil.Marshal(Topic{SegTax: segTax, SegClass: segClass})
+				if err != nil {
+					continue
+				}
+
+				for segID := range segIDs {
+					data.Segment = append(data.Segment, openrtb2.Segment{
+						ID: strconv.Itoa(segID),
+					})
+				}
+
+				userData = append(userData, data)
+			}
+		}
+	}
+
+	return userData
+}
+
+// createHeaderDataMap creates a map of header data (segtax-segclass-segIDs) for faster lookup
+// topicsdomain is not needed as we are only interested data from one domain configured in host config
+func createHeaderDataMap(headerData []Topic) map[int]map[string]map[int]struct{} {
+	headerDataMap := make(map[int]map[string]map[int]struct{})
+
+	for _, topic := range headerData {
+		segClassMap, ok := headerDataMap[topic.SegTax]
+		if !ok {
+			segClassMap = make(map[string]map[int]struct{})
+			headerDataMap[topic.SegTax] = segClassMap
+		}
+
+		segIDsMap, ok := segClassMap[topic.SegClass]
+		if !ok {
+			segIDsMap = make(map[int]struct{})
+			segClassMap[topic.SegClass] = segIDsMap
+		}
+
+		for _, segID := range topic.SegIDs {
+			segIDsMap[segID] = struct{}{}
+		}
+	}
+
+	return headerDataMap
+}
+
+// findNewSegIDs merge unique segIDs in single user.data if request.user.data and header data match. i.e. segclass, segtax and topicsdomain match
+func findNewSegIDs(dataName, topicsDomain string, userData Topic, userDataSegments []openrtb2.Segment, headerDataMap map[int]map[string]map[int]struct{}) []int {
+	if dataName != topicsDomain {
+		return nil
+	}
+
+	segClassMap, exists := headerDataMap[userData.SegTax]
+	if !exists {
+		return nil
+	}
+
+	segIDsMap, exists := segClassMap[userData.SegClass]
+	if !exists {
+		return nil
+	}
+
+	// remove existing segIDs entries
+	for _, segID := range userDataSegments {
+		if id, err := strconv.Atoi(segID.ID); err == nil {
+			delete(segIDsMap, id)
+		}
+	}
+
+	// collect remaining segIDs
+	segIDs := make([]int, 0, len(segIDsMap))
+	for segID := range segIDsMap {
+		segIDs = append(segIDs, segID)
+	}
+
+	return segIDs
+}
+
+func formatWarning(msg string) error {
+	return &errortypes.DebugWarning{
+		WarningCode: errortypes.SecBrowsingTopicsWarningCode,
+		Message:     fmt.Sprintf("Invalid field in Sec-Browsing-Topics header: %s", msg),
+	}
+}
diff --git a/static/bidder-info/onetag.yaml b/static/bidder-info/onetag.yaml
index bc52faff..00b83526 100644
--- a/static/bidder-info/onetag.yaml
+++ b/static/bidder-info/onetag.yaml
@@ -19,4 +19,7 @@ userSync:
   iframe:
     url: "https://onetag-sys.com/usync/?redir={{.RedirectURL}}&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}"
     userMacro: "${USER_TOKEN}"
-endpointCompression: "GZIP"
\ No newline at end of file
+  redirect:
+    url: "https://onetag-sys.com/usync/?tag=img&redir={{.RedirectURL}}&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}"
+    userMacro: "${USER_TOKEN}"
+endpointCompression: "GZIP"
diff --git a/static/bidder-info/smrtconnect.yaml b/static/bidder-info/smrtconnect.yaml
new file mode 100644
index 00000000..7078248d
--- /dev/null
+++ b/static/bidder-info/smrtconnect.yaml
@@ -0,0 +1,20 @@
+endpoint: "https://amp.smrtconnect.com/openrtb2/auction?supply_id={{.SupplyId}}"
+# This bidder does not operate globally. Please consider setting "disabled: true" in European datacenters.
+geoscope:
+  - "!EEA"
+endpointCompression: gzip
+maintainer:
+  email: "prebid@smrtconnect.com"
+capabilities:
+  app:
+    mediaTypes:
+      - banner
+      - native
+      - video
+      - audio
+  site:
+    mediaTypes:
+      - banner
+      - native
+      - video
+      - audio
\ No newline at end of file
diff --git a/static/bidder-params/smrtconnect.json b/static/bidder-params/smrtconnect.json
new file mode 100644
index 00000000..6916d456
--- /dev/null
+++ b/static/bidder-params/smrtconnect.json
@@ -0,0 +1,14 @@
+{
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "title": "Smrtconnect Params",
+    "description": "A schema which validates params accepted by the Smrtconnect",
+    "type": "object",
+    "properties": {
+        "supply_id": {
+          "type": "string",
+          "description": "Supply id",
+          "minLength": 1
+        }
+      },
+      "required": ["supply_id"]
+}


Test patch:
diff --git a/account/account_test.go b/account/account_test.go
index 7788f7c4..b4ea64f0 100644
--- a/account/account_test.go
+++ b/account/account_test.go
@@ -12,12 +12,20 @@ import (
 	"github.com/prebid/prebid-server/v2/openrtb_ext"
 	"github.com/prebid/prebid-server/v2/stored_requests"
 	"github.com/prebid/prebid-server/v2/util/iputil"
+	"github.com/prebid/prebid-server/v2/util/ptrutil"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
 )
 
+var (
+	validDSA   = `{\"dsarequired\":1,\"pubrender\":2,\"transparency\":[{\"domain\":\"test.com\"}]}`
+	invalidDSA = `{\"dsarequired\":\"invalid\",\"pubrender\":2,\"transparency\":[{\"domain\":\"test.com\"}]}`
+)
+
 var mockAccountData = map[string]json.RawMessage{
 	"valid_acct":                json.RawMessage(`{"disabled":false}`),
+	"valid_acct_dsa":            json.RawMessage(`{"disabled":false, "privacy": {"dsa": {"default": "` + validDSA + `"}}}`),
+	"invalid_acct_dsa":          json.RawMessage(`{"disabled":false, "privacy": {"dsa": {"default": "` + invalidDSA + `"}}}`),
 	"invalid_acct_ipv6_ipv4":    json.RawMessage(`{"disabled":false, "privacy": {"ipv6": {"anon_keep_bits": -32}, "ipv4": {"anon_keep_bits": -16}}}`),
 	"disabled_acct":             json.RawMessage(`{"disabled":true}`),
 	"malformed_acct":            json.RawMessage(`{"disabled":"invalid type"}`),
@@ -36,6 +44,16 @@ func (af mockAccountFetcher) FetchAccount(ctx context.Context, accountDefaultsJS
 }
 
 func TestGetAccount(t *testing.T) {
+	validDSA := &openrtb_ext.ExtRegsDSA{
+		Required:  ptrutil.ToPtr[int8](1),
+		PubRender: ptrutil.ToPtr[int8](2),
+		Transparency: []openrtb_ext.ExtBidDSATransparency{
+			{
+				Domain: "test.com",
+			},
+		},
+	}
+
 	unknown := metrics.PublisherUnknown
 	testCases := []struct {
 		accountID string
@@ -44,7 +62,8 @@ func TestGetAccount(t *testing.T) {
 		// account_defaults.disabled
 		disabled bool
 		// checkDefaultIP indicates IPv6 and IPv6 should be set to default values
-		checkDefaultIP bool
+		wantDefaultIP bool
+		wantDSA       *openrtb_ext.ExtRegsDSA
 		// expected error, or nil if account should be found
 		err error
 	}{
@@ -66,7 +85,13 @@ func TestGetAccount(t *testing.T) {
 		{accountID: "valid_acct", required: false, disabled: true, err: nil},
 		{accountID: "valid_acct", required: true, disabled: true, err: nil},
 
-		{accountID: "invalid_acct_ipv6_ipv4", required: true, disabled: false, err: nil, checkDefaultIP: true},
+		{accountID: "valid_acct_dsa", required: false, disabled: false, wantDSA: validDSA, err: nil},
+		{accountID: "valid_acct_dsa", required: true, disabled: false, wantDSA: validDSA, err: nil},
+		{accountID: "valid_acct_dsa", required: false, disabled: true, wantDSA: validDSA, err: nil},
+		{accountID: "valid_acct_dsa", required: true, disabled: true, wantDSA: validDSA, err: nil},
+
+		{accountID: "invalid_acct_ipv6_ipv4", required: true, disabled: false, err: nil, wantDefaultIP: true},
+		{accountID: "invalid_acct_dsa", required: false, disabled: false, err: &errortypes.MalformedAcct{}},
 
 		// pubID given and matches a host account explicitly disabled (Disabled: true on account json)
 		{accountID: "disabled_acct", required: false, disabled: false, err: &errortypes.AccountDisabled{}},
@@ -111,10 +136,13 @@ func TestGetAccount(t *testing.T) {
 				assert.Nil(t, account, "return account must be nil on error")
 				assert.IsType(t, test.err, errors[0], "error is of unexpected type")
 			}
-			if test.checkDefaultIP {
+			if test.wantDefaultIP {
 				assert.Equal(t, account.Privacy.IPv6Config.AnonKeepBits, iputil.IPv6DefaultMaskingBitSize, "ipv6 should be set to default value")
 				assert.Equal(t, account.Privacy.IPv4Config.AnonKeepBits, iputil.IPv4DefaultMaskingBitSize, "ipv4 should be set to default value")
 			}
+			if test.wantDSA != nil {
+				assert.Equal(t, test.wantDSA, account.Privacy.DSA.DefaultUnpacked)
+			}
 		})
 	}
 }
diff --git a/adapters/smrtconnect/smrtconnect_test.go b/adapters/smrtconnect/smrtconnect_test.go
new file mode 100644
index 00000000..38d071d9
--- /dev/null
+++ b/adapters/smrtconnect/smrtconnect_test.go
@@ -0,0 +1,28 @@
+package smrtconnect
+
+import (
+	"testing"
+
+	"github.com/prebid/prebid-server/v2/adapters/adapterstest"
+	"github.com/prebid/prebid-server/v2/config"
+	"github.com/prebid/prebid-server/v2/openrtb_ext"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestJsonSamples(t *testing.T) {
+	bidder, buildErr := Builder(openrtb_ext.BidderSmrtconnect, config.Adapter{
+		Endpoint: "http://test.smrtconnect.com/openrtb2/auction?supply_id={{.SupplyId}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 196})
+
+	if buildErr != nil {
+		t.Fatalf("Builder returned unexpected error %v", buildErr)
+	}
+
+	adapterstest.RunJSONBidderTest(t, "smrtconnecttest", bidder)
+}
+
+func TestEndpointTemplateMalformed(t *testing.T) {
+	_, buildErr := Builder(openrtb_ext.BidderSmrtconnect, config.Adapter{
+		Endpoint: "{{Malformed}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 196})
+
+	assert.Error(t, buildErr)
+}
diff --git a/adapters/smrtconnect/smrtconnecttest/exemplary/audio-app.json b/adapters/smrtconnect/smrtconnecttest/exemplary/audio-app.json
new file mode 100644
index 00000000..7f7c23d9
--- /dev/null
+++ b/adapters/smrtconnect/smrtconnecttest/exemplary/audio-app.json
@@ -0,0 +1,90 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "app": {
+      "bundle": "com.prebid"
+    },
+    "device": {
+      "ifa":"ec943cb9-61ec-460f-a925-6489c3fcc4e3"
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "audio": {
+          "mimes": ["audio/mp4"],
+          "protocols": [9,10]
+        },
+        "ext": {
+          "bidder": {
+            "supply_id": "1"
+          }
+        }
+      }
+    ]
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1",
+        "body": {
+          "id": "test-request-id",
+          "app": {
+            "bundle": "com.prebid"
+          },
+          "device": {
+            "ifa":"ec943cb9-61ec-460f-a925-6489c3fcc4e3"
+          },
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "audio": {
+                "mimes": ["audio/mp4"],
+                "protocols": [9,10]
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "smrtconnect",
+              "bid": [{
+                "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
+                "impid": "test-imp-id",
+                "price": 0.500000,
+                "adm": "some-test-ad",
+                "crid": "test-crid",
+                "mtype": 3
+              }]
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
+            "impid": "test-imp-id",
+            "price": 0.5,
+            "adm": "some-test-ad",
+            "crid": "test-crid",
+            "mtype": 3
+          },
+          "type": "audio"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/smrtconnect/smrtconnecttest/exemplary/audio-web.json b/adapters/smrtconnect/smrtconnecttest/exemplary/audio-web.json
new file mode 100644
index 00000000..f30494a4
--- /dev/null
+++ b/adapters/smrtconnect/smrtconnecttest/exemplary/audio-web.json
@@ -0,0 +1,90 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "site": {
+      "page": "prebid.org"
+    },
+    "user": {
+      "buyeruid": "be5e209ad46927520000000000000000"
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "audio": {
+          "mimes": ["audio/mp4"],
+          "protocols": [9,10]
+        },
+        "ext": {
+          "bidder": {
+            "supply_id": "1"
+          }
+        }
+      }
+    ]
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1",
+        "body": {
+          "id": "test-request-id",
+          "site": {
+            "page": "prebid.org"
+          },
+          "user": {
+            "buyeruid": "be5e209ad46927520000000000000000"
+          },
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "audio": {
+                "mimes": ["audio/mp4"],
+                "protocols": [9,10]
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "smrtconnect",
+              "bid": [{
+                "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
+                "impid": "test-imp-id",
+                "price": 0.500000,
+                "adm": "some-test-ad",
+                "crid": "test-crid",
+                "mtype": 3
+              }]
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
+            "impid": "test-imp-id",
+            "price": 0.5,
+            "adm": "some-test-ad",
+            "crid": "test-crid",
+            "mtype": 3
+          },
+          "type": "audio"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/smrtconnect/smrtconnecttest/exemplary/banner-app.json b/adapters/smrtconnect/smrtconnecttest/exemplary/banner-app.json
new file mode 100644
index 00000000..4111eb73
--- /dev/null
+++ b/adapters/smrtconnect/smrtconnecttest/exemplary/banner-app.json
@@ -0,0 +1,139 @@
+{
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "device": {
+      "ua": "test-user-agent",
+      "ip": "123.123.123.123",
+      "language": "en",
+      "dnt": 0
+    },
+    "tmax": 1000,
+    "user": {
+      "buyeruid": "awesome-user"
+    },
+    "app": {
+      "publisher": {
+        "id": "123456789"
+      },
+      "cat": [
+        "IAB22-1"
+      ],
+      "bundle": "com.app.awesome",
+      "name": "Awesome App",
+      "domain": "awesomeapp.com",
+      "id": "123456789"
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "tagid": "ogTAGID",
+        "banner": {
+          "w": 320,
+          "h": 50
+        },
+        "ext": {
+          "bidder": {
+            "supply_id": "1"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1",
+        "body": {
+          "id": "some-request-id",
+          "device": {
+            "ua": "test-user-agent",
+            "ip": "123.123.123.123",
+            "language": "en",
+            "dnt": 0
+          },
+          "imp": [
+            {
+              "id": "some-impression-id",
+              "banner": {
+                "w": 320,
+                "h": 50
+              },
+              "tagid": "ogTAGID"
+            }
+          ],
+          "app": {
+            "id": "123456789",
+            "name": "Awesome App",
+            "bundle": "com.app.awesome",
+            "domain": "awesomeapp.com",
+            "cat": [
+              "IAB22-1"
+            ],
+            "publisher": {
+              "id": "123456789"
+            }
+          },
+          "user": {
+            "buyeruid": "awesome-user"
+          },
+          "tmax": 1000
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "awesome-resp-id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+                  "impid": "1",
+                  "price": 3.5,
+                  "adm": "awesome-markup",
+                  "adomain": [
+                    "awesome.com"
+                  ],
+                  "crid": "20",
+                  "w": 320,
+                  "h": 50,
+                  "mtype": 1
+                }
+              ],
+              "seat": "smrtconnect"
+            }
+          ],
+          "cur": "USD",
+          "ext": {
+            "responsetimemillis": {
+              "smrtconnect": 154
+            },
+            "tmaxrequest": 1000
+          }
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "bids": [
+        {
+          "bid": {
+            "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+            "impid": "1",
+            "price": 3.5,
+            "adm": "awesome-markup",
+            "adomain": [
+              "awesome.com"
+            ],
+            "crid": "20",
+            "w": 320,
+            "h": 50,
+            "mtype": 1       
+          },
+          "type": "banner" 
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/smrtconnect/smrtconnecttest/exemplary/banner-multiple-bids.json b/adapters/smrtconnect/smrtconnecttest/exemplary/banner-multiple-bids.json
new file mode 100644
index 00000000..18391277
--- /dev/null
+++ b/adapters/smrtconnect/smrtconnecttest/exemplary/banner-multiple-bids.json
@@ -0,0 +1,227 @@
+{
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "device": {
+      "ua": "test-user-agent",
+      "ip": "123.123.123.123",
+      "language": "en",
+      "dnt": 0
+    },
+    "tmax": 1000,
+    "user": {
+      "buyeruid": "awesome-user"
+    },
+    "site": {
+      "page": "test.com",
+      "publisher": {
+        "id": "123456789"
+      }
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "tagid": "ogTAGID",
+        "banner": {
+          "w":320,
+          "h":50
+        },
+        "ext": {
+          "bidder": {
+            "supply_id": "1"
+          }
+        }
+      },
+      {
+        "id": "some-impression-id-2",
+        "tagid": "ogTAGID-2",
+        "banner": {
+          "w":300,
+          "h":250
+        },
+        "ext": {
+          "bidder": {
+            "supply_id": "1"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1",
+        "body": {
+          "id": "some-request-id",
+          "device": {
+            "ua": "test-user-agent",
+            "ip": "123.123.123.123",
+            "language": "en",
+            "dnt": 0
+          },
+          "imp": [
+            {
+              "id": "some-impression-id",
+              "tagid": "ogTAGID",
+              "banner": {
+                "w":320,
+                "h":50
+              }
+            }
+          ],
+          "site": {
+            "page": "test.com",
+            "publisher": {
+              "id": "123456789"
+            }
+          },
+          "user": {
+            "buyeruid": "awesome-user"
+          },
+          "tmax": 1000
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "awesome-resp-id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+                  "impid": "1",
+                  "price": 3.5,
+                  "adm": "awesome-markup",
+                  "adomain": [
+                    "awesome.com"
+                  ],
+                  "crid": "20",
+                  "w": 320,
+                  "h": 50,
+                  "mtype": 1
+                }
+              ],
+              "seat": "smrtconnect"
+            }
+          ],
+          "cur": "USD",
+          "ext": {
+            "responsetimemillis": {
+              "smrtconnect": 154
+            },
+            "tmaxrequest": 1000
+          }
+        }
+      }
+    },
+    {
+      "expectedRequest": {
+        "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1",
+        "body": {
+          "id": "some-request-id",
+          "device": {
+            "ua": "test-user-agent",
+            "ip": "123.123.123.123",
+            "language": "en",
+            "dnt": 0
+          },
+          "imp": [
+            {
+              "id": "some-impression-id-2",
+              "tagid": "ogTAGID-2",
+              "banner": {
+                "w":300,
+                "h":250
+              }
+            }
+          ],
+          "site": {
+            "page": "test.com",
+            "publisher": {
+              "id": "123456789"
+            }
+          },
+          "user": {
+            "buyeruid": "awesome-user"
+          },
+          "tmax": 1000
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "awesome-resp-id-2",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+                  "impid": "1",
+                  "price": 3.5,
+                  "adm": "awesome-markup-2",
+                  "adomain": [
+                    "awesome.com"
+                  ],
+                  "crid": "20",
+                  "w": 300,
+                  "h": 250,
+                  "mtype": 1
+                }
+              ],
+              "seat": "smrtconnect"
+            }
+          ],
+          "cur": "USD",
+          "ext": {
+            "responsetimemillis": {
+              "smrtconnect": 154
+            },
+            "tmaxrequest": 1000
+          }
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "bids":[
+        {
+          "bid":  {
+            "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+            "impid": "1",
+            "price": 3.5,
+            "adm": "awesome-markup",
+            "crid": "20",
+            "adomain": [
+              "awesome.com"
+            ],
+            "w": 320,
+            "h": 50,
+            "mtype": 1
+          },
+          "type": "banner"
+        }
+      ]
+    },
+    {
+      "bids":[
+        {
+          "bid":  {
+            "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+            "impid": "1",
+            "price": 3.5,
+            "adm": "awesome-markup-2",
+            "crid": "20",
+            "adomain": [
+              "awesome.com"
+            ],
+            "w": 300,
+            "h": 250,
+            "mtype": 1
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/smrtconnect/smrtconnecttest/exemplary/banner-web.json b/adapters/smrtconnect/smrtconnecttest/exemplary/banner-web.json
new file mode 100644
index 00000000..69743fd6
--- /dev/null
+++ b/adapters/smrtconnect/smrtconnecttest/exemplary/banner-web.json
@@ -0,0 +1,127 @@
+{
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "device": {
+      "ua": "test-user-agent",
+      "ip": "123.123.123.123",
+      "language": "en",
+      "dnt": 0
+    },
+    "tmax": 1000,
+    "user": {
+      "buyeruid": "awesome-user"
+    },
+    "site": {
+      "page": "test.com",
+      "publisher": {
+        "id": "123456789"
+      }
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "tagid": "ogTAGID",
+        "banner": {
+          "w":320,
+          "h":50
+        },
+        "ext": {
+          "bidder": {
+            "supply_id": "1"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1",
+        "body": {
+          "id": "some-request-id",
+          "device": {
+            "ua": "test-user-agent",
+            "ip": "123.123.123.123",
+            "language": "en",
+            "dnt": 0
+          },
+          "imp": [
+            {
+              "id": "some-impression-id",
+              "tagid": "ogTAGID",
+              "banner": {
+                "w":320,
+                "h":50
+              }
+            }
+          ],
+          "site": {
+            "page": "test.com",
+            "publisher": {
+              "id": "123456789"
+            }
+          },
+          "user": {
+            "buyeruid": "awesome-user"
+          },
+          "tmax": 1000
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "awesome-resp-id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+                  "impid": "1",
+                  "price": 3.5,
+                  "adm": "awesome-markup",
+                  "adomain": [
+                    "awesome.com"
+                  ],
+                  "crid": "20",
+                  "w": 320,
+                  "h": 50,
+                  "mtype": 1
+                }
+              ],
+              "seat": "smrtconnect"
+            }
+          ],
+          "cur": "USD",
+          "ext": {
+            "responsetimemillis": {
+              "smrtconnect": 154
+            },
+            "tmaxrequest": 1000
+          }
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "bids":[
+        {
+          "bid":  {
+            "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+            "impid": "1",
+            "price": 3.5,
+            "adm": "awesome-markup",
+            "crid": "20",
+            "adomain": [
+              "awesome.com"
+            ],
+            "w": 320,
+            "h": 50,
+            "mtype": 1
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/smrtconnect/smrtconnecttest/exemplary/native-app.json b/adapters/smrtconnect/smrtconnecttest/exemplary/native-app.json
new file mode 100644
index 00000000..63188c0c
--- /dev/null
+++ b/adapters/smrtconnect/smrtconnecttest/exemplary/native-app.json
@@ -0,0 +1,136 @@
+{
+    "mockBidRequest": {
+      "id": "some-request-id",
+      "device": {
+        "ua": "test-user-agent",
+        "ip": "123.123.123.123",
+        "language": "en",
+        "dnt": 0
+      },
+      "tmax": 1000,
+      "user": {
+        "buyeruid": "awesome-user"
+      },
+      "app": {
+        "publisher": {
+          "id": "123456789"
+        },
+        "cat": [
+          "IAB22-1"
+        ],
+        "bundle": "com.app.awesome",
+        "name": "Awesome App",
+        "domain": "awesomeapp.com",
+        "id": "123456789"
+      },
+      "imp": [
+        {
+          "id": "some-impression-id",
+          "tagid": "ogTAGID",
+          "native": {
+            "ver":"1.1",
+            "request":"{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}"
+          },
+          "ext": {
+            "bidder": {
+              "supply_id": "1"
+            }
+          }
+        }
+      ]
+    },
+    "httpCalls": [
+      {
+        "expectedRequest": {
+          "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1",
+          "body": {
+            "id": "some-request-id",
+            "device": {
+              "ua": "test-user-agent",
+              "ip": "123.123.123.123",
+              "language": "en",
+              "dnt": 0
+            },
+            "imp": [
+              {
+                "id": "some-impression-id",
+                "native": {
+                  "ver":"1.1",
+                  "request":"{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}"
+                },
+                "tagid": "ogTAGID"
+              }
+            ],
+            "app": {
+              "id": "123456789",
+              "name": "Awesome App",
+              "bundle": "com.app.awesome",
+              "domain": "awesomeapp.com",
+              "cat": [
+                "IAB22-1"
+              ],
+              "publisher": {
+                "id": "123456789"
+              }
+            },
+            "user": {
+              "buyeruid": "awesome-user"
+            },
+            "tmax": 1000
+          }
+        },
+        "mockResponse": {
+          "status": 200,
+          "body": {
+            "id": "awesome-resp-id",
+            "seatbid": [
+              {
+                "bid": [
+                  {
+                    "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+                    "impid": "some-impression-id",
+                    "price": 3.5,
+                    "adm": "awesome-markup",
+                    "adomain": [
+                      "awesome.com"
+                    ],
+                    "crid": "20",
+                    "mtype": 4
+                  }
+                ],
+                "seat": "smrtconnect"
+              }
+            ],
+            "cur": "USD",
+            "ext": {
+              "responsetimemillis": {
+                "smrtconnect": 154
+              },
+              "tmaxrequest": 1000
+            }
+          }
+        }
+      }
+    ],
+    "expectedBidResponses": [
+      {
+        "bids":[
+          {
+            "bid":  {
+              "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+              "impid": "some-impression-id",
+              "price": 3.5,
+              "adm": "awesome-markup",
+              "crid": "20",
+              "adomain": [
+                "awesome.com"
+              ],
+              "mtype": 4
+            },
+            "type": "native"
+          }
+        ]
+      }
+    ]
+  }
+  
\ No newline at end of file
diff --git a/adapters/smrtconnect/smrtconnecttest/exemplary/native-web.json b/adapters/smrtconnect/smrtconnecttest/exemplary/native-web.json
new file mode 100644
index 00000000..dd44926e
--- /dev/null
+++ b/adapters/smrtconnect/smrtconnecttest/exemplary/native-web.json
@@ -0,0 +1,124 @@
+{
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "device": {
+      "ua": "test-user-agent",
+      "ipv6": "2607:fb90:f27:4512:d800:cb23:a603:e245",
+      "language": "en",
+      "dnt": 0
+    },
+    "tmax": 1000,
+    "user": {
+      "buyeruid": "awesome-user"
+    },
+    "site": {
+      "page": "test.com",
+      "publisher": {
+        "id": "123456789"
+      }
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "tagid": "ogTAGID",
+        "native": {
+          "ver":"1.1",
+          "request":"{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}"
+        },
+        "ext": {
+          "bidder": {
+            "host": "ep1",
+            "supply_id": "1"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1",
+        "body": {
+          "id": "some-request-id",
+          "device": {
+            "ua": "test-user-agent",
+            "ipv6": "2607:fb90:f27:4512:d800:cb23:a603:e245",
+            "language": "en",
+            "dnt": 0
+          },
+          "imp": [
+            {
+              "id": "some-impression-id",
+              "tagid": "ogTAGID",
+              "native": {
+                "ver":"1.1",
+                "request":"{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}"
+              }
+            }
+          ],
+          "site": {
+            "page": "test.com",
+            "publisher": {
+              "id": "123456789"
+            }
+          },
+          "user": {
+            "buyeruid": "awesome-user"
+          },
+          "tmax": 1000
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "awesome-resp-id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+                  "impid": "some-impression-id",
+                  "price": 3.5,
+                  "adm": "awesome-markup",
+                  "adomain": [
+                    "awesome.com"
+                  ],
+                  "crid": "20",
+                  "mtype": 4
+                }
+              ],
+              "seat": "acuityads"
+            }
+          ],
+          "cur": "USD",
+          "ext": {
+            "responsetimemillis": {
+              "acuityads": 154
+            },
+            "tmaxrequest": 1000
+          }
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "bids":[
+        {
+          "bid":  {
+            "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+            "impid": "some-impression-id",
+            "price": 3.5,
+            "adm": "awesome-markup",
+            "crid": "20",
+            "adomain": [
+              "awesome.com"
+            ],
+            "mtype": 4
+          },
+          "type": "native"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/smrtconnect/smrtconnecttest/exemplary/video-app.json b/adapters/smrtconnect/smrtconnecttest/exemplary/video-app.json
new file mode 100644
index 00000000..88d30666
--- /dev/null
+++ b/adapters/smrtconnect/smrtconnecttest/exemplary/video-app.json
@@ -0,0 +1,149 @@
+{
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "device": {
+      "ua": "test-user-agent",
+      "ip": "123.123.123.123",
+      "language": "en",
+      "dnt": 0
+    },
+    "tmax": 1000,
+    "user": {
+      "buyeruid": "awesome-user"
+    },
+    "app": {
+      "publisher": {
+        "id": "123456789"
+      },
+      "cat": [
+        "IAB22-1"
+      ],
+      "bundle": "com.app.awesome",
+      "name": "Awesome App",
+      "domain": "awesomeapp.com",
+      "id": "123456789"
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "tagid": "ogTAGID",
+        "video": {
+          "mimes": [
+            "video/mp4"
+          ],
+          "w": 640,
+          "h": 480,
+          "minduration": 120,
+          "maxduration": 150
+        },
+        "ext": {
+          "bidder": {
+            "supply_id": "1"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1",
+        "body": {
+          "id": "some-request-id",
+          "device": {
+            "ua": "test-user-agent",
+            "ip": "123.123.123.123",
+            "language": "en",
+            "dnt": 0
+          },
+          "imp": [
+            {
+              "id": "some-impression-id",
+              "video": {
+                "mimes": [
+                  "video/mp4"
+                ],
+                "minduration": 120,
+                "maxduration": 150,
+                "w": 640,
+                "h": 480
+              },
+              "tagid": "ogTAGID"
+            }
+          ],
+          "app": {
+            "id": "123456789",
+            "name": "Awesome App",
+            "bundle": "com.app.awesome",
+            "domain": "awesomeapp.com",
+            "cat": [
+              "IAB22-1"
+            ],
+            "publisher": {
+              "id": "123456789"
+            }
+          },
+          "user": {
+            "buyeruid": "awesome-user"
+          },
+          "tmax": 1000
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "awesome-resp-id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+                  "impid": "some-impression-id",
+                  "price": 3.5,
+                  "adm": "awesome-markup",
+                  "adomain": [
+                    "awesome.com"
+                  ],
+                  "crid": "20",
+                  "w": 1280,
+                  "h": 720,
+                  "mtype": 2
+                }
+              ],
+              "seat": "smrtconnect"
+            }
+          ],
+          "cur": "USD",
+          "ext": {
+            "responsetimemillis": {
+              "smrtconnect": 154
+            },
+            "tmaxrequest": 1000
+          }
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "bids":[
+        {
+          "bid":  {
+            "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+            "impid": "some-impression-id",
+            "price": 3.5,
+            "adm": "awesome-markup",
+            "crid": "20",
+            "adomain": [
+              "awesome.com"
+            ],
+            "w": 1280,
+            "h": 720,
+            "mtype": 2
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/smrtconnect/smrtconnecttest/exemplary/video-web.json b/adapters/smrtconnect/smrtconnecttest/exemplary/video-web.json
new file mode 100644
index 00000000..cab53723
--- /dev/null
+++ b/adapters/smrtconnect/smrtconnecttest/exemplary/video-web.json
@@ -0,0 +1,147 @@
+{
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "device": {
+      "ua": "test-user-agent",
+      "ip": "123.123.123.123",
+      "language": "en",
+      "dnt": 0
+    },
+    "tmax": 1000,
+    "user": {
+      "buyeruid": "awesome-user"
+    },
+    "site": {
+      "page": "test.com",
+      "publisher": {
+        "id": "123456789"
+      }
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "tagid": "ogTAGID",
+        "video": {
+          "mimes": [
+            "video/mp4"
+          ],
+          "w": 640,
+          "h": 480,
+          "minduration": 120,
+          "maxduration": 150
+        },
+        "ext": {
+          "bidder": {
+            "supply_id": "1"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1",
+        "body": {
+          "id": "some-request-id",
+          "device": {
+            "ua": "test-user-agent",
+            "ip": "123.123.123.123",
+            "language": "en",
+            "dnt": 0
+          },
+          "imp": [
+            {
+              "id": "some-impression-id",
+              "tagid": "ogTAGID",
+              "video": {
+                "mimes": [
+                  "video/mp4"
+                ],
+                "minduration": 120,
+                "maxduration": 150,
+                "w": 640,
+                "h": 480
+              }
+            }
+          ],
+          "site": {
+            "page": "test.com",
+            "publisher": {
+              "id": "123456789"
+            }
+          },
+          "user": {
+            "buyeruid": "awesome-user"
+          },
+          "tmax": 1000
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "awesome-resp-id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+                  "impid": "some-impression-id",
+                  "price": 3.5,
+                  "adm": "awesome-markup",
+                  "adomain": [
+                    "awesome.com"
+                  ],
+                  "crid": "20",
+                  "w": 1280,
+                  "h": 720,
+                  "mtype": 2,
+                  "ext": {
+                    "prebid": {
+                      "type": "video"
+                    }
+                  }
+                }
+              ],
+              "seat": "smrtconnect"
+            }
+          ],
+          "cur": "USD",
+          "ext": {
+            "responsetimemillis": {
+              "smrtconnect": 154
+            },
+            "tmaxrequest": 1000
+          }
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "bids": [
+        {
+          "bid": {
+            "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+            "impid": "some-impression-id",
+            "price": 3.5,
+            "adm": "awesome-markup",
+            "adomain": [
+              "awesome.com"
+            ],
+            "crid": "20",
+            "w": 1280,
+            "h": 720,
+            "mtype": 2,
+            "ext": {
+              "prebid": {
+                "type": "video"
+              }
+            }
+          },
+          "type":"video"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/smrtconnect/smrtconnecttest/supplemental/empty-seatbid-array.json b/adapters/smrtconnect/smrtconnecttest/supplemental/empty-seatbid-array.json
new file mode 100644
index 00000000..719bc762
--- /dev/null
+++ b/adapters/smrtconnect/smrtconnecttest/supplemental/empty-seatbid-array.json
@@ -0,0 +1,119 @@
+{
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "device": {
+      "ua": "test-user-agent",
+      "ip": "123.123.123.123",
+      "language": "en",
+      "dnt": 0
+    },
+    "tmax": 1000,
+    "user": {
+      "buyeruid": "awesome-user"
+    },
+    "app": {
+      "publisher": {
+        "id": "123456789"
+      },
+      "cat": [
+        "IAB22-1"
+      ],
+      "bundle": "com.app.awesome",
+      "name": "Awesome App",
+      "domain": "awesomeapp.com",
+      "id": "123456789"
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "tagid": "ogTAGID",
+        "video": {
+          "mimes": [
+            "video/mp4"
+          ],
+          "w": 640,
+          "h": 480,
+          "minduration": 120,
+          "maxduration": 150
+        },
+        "ext": {
+          "bidder": {
+            "supply_id": "1"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1",
+        "body": {
+          "id": "some-request-id",
+          "device": {
+            "ua": "test-user-agent",
+            "ip": "123.123.123.123",
+            "language": "en",
+            "dnt": 0
+          },
+          "imp": [
+            {
+              "id": "some-impression-id",
+              "video": {
+                "mimes": [
+                  "video/mp4"
+                ],
+                "minduration": 120,
+                "maxduration": 150,
+                "w": 640,
+                "h": 480
+              },
+              "tagid": "ogTAGID"
+            }
+          ],
+          "app": {
+            "id": "123456789",
+            "name": "Awesome App",
+            "bundle": "com.app.awesome",
+            "domain": "awesomeapp.com",
+            "cat": [
+              "IAB22-1"
+            ],
+            "publisher": {
+              "id": "123456789"
+            }
+          },
+          "user": {
+            "buyeruid": "awesome-user"
+          },
+          "tmax": 1000
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "awesome-resp-id",
+          "seatbid": [
+          ],
+          "cur": "USD",
+          "ext": {
+            "responsetimemillis": {
+              "smrtconnect": 154
+            },
+            "tmaxrequest": 1000
+          }
+        }
+      }
+    }
+  ],
+    "mockResponse": {
+      "status": 200,
+      "body": "invalid response"
+    },
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Empty SeatBid array",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/smrtconnect/smrtconnecttest/supplemental/invalid-aceex-ext-object.json b/adapters/smrtconnect/smrtconnecttest/supplemental/invalid-aceex-ext-object.json
new file mode 100644
index 00000000..77752d01
--- /dev/null
+++ b/adapters/smrtconnect/smrtconnecttest/supplemental/invalid-aceex-ext-object.json
@@ -0,0 +1,29 @@
+{
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "ext.bidder not provided",
+      "comparison": "literal"
+    }
+  ],
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "tagid": "my-adcode",
+        "video": {
+          "mimes": ["video/mp4"],
+          "w": 640,
+          "h": 480,
+          "minduration": 120,
+          "maxduration": 150
+        },
+        "ext": "Awesome"
+      }
+    ],
+    "site": {
+      "page": "test.com"
+    }
+  },
+  "httpCalls": []
+}
diff --git a/adapters/smrtconnect/smrtconnecttest/supplemental/invalid-response.json b/adapters/smrtconnect/smrtconnecttest/supplemental/invalid-response.json
new file mode 100644
index 00000000..6ae0e5e4
--- /dev/null
+++ b/adapters/smrtconnect/smrtconnecttest/supplemental/invalid-response.json
@@ -0,0 +1,94 @@
+
+{
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "device": {
+      "ua": "test-user-agent",
+      "ip": "123.123.123.123",
+      "language": "en",
+      "dnt": 0
+    },
+    "tmax": 1000,
+    "user": {
+      "buyeruid": "awesome-user"
+    },
+    "app": {
+      "publisher": {
+        "id": "123456789"
+      },
+      "cat": [
+        "IAB22-1"
+      ],
+      "bundle": "com.app.awesome",
+      "name": "Awesome App",
+      "domain": "awesomeapp.com",
+      "id": "123456789"
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "tagid": "ogTAGID",
+        "banner": {
+          "w":320,
+          "h":50
+        },
+        "ext": {
+          "bidder": {
+            "supply_id": "1"
+          }
+        }
+      }
+    ]
+  },
+
+  "httpCalls": [{
+    "expectedRequest": {
+      "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1",
+      "body": {
+        "id": "some-request-id",
+        "device": {
+          "ua": "test-user-agent",
+          "ip": "123.123.123.123",
+          "language": "en",
+          "dnt": 0
+        },
+        "imp": [
+          {
+            "id": "some-impression-id",
+            "banner": {
+                "w":320,
+                "h":50
+            },
+            "tagid": "ogTAGID"
+          }
+        ],
+        "app": {
+          "id": "123456789",
+          "name": "Awesome App",
+          "bundle": "com.app.awesome",
+          "domain": "awesomeapp.com",
+          "cat": [
+            "IAB22-1"
+          ],
+          "publisher": {
+            "id": "123456789"
+          }
+        },
+        "user": {
+          "buyeruid": "awesome-user"
+        },
+        "tmax": 1000
+      }
+    },
+    "mockResponse": {
+      "status": 200,
+      "body": "invalid response"
+    }
+  }],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Bad Server Response",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/smrtconnect/smrtconnecttest/supplemental/status-code-bad-request.json b/adapters/smrtconnect/smrtconnecttest/supplemental/status-code-bad-request.json
new file mode 100644
index 00000000..daaa957c
--- /dev/null
+++ b/adapters/smrtconnect/smrtconnecttest/supplemental/status-code-bad-request.json
@@ -0,0 +1,92 @@
+
+{
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "tmax": 1000,
+    "user": {
+      "buyeruid": "awesome-user"
+    },
+    "app": {
+      "publisher": {
+        "id": "123456789"
+      },
+      "cat": [
+        "IAB22-1"
+      ],
+      "bundle": "com.app.awesome",
+      "name": "Awesome App",
+      "domain": "awesomeapp.com",
+      "id": "123456789"
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "tagid": "ogTAGID",
+        "video": {
+          "mimes": [
+            "video/mp4"
+          ],
+          "w": 640,
+          "h": 480,
+          "minduration": 120,
+          "maxduration": 150
+        },
+        "ext": {
+          "bidder": {
+            "supply_id": "1"
+          }
+        }
+      }
+    ]
+  },
+
+  "httpCalls": [{
+    "expectedRequest": {
+      "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1",
+      "body": {
+        "id": "some-request-id",
+        "imp": [
+          {
+            "id": "some-impression-id",
+            "video": {
+              "mimes": [
+                "video/mp4"
+              ],
+              "minduration": 120,
+              "maxduration": 150,
+              "w": 640,
+              "h": 480
+            },
+            "tagid": "ogTAGID"
+          }
+        ],
+        "app":  {
+          "publisher": {
+            "id": "123456789"
+          },
+          "cat": [
+            "IAB22-1"
+          ],
+          "bundle": "com.app.awesome",
+          "name": "Awesome App",
+          "domain": "awesomeapp.com",
+          "id": "123456789"
+        },
+        "user": {
+          "buyeruid": "awesome-user"
+        },
+        "tmax": 1000
+      }
+    },
+    "mockResponse": {
+      "status": 400
+    }
+  }],
+  "expectedBidResponses": [],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Unexpected status code: 400. Run with request.debug = 1 for more info",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/smrtconnect/smrtconnecttest/supplemental/status-code-no-content.json b/adapters/smrtconnect/smrtconnecttest/supplemental/status-code-no-content.json
new file mode 100644
index 00000000..734899fb
--- /dev/null
+++ b/adapters/smrtconnect/smrtconnecttest/supplemental/status-code-no-content.json
@@ -0,0 +1,68 @@
+{
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "tmax": 1000,
+    "user": {
+      "buyeruid": "awesome-user"
+    },
+    "site": {
+      "page": "test.com",
+      "publisher": {
+        "id": "123456789"
+      }
+    },
+    "imp": [{
+      "id": "some-impression-id",
+      "tagid": "ogTAGID",
+      "video": {
+        "mimes": ["video/mp4"],
+        "w": 640,
+        "h": 480,
+        "minduration": 120,
+        "maxduration": 150
+      },
+      "ext": {
+        "bidder": {
+          "supply_id": "1"
+        }
+      }
+    }]
+  },
+
+  "httpCalls": [{
+    "expectedRequest": {
+      "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1",
+      "body": {
+        "id": "some-request-id",
+        "imp": [{
+          "id": "some-impression-id",
+          "tagid": "ogTAGID",
+          "video": {
+            "mimes": [
+              "video/mp4"
+            ],
+            "minduration": 120,
+            "maxduration": 150,
+            "w": 640,
+            "h": 480
+          }
+        }],
+        "site": {
+          "page": "test.com",
+          "publisher": {
+            "id": "123456789"
+          }
+        },
+        "user": {
+          "buyeruid": "awesome-user"
+        },
+        "tmax": 1000
+      }
+    },
+    "mockResponse": {
+      "status": 204
+    }
+  }],
+  "expectedBidResponses": [],
+  "expectedMakeBidsErrors": []
+}
\ No newline at end of file
diff --git a/adapters/smrtconnect/smrtconnecttest/supplemental/status-code-other-error.json b/adapters/smrtconnect/smrtconnecttest/supplemental/status-code-other-error.json
new file mode 100644
index 00000000..3f3ecf01
--- /dev/null
+++ b/adapters/smrtconnect/smrtconnecttest/supplemental/status-code-other-error.json
@@ -0,0 +1,78 @@
+
+{
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "tmax": 1000,
+    "user": {
+      "buyeruid": "awesome-user"
+    },
+    "site": {
+      "page": "test.com",
+      "publisher": {
+        "id": "123456789"
+      }
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "tagid": "ogTAGID",
+        "video": {
+          "mimes": ["video/mp4"],
+          "w": 640,
+          "h": 480,
+          "minduration": 120,
+          "maxduration": 150
+        },
+        "ext": {
+          "bidder": {
+            "supply_id": "1"
+          }
+        }
+      }
+    ]
+  },
+
+  "httpCalls": [{
+    "expectedRequest": {
+      "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1",
+      "body": {
+        "id": "some-request-id",
+        "imp": [
+          {
+            "id": "some-impression-id",
+            "tagid": "ogTAGID",
+            "video": {
+              "mimes": [
+                "video/mp4"
+              ],
+              "minduration": 120,
+              "maxduration": 150,
+              "w": 640,
+              "h": 480
+            }
+          }
+        ],
+        "site": {
+          "page": "test.com",
+          "publisher": {
+            "id": "123456789"
+          }
+        },
+        "user": {
+          "buyeruid": "awesome-user"
+        },
+        "tmax": 1000
+      }
+    },
+    "mockResponse": {
+      "status": 306
+    }
+  }],
+  "expectedBidResponses": [],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Unexpected status code: 306. Run with request.debug = 1 for more info",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/smrtconnect/smrtconnecttest/supplemental/status-code-service-unavailable.json b/adapters/smrtconnect/smrtconnecttest/supplemental/status-code-service-unavailable.json
new file mode 100644
index 00000000..2e0212f1
--- /dev/null
+++ b/adapters/smrtconnect/smrtconnecttest/supplemental/status-code-service-unavailable.json
@@ -0,0 +1,78 @@
+
+{
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "tmax": 1000,
+    "user": {
+      "buyeruid": "awesome-user"
+    },
+    "site": {
+      "page": "test.com",
+      "publisher": {
+        "id": "123456789"
+      }
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "tagid": "ogTAGID",
+        "video": {
+          "mimes": ["video/mp4"],
+          "w": 640,
+          "h": 480,
+          "minduration": 120,
+          "maxduration": 150
+        },
+        "ext": {
+          "bidder": {
+            "supply_id": "1"
+          }
+        }
+      }
+    ]
+  },
+
+  "httpCalls": [{
+    "expectedRequest": {
+      "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1",
+      "body": {
+        "id": "some-request-id",
+        "imp": [
+          {
+            "id": "some-impression-id",
+            "tagid": "ogTAGID",
+            "video": {
+              "mimes": [
+                "video/mp4"
+              ],
+              "minduration": 120,
+              "maxduration": 150,
+              "w": 640,
+              "h": 480
+            }
+          }
+        ],
+        "site": {
+          "page": "test.com",
+          "publisher": {
+            "id": "123456789"
+          }
+        },
+        "user": {
+          "buyeruid": "awesome-user"
+        },
+        "tmax": 1000
+      }
+    },
+    "mockResponse": {
+      "status": 503
+    }
+  }],
+  "expectedBidResponses": [],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Unexpected status code: 503. Run with request.debug = 1 for more info",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/config/config_test.go b/config/config_test.go
index 3cad8fa1..4c97f125 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -11,6 +11,7 @@ import (
 
 	"github.com/prebid/go-gdpr/consentconstants"
 	"github.com/prebid/prebid-server/v2/openrtb_ext"
+	"github.com/prebid/prebid-server/v2/util/ptrutil"
 	"github.com/spf13/viper"
 	"github.com/stretchr/testify/assert"
 )
@@ -203,6 +204,7 @@ func TestDefaults(t *testing.T) {
 	cmpInts(t, "account_defaults.price_floors.fetch.period_sec", 3600, cfg.AccountDefaults.PriceFloors.Fetcher.Period)
 	cmpInts(t, "account_defaults.price_floors.fetch.max_age_sec", 86400, cfg.AccountDefaults.PriceFloors.Fetcher.MaxAge)
 	cmpInts(t, "account_defaults.price_floors.fetch.max_schema_dims", 0, cfg.AccountDefaults.PriceFloors.Fetcher.MaxSchemaDims)
+	cmpStrings(t, "account_defaults.privacy.topicsdomain", "", cfg.AccountDefaults.Privacy.PrivacySandbox.TopicsDomain)
 	cmpBools(t, "account_defaults.privacy.privacysandbox.cookiedeprecation.enabled", false, cfg.AccountDefaults.Privacy.PrivacySandbox.CookieDeprecation.Enabled)
 	cmpInts(t, "account_defaults.privacy.privacysandbox.cookiedeprecation.ttl_sec", 604800, cfg.AccountDefaults.Privacy.PrivacySandbox.CookieDeprecation.TTLSec)
 
@@ -527,7 +529,11 @@ account_defaults:
             anon_keep_bits: 50
         ipv4:
             anon_keep_bits: 20
+        dsa:
+            default: "{\"dsarequired\":3,\"pubrender\":1,\"datatopub\":2,\"transparency\":[{\"domain\":\"domain.com\",\"dsaparams\":[1]}]}"
+            gdpr_only: true
         privacysandbox:
+            topicsdomain: "test.com"
             cookiedeprecation:
                 enabled: true
                 ttl_sec: 86400
@@ -660,11 +666,30 @@ func TestFullConfig(t *testing.T) {
 	cmpInts(t, "account_defaults.price_floors.fetch.max_age_sec", 6000, cfg.AccountDefaults.PriceFloors.Fetcher.MaxAge)
 	cmpInts(t, "account_defaults.price_floors.fetch.max_schema_dims", 10, cfg.AccountDefaults.PriceFloors.Fetcher.MaxSchemaDims)
 
+	// Assert the DSA was correctly unmarshalled and DefaultUnpacked was built correctly
+	expectedDSA := AccountDSA{
+		Default: "{\"dsarequired\":3,\"pubrender\":1,\"datatopub\":2,\"transparency\":[{\"domain\":\"domain.com\",\"dsaparams\":[1]}]}",
+		DefaultUnpacked: &openrtb_ext.ExtRegsDSA{
+			Required:  ptrutil.ToPtr[int8](3),
+			PubRender: ptrutil.ToPtr[int8](1),
+			DataToPub: ptrutil.ToPtr[int8](2),
+			Transparency: []openrtb_ext.ExtBidDSATransparency{
+				{
+					Domain: "domain.com",
+					Params: []int{1},
+				},
+			},
+		},
+		GDPROnly: true,
+	}
+	assert.Equal(t, &expectedDSA, cfg.AccountDefaults.Privacy.DSA)
+
 	cmpBools(t, "account_defaults.events.enabled", true, cfg.AccountDefaults.Events.Enabled)
 
 	cmpInts(t, "account_defaults.privacy.ipv6.anon_keep_bits", 50, cfg.AccountDefaults.Privacy.IPv6Config.AnonKeepBits)
 	cmpInts(t, "account_defaults.privacy.ipv4.anon_keep_bits", 20, cfg.AccountDefaults.Privacy.IPv4Config.AnonKeepBits)
 
+	cmpStrings(t, "account_defaults.privacy.topicsdomain", "test.com", cfg.AccountDefaults.Privacy.PrivacySandbox.TopicsDomain)
 	cmpBools(t, "account_defaults.privacy.cookiedeprecation.enabled", true, cfg.AccountDefaults.Privacy.PrivacySandbox.CookieDeprecation.Enabled)
 	cmpInts(t, "account_defaults.privacy.cookiedeprecation.ttl_sec", 86400, cfg.AccountDefaults.Privacy.PrivacySandbox.CookieDeprecation.TTLSec)
 
@@ -1853,3 +1878,70 @@ func TestTCF2FeatureOneVendorException(t *testing.T) {
 		assert.Equal(t, tt.wantIsVendorException, value, tt.description)
 	}
 }
+
+func TestUnpackDSADefault(t *testing.T) {
+	tests := []struct {
+		name      string
+		giveDSA   *AccountDSA
+		wantError bool
+	}{
+		{
+			name:      "nil",
+			giveDSA:   nil,
+			wantError: false,
+		},
+		{
+			name: "empty",
+			giveDSA: &AccountDSA{
+				Default: "",
+			},
+			wantError: false,
+		},
+		{
+			name: "empty_json",
+			giveDSA: &AccountDSA{
+				Default: "{}",
+			},
+			wantError: false,
+		},
+		{
+			name: "well_formed",
+			giveDSA: &AccountDSA{
+				Default: "{\"dsarequired\":3,\"pubrender\":1,\"datatopub\":2,\"transparency\":[{\"domain\":\"domain.com\",\"dsaparams\":[1]}]}",
+			},
+			wantError: false,
+		},
+		{
+			name: "well_formed_with_extra_fields",
+			giveDSA: &AccountDSA{
+				Default: "{\"unmappedkey\":\"unmappedvalue\",\"dsarequired\":3,\"pubrender\":1,\"datatopub\":2,\"transparency\":[{\"domain\":\"domain.com\",\"dsaparams\":[1]}]}",
+			},
+			wantError: false,
+		},
+		{
+			name: "invalid_type",
+			giveDSA: &AccountDSA{
+				Default: "{\"dsarequired\":\"invalid\",\"pubrender\":1,\"datatopub\":2,\"transparency\":[{\"domain\":\"domain.com\",\"dsaparams\":[1]}]}",
+			},
+			wantError: true,
+		},
+		{
+			name: "invalid_malformed_missing_colon",
+			giveDSA: &AccountDSA{
+				Default: "{\"dsarequired\"3,\"pubrender\":1,\"datatopub\":2,\"transparency\":[{\"domain\":\"domain.com\",\"dsaparams\":[1]}]}",
+			},
+			wantError: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := UnpackDSADefault(tt.giveDSA)
+			if tt.wantError {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
diff --git a/dsa/writer_test.go b/dsa/writer_test.go
new file mode 100644
index 00000000..7f530512
--- /dev/null
+++ b/dsa/writer_test.go
@@ -0,0 +1,189 @@
+package dsa
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/prebid/openrtb/v20/openrtb2"
+	"github.com/prebid/prebid-server/v2/config"
+	"github.com/prebid/prebid-server/v2/openrtb_ext"
+	"github.com/prebid/prebid-server/v2/util/ptrutil"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestWrite(t *testing.T) {
+	requestDSAJSON := json.RawMessage(`{"dsa":{"dsarequired":2,"pubrender":1,"datatopub":1,"transparency":[{"domain":"example1.com","dsaparams":[1,2,3]}]}}`)
+	defaultDSAJSON := json.RawMessage(`{"dsa":{"dsarequired":3,"pubrender":2,"datatopub":2,"transparency":[{"domain":"example2.com","dsaparams":[4,5,6]}]}}`)
+	defaultDSA := &openrtb_ext.ExtRegsDSA{
+		DataToPub: ptrutil.ToPtr[int8](2),
+		Required:  ptrutil.ToPtr[int8](3),
+		PubRender: ptrutil.ToPtr[int8](2),
+		Transparency: []openrtb_ext.ExtBidDSATransparency{
+			{
+				Domain: "example2.com",
+				Params: []int{4, 5, 6},
+			},
+		},
+	}
+
+	tests := []struct {
+		name          string
+		giveConfig    *config.AccountDSA
+		giveGDPR      bool
+		giveRequest   *openrtb_ext.RequestWrapper
+		expectRequest *openrtb_ext.RequestWrapper
+	}{
+		{
+			name: "request_nil",
+			giveConfig: &config.AccountDSA{
+				DefaultUnpacked: defaultDSA,
+			},
+			giveRequest:   nil,
+			expectRequest: nil,
+		},
+		{
+			name:       "config_nil",
+			giveConfig: nil,
+			giveRequest: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Regs: &openrtb2.Regs{
+						Ext: nil,
+					},
+				},
+			},
+			expectRequest: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Regs: &openrtb2.Regs{
+						Ext: nil,
+					},
+				},
+			},
+		},
+		{
+			name: "config_default_nil",
+			giveConfig: &config.AccountDSA{
+				DefaultUnpacked: nil,
+			},
+			giveRequest: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Regs: &openrtb2.Regs{
+						Ext: nil,
+					},
+				},
+			},
+			expectRequest: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Regs: &openrtb2.Regs{
+						Ext: nil,
+					},
+				},
+			},
+		},
+		{
+			name: "request_dsa_present",
+			giveConfig: &config.AccountDSA{
+				DefaultUnpacked: defaultDSA,
+			},
+			giveRequest: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Regs: &openrtb2.Regs{
+						Ext: requestDSAJSON,
+					},
+				},
+			},
+			expectRequest: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Regs: &openrtb2.Regs{
+						Ext: requestDSAJSON,
+					},
+				},
+			},
+		},
+		{
+			name: "config_default_present_with_gdpr_only_set_and_gdpr_in_scope",
+			giveConfig: &config.AccountDSA{
+				DefaultUnpacked: defaultDSA,
+				GDPROnly:        true,
+			},
+			giveGDPR: true,
+			giveRequest: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{},
+			},
+			expectRequest: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Regs: &openrtb2.Regs{
+						Ext: defaultDSAJSON,
+					},
+				},
+			},
+		},
+		{
+			name: "config_default_present_with_gdpr_only_set_and_gdpr_not_in_scope",
+			giveConfig: &config.AccountDSA{
+				DefaultUnpacked: defaultDSA,
+				GDPROnly:        true,
+			},
+			giveGDPR: false,
+			giveRequest: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{},
+			},
+			expectRequest: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{},
+			},
+		},
+		{
+			name: "config_default_present_with_gdpr_only_not_set_and_gdpr_in_scope",
+			giveConfig: &config.AccountDSA{
+				DefaultUnpacked: defaultDSA,
+				GDPROnly:        false,
+			},
+			giveGDPR: true,
+			giveRequest: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{},
+			},
+			expectRequest: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Regs: &openrtb2.Regs{
+						Ext: defaultDSAJSON,
+					},
+				},
+			},
+		},
+		{
+			name: "config_default_present_with_gdpr_only_not_set_and_gdpr_not_in_scope",
+			giveConfig: &config.AccountDSA{
+				DefaultUnpacked: defaultDSA,
+				GDPROnly:        false,
+			},
+			giveGDPR: false,
+			giveRequest: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{},
+			},
+			expectRequest: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Regs: &openrtb2.Regs{
+						Ext: defaultDSAJSON,
+					},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			writer := Writer{
+				Config:      tt.giveConfig,
+				GDPRInScope: tt.giveGDPR,
+			}
+			err := writer.Write(tt.giveRequest)
+
+			if tt.giveRequest != nil {
+				tt.giveRequest.RebuildRequest()
+				assert.Equal(t, tt.expectRequest.BidRequest, tt.giveRequest.BidRequest)
+			} else {
+				assert.Nil(t, tt.giveRequest)
+			}
+			assert.Nil(t, err)
+		})
+	}
+}
diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go
index 08dec09e..6e6e1ac8 100644
--- a/endpoints/openrtb2/amp_auction_test.go
+++ b/endpoints/openrtb2/amp_auction_test.go
@@ -2052,7 +2052,7 @@ func TestAmpAuctionResponseHeaders(t *testing.T) {
 	)
 
 	for _, test := range testCases {
-		httpReq := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp"+test.requestURLArguments), nil)
+		httpReq := httptest.NewRequest("GET", "/openrtb2/auction/amp"+test.requestURLArguments, nil)
 		recorder := httptest.NewRecorder()
 
 		endpoint(recorder, httpReq, nil)
@@ -2479,3 +2479,87 @@ func TestSetSeatNonBid(t *testing.T) {
 		})
 	}
 }
+
+func TestAmpAuctionDebugWarningsOnly(t *testing.T) {
+	testCases := []struct {
+		description         string
+		requestURLArguments string
+		addRequestHeaders   func(r *http.Request)
+		expectedStatus      int
+		expectedWarnings    map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage
+	}{
+		{
+			description:         "debug_enabled_request_with_invalid_Sec-Browsing-Topics_header",
+			requestURLArguments: "?tag_id=1&debug=1",
+			addRequestHeaders: func(r *http.Request) {
+				r.Header.Add("Sec-Browsing-Topics", "foo")
+			},
+			expectedStatus: 200,
+			expectedWarnings: map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage{
+				"general": {
+					{
+						Code:    10012,
+						Message: "Invalid field in Sec-Browsing-Topics header: foo",
+					},
+				},
+			},
+		},
+		{
+			description:         "debug_disabled_request_with_invalid_Sec-Browsing-Topics_header",
+			requestURLArguments: "?tag_id=1",
+			addRequestHeaders: func(r *http.Request) {
+				r.Header.Add("Sec-Browsing-Topics", "foo")
+			},
+			expectedStatus:   200,
+			expectedWarnings: nil,
+		},
+	}
+
+	storedRequests := map[string]json.RawMessage{
+		"1": json.RawMessage(validRequest(t, "site.json")),
+	}
+	exchange := &nobidExchange{}
+	endpoint, _ := NewAmpEndpoint(
+		fakeUUIDGenerator{},
+		exchange,
+		newParamsValidator(t),
+		&mockAmpStoredReqFetcher{storedRequests},
+		empty_fetcher.EmptyFetcher{},
+		&config.Configuration{
+			MaxRequestSize: maxSize,
+			AccountDefaults: config.Account{
+				Privacy: config.AccountPrivacy{
+					PrivacySandbox: config.PrivacySandbox{
+						TopicsDomain: "abc",
+					},
+				},
+			},
+		},
+		&metricsConfig.NilMetricsEngine{},
+		analyticsBuild.New(&config.Analytics{}),
+		map[string]string{},
+		[]byte{},
+		openrtb_ext.BuildBidderMap(),
+		empty_fetcher.EmptyFetcher{},
+		hooks.EmptyPlanBuilder{},
+		nil,
+	)
+
+	for _, test := range testCases {
+		httpReq := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp"+test.requestURLArguments), nil)
+		test.addRequestHeaders(httpReq)
+		recorder := httptest.NewRecorder()
+
+		endpoint(recorder, httpReq, nil)
+
+		assert.Equal(t, test.expectedStatus, recorder.Result().StatusCode)
+
+		// Parse Response
+		var response AmpResponse
+		if err := jsonutil.UnmarshalValid(recorder.Body.Bytes(), &response); err != nil {
+			t.Fatalf("Error unmarshalling response: %s", err.Error())
+		}
+
+		assert.Equal(t, test.expectedWarnings, response.ORTB2.Ext.Warnings)
+	}
+}
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index 5356815d..af6f4a1b 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -13,6 +13,7 @@ import (
 	"net/http/httptest"
 	"os"
 	"path/filepath"
+	"sort"
 	"strings"
 	"testing"
 	"time"
@@ -4660,13 +4661,13 @@ func TestValidateNativeAssetData(t *testing.T) {
 func TestAuctionResponseHeaders(t *testing.T) {
 	testCases := []struct {
 		description     string
-		requestBody     string
+		httpRequest     *http.Request
 		expectedStatus  int
 		expectedHeaders func(http.Header)
 	}{
 		{
 			description:    "Success Response",
-			requestBody:    validRequest(t, "site.json"),
+			httpRequest:    httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, "site.json"))),
 			expectedStatus: 200,
 			expectedHeaders: func(h http.Header) {
 				h.Set("X-Prebid", "pbs-go/unknown")
@@ -4675,12 +4676,39 @@ func TestAuctionResponseHeaders(t *testing.T) {
 		},
 		{
 			description:    "Failure Response",
-			requestBody:    "{}",
+			httpRequest:    httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader("{}")),
 			expectedStatus: 400,
 			expectedHeaders: func(h http.Header) {
 				h.Set("X-Prebid", "pbs-go/unknown")
 			},
 		},
+		{
+			description: "Success Response with Chrome BrowsingTopicsHeader",
+			httpRequest: func() *http.Request {
+				httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, "site.json")))
+				httpReq.Header.Add(secBrowsingTopics, "sample-value")
+				return httpReq
+			}(),
+			expectedStatus: 200,
+			expectedHeaders: func(h http.Header) {
+				h.Set("X-Prebid", "pbs-go/unknown")
+				h.Set("Content-Type", "application/json")
+				h.Set("Observe-Browsing-Topics", "?1")
+			},
+		},
+		{
+			description: "Failure Response with Chrome BrowsingTopicsHeader",
+			httpRequest: func() *http.Request {
+				httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader("{}"))
+				httpReq.Header.Add(secBrowsingTopics, "sample-value")
+				return httpReq
+			}(),
+			expectedStatus: 400,
+			expectedHeaders: func(h http.Header) {
+				h.Set("X-Prebid", "pbs-go/unknown")
+				h.Set("Observe-Browsing-Topics", "?1")
+			},
+		},
 	}
 
 	exchange := &nobidExchange{}
@@ -4702,10 +4730,9 @@ func TestAuctionResponseHeaders(t *testing.T) {
 	)
 
 	for _, test := range testCases {
-		httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(test.requestBody))
 		recorder := httptest.NewRecorder()
 
-		endpoint(recorder, httpReq, nil)
+		endpoint(recorder, test.httpRequest, nil)
 
 		expectedHeaders := http.Header{}
 		test.expectedHeaders(expectedHeaders)
@@ -6048,7 +6075,7 @@ func fakeNormalizeBidderName(name string) (openrtb_ext.BidderName, bool) {
 	return openrtb_ext.BidderName(strings.ToLower(name)), true
 }
 
-func TestValidateOrFillCDep(t *testing.T) {
+func TestValidateOrFillCookieDeprecation(t *testing.T) {
 	type args struct {
 		httpReq *http.Request
 		req     *openrtb_ext.RequestWrapper
@@ -6280,7 +6307,7 @@ func TestValidateOrFillCDep(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			err := validateOrFillCDep(tt.args.httpReq, tt.args.req, &tt.args.account)
+			err := validateOrFillCookieDeprecation(tt.args.httpReq, tt.args.req, &tt.args.account)
 			assert.Equal(t, tt.wantErr, err)
 			if tt.args.req != nil {
 				err := tt.args.req.RebuildRequest()
@@ -6436,3 +6463,168 @@ func TestValidateRequestCookieDeprecation(t *testing.T) {
 		assert.Equal(t, test.wantCDep, deviceExt.GetCDep())
 	}
 }
+
+func TestSetSecBrowsingTopicsImplicitly(t *testing.T) {
+	type args struct {
+		httpReq *http.Request
+		r       *openrtb_ext.RequestWrapper
+		account *config.Account
+	}
+	tests := []struct {
+		name     string
+		args     args
+		wantUser *openrtb2.User
+	}{
+		{
+			name: "empty HTTP request, no change in user data",
+			args: args{
+				httpReq: &http.Request{},
+				r:       &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}},
+				account: &config.Account{Privacy: config.AccountPrivacy{PrivacySandbox: config.PrivacySandbox{TopicsDomain: "ads.pubmatic.com"}}},
+			},
+			wantUser: nil,
+		},
+		{
+			name: "valid topic in request but topicsdomain not configured by host, no change in user data",
+			args: args{
+				httpReq: &http.Request{
+					Header: http.Header{
+						secBrowsingTopics: []string{"(1);v=chrome.1:1:2, ();p=P00000000000"},
+					},
+				},
+				r:       &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}},
+				account: &config.Account{Privacy: config.AccountPrivacy{PrivacySandbox: config.PrivacySandbox{TopicsDomain: ""}}},
+			},
+			wantUser: nil,
+		},
+		{
+			name: "valid topic in request and topicsdomain configured by host, topics copied to user data",
+			args: args{
+				httpReq: &http.Request{
+					Header: http.Header{
+						secBrowsingTopics: []string{"(1);v=chrome.1:1:2, ();p=P00000000000"},
+					},
+				},
+				r:       &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}},
+				account: &config.Account{Privacy: config.AccountPrivacy{PrivacySandbox: config.PrivacySandbox{TopicsDomain: "ads.pubmatic.com"}}},
+			},
+			wantUser: &openrtb2.User{
+				Data: []openrtb2.Data{
+					{
+						Name: "ads.pubmatic.com",
+						Segment: []openrtb2.Segment{
+							{
+								ID: "1",
+							},
+						},
+						Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`),
+					},
+				},
+			},
+		},
+		{
+			name: "valid empty topic in request, no change in user data",
+			args: args{
+				httpReq: &http.Request{
+					Header: http.Header{
+						secBrowsingTopics: []string{"();p=P0000000000000000000000000000000"},
+					},
+				},
+				r:       &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}},
+				account: &config.Account{Privacy: config.AccountPrivacy{PrivacySandbox: config.PrivacySandbox{TopicsDomain: "ads.pubmatic.com"}}},
+			},
+			wantUser: nil,
+		},
+		{
+			name: "request with a few valid topics (including duplicate topics, segIDs, matching segtax, segclass, etc) and a few invalid topics(different invalid format), only valid and unique topics copied/merged to/with user data",
+			args: args{
+				httpReq: &http.Request{
+					Header: http.Header{
+						secBrowsingTopics: []string{"(1);v=chrome.1:1:2, (1 2);v=chrome.1:1:2,(4);v=chrome.1:1:2,();p=P0000000000,(4);v=chrome.1, 5);v=chrome.1, (6;v=chrome.1, ();v=chrome.1, (	);v=chrome.1, (1);v=chrome.1:1:2, (1 2 4		6 7			4567	  ) ; v=chrome.1: 2 : 3,();p=P0000000000"},
+					},
+				},
+				r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{
+					User: &openrtb2.User{
+						Data: []openrtb2.Data{
+							{
+								Name: "chrome.com",
+								Segment: []openrtb2.Segment{
+									{ID: "1"},
+								},
+								Ext: json.RawMessage(`{"segtax":603,"segclass":"4"}`),
+							},
+							{
+								Name: "ads.pubmatic.com",
+								Segment: []openrtb2.Segment{
+									{ID: "1"},
+									{ID: "3"},
+								},
+								Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`),
+							},
+						},
+					},
+				}},
+				account: &config.Account{Privacy: config.AccountPrivacy{PrivacySandbox: config.PrivacySandbox{TopicsDomain: "ads.pubmatic.com"}}},
+			},
+			wantUser: &openrtb2.User{
+				Data: []openrtb2.Data{
+					{
+						Name: "chrome.com",
+						Segment: []openrtb2.Segment{
+							{ID: "1"},
+						},
+						Ext: json.RawMessage(`{"segtax":603,"segclass":"4"}`),
+					},
+					{
+						Name: "ads.pubmatic.com",
+						Segment: []openrtb2.Segment{
+							{ID: "1"},
+							{ID: "2"},
+							{ID: "3"},
+							{ID: "4"},
+						},
+						Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`),
+					},
+					{
+						Name: "ads.pubmatic.com",
+						Segment: []openrtb2.Segment{
+							{ID: "1"},
+							{ID: "2"},
+							{ID: "4"},
+							{ID: "6"},
+							{ID: "7"},
+							{ID: "4567"},
+						},
+						Ext: json.RawMessage(`{"segtax":601,"segclass":"3"}`),
+					},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			setSecBrowsingTopicsImplicitly(tt.args.httpReq, tt.args.r, tt.args.account)
+
+			// sequence is not guaranteed we're using a map to filter segids
+			sortUserData(tt.wantUser)
+			sortUserData(tt.args.r.User)
+			assert.Equal(t, tt.wantUser, tt.args.r.User, tt.name)
+		})
+	}
+}
+
+func sortUserData(user *openrtb2.User) {
+	if user != nil {
+		sort.Slice(user.Data, func(i, j int) bool {
+			if user.Data[i].Name == user.Data[j].Name {
+				return string(user.Data[i].Ext) < string(user.Data[j].Ext)
+			}
+			return user.Data[i].Name < user.Data[j].Name
+		})
+		for g := range user.Data {
+			sort.Slice(user.Data[g].Segment, func(i, j int) bool {
+				return user.Data[g].Segment[i].ID < user.Data[g].Segment[j].ID
+			})
+		}
+	}
+}
diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go
index 98d6ca35..cc522c91 100644
--- a/endpoints/openrtb2/video_auction_test.go
+++ b/endpoints/openrtb2/video_auction_test.go
@@ -1162,6 +1162,7 @@ func TestVideoAuctionResponseHeaders(t *testing.T) {
 	testCases := []struct {
 		description     string
 		givenTestFile   string
+		givenHeader     map[string]string
 		expectedStatus  int
 		expectedHeaders func(http.Header)
 	}{
@@ -1173,7 +1174,8 @@ func TestVideoAuctionResponseHeaders(t *testing.T) {
 				h.Set("X-Prebid", "pbs-go/unknown")
 				h.Set("Content-Type", "application/json")
 			},
-		}, {
+		},
+		{
 			description:    "Failure Response",
 			givenTestFile:  "sample-requests/video/video_invalid_sample.json",
 			expectedStatus: 500,
@@ -1181,6 +1183,27 @@ func TestVideoAuctionResponseHeaders(t *testing.T) {
 				h.Set("X-Prebid", "pbs-go/unknown")
 			},
 		},
+		{
+			description:    "Success Response with header Observe-Browsing-Topics",
+			givenTestFile:  "sample-requests/video/video_valid_sample.json",
+			givenHeader:    map[string]string{secBrowsingTopics: "anyValue"},
+			expectedStatus: 200,
+			expectedHeaders: func(h http.Header) {
+				h.Set("X-Prebid", "pbs-go/unknown")
+				h.Set("Content-Type", "application/json")
+				h.Set("Observe-Browsing-Topics", "?1")
+			},
+		},
+		{
+			description:    "Failure Response with header Observe-Browsing-Topics",
+			givenTestFile:  "sample-requests/video/video_invalid_sample.json",
+			givenHeader:    map[string]string{secBrowsingTopics: "anyValue"},
+			expectedStatus: 500,
+			expectedHeaders: func(h http.Header) {
+				h.Set("X-Prebid", "pbs-go/unknown")
+				h.Set("Observe-Browsing-Topics", "?1")
+			},
+		},
 	}
 
 	exchange := &mockExchangeVideo{}
@@ -1190,6 +1213,9 @@ func TestVideoAuctionResponseHeaders(t *testing.T) {
 		requestBody := readVideoTestFile(t, test.givenTestFile)
 
 		httpReq := httptest.NewRequest("POST", "/openrtb2/video", strings.NewReader(requestBody))
+		for k, v := range test.givenHeader {
+			httpReq.Header.Add(k, v)
+		}
 		recorder := httptest.NewRecorder()
 
 		endpoint.VideoAuctionEndpoint(recorder, httpReq, nil)
@@ -1472,3 +1498,18 @@ func readVideoTestFile(t *testing.T, filename string) string {
 
 	return string(getRequestPayload(t, requestData))
 }
+
+func TestVideoRequestValidationFailed(t *testing.T) {
+	ex := &mockExchangeVideo{}
+	reqBody := readVideoTestFile(t, "sample-requests/video/video_invalid_sample_negative_tmax.json")
+	req := httptest.NewRequest("POST", "/openrtb2/video", strings.NewReader(reqBody))
+	recorder := httptest.NewRecorder()
+
+	deps := mockDeps(t, ex)
+	deps.VideoAuctionEndpoint(recorder, req, nil)
+
+	errorMessage := recorder.Body.String()
+
+	assert.Equal(t, 500, recorder.Code, "Should catch error in request")
+	assert.Equal(t, "Critical error while running the video endpoint:  request.tmax must be nonnegative. Got -2", errorMessage, "Incorrect request validation message")
+}
diff --git a/errortypes/scope_test.go b/errortypes/scope_test.go
new file mode 100644
index 00000000..7d90d558
--- /dev/null
+++ b/errortypes/scope_test.go
@@ -0,0 +1,37 @@
+package errortypes
+
+import (
+	"errors"
+	"testing"
+)
+
+func TestReadScope(t *testing.T) {
+	tests := []struct {
+		name string
+		err  error
+		want Scope
+	}{
+		{
+			name: "scope-debug",
+			err:  &DebugWarning{Message: "scope is debug"},
+			want: ScopeDebug,
+		},
+		{
+			name: "scope-any",
+			err:  &Warning{Message: "scope is any"},
+			want: ScopeAny,
+		},
+		{
+			name: "default-error",
+			err:  errors.New("default error"),
+			want: ScopeAny,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := ReadScope(tt.err); got != tt.want {
+				t.Errorf("ReadScope() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index 78bc167b..87c3936f 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -978,6 +978,7 @@ func TestFloorsSignalling(t *testing.T) {
 			Account:           config.Account{DebugAllow: true, PriceFloors: config.AccountPriceFloors{Enabled: test.floorsEnable, MaxRule: 100, MaxSchemaDims: 5}},
 			UserSyncs:         &emptyUsersync{},
 			HookExecutor:      &hookexecution.EmptyHookExecutor{},
+			TCF2Config:        gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
 		}
 		outBidResponse, err := e.HoldAuction(context.Background(), auctionRequest, &DebugLog{})
 
@@ -2170,7 +2171,13 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
 		impExtInfoMap[impID] = ImpExtInfo{}
 	}
 
-	activityControl := privacy.NewActivityControl(spec.AccountPrivacy)
+	if spec.AccountPrivacy.DSA != nil && len(spec.AccountPrivacy.DSA.Default) > 0 {
+		if err := jsonutil.Unmarshal([]byte(spec.AccountPrivacy.DSA.Default), &spec.AccountPrivacy.DSA.DefaultUnpacked); err != nil {
+			t.Errorf("%s: Exchange returned an unexpected error. Got %s", filename, err.Error())
+		}
+	}
+
+	activityControl := privacy.NewActivityControl(&spec.AccountPrivacy)
 
 	auctionRequest := &AuctionRequest{
 		BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: &spec.IncomingRequest.OrtbRequest},
@@ -2181,6 +2188,7 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
 			},
 			DebugAllow:  true,
 			PriceFloors: config.AccountPriceFloors{Enabled: spec.AccountFloorsEnabled},
+			Privacy:     spec.AccountPrivacy,
 			Validations: spec.AccountConfigBidValidation,
 		},
 		UserSyncs:     mockIdFetcher(spec.IncomingRequest.Usersyncs),
@@ -5488,7 +5496,7 @@ type exchangeSpec struct {
 	FledgeEnabled              bool                   `json:"fledge_enabled,omitempty"`
 	MultiBid                   *multiBidSpec          `json:"multiBid,omitempty"`
 	Server                     exchangeServer         `json:"server,omitempty"`
-	AccountPrivacy             *config.AccountPrivacy `json:"accountPrivacy,omitempty"`
+	AccountPrivacy             config.AccountPrivacy  `json:"accountPrivacy,omitempty"`
 }
 
 type multiBidSpec struct {
diff --git a/exchange/exchangetest/dsa-default-ignored.json b/exchange/exchangetest/dsa-default-ignored.json
new file mode 100644
index 00000000..d70009e5
--- /dev/null
+++ b/exchange/exchangetest/dsa-default-ignored.json
@@ -0,0 +1,147 @@
+{
+    "accountPrivacy": {
+        "dsa": {
+            "default": "{\"dsarequired\":2,\"pubrender\":1,\"datatopub\":2,\"transparency\":[{\"domain\":\"platform1domain.com\",\"dsaparams\":[1]},{\"domain\":\"SSP2domain.com\",\"dsaparams\":[1,2]}]}"
+        }
+    },
+    "incomingRequest": {
+        "ortbRequest": {
+            "id": "some-request-id",
+            "site": {
+                "page": "test.somepage.com"
+            },
+            "imp": [{
+                "id": "my-imp-id",
+                "video": {
+                    "mimes": [
+                        "video/mp4"
+                    ]
+                },
+                "ext": {
+                    "prebid": {
+                        "bidder": {
+                            "appnexus": {
+                                "placementId": 1
+                            }
+                        }
+                    }
+                }
+            }],
+            "regs": {
+                "ext": {
+                    "dsa": {
+                        "dsarequired": 1,
+                        "pubrender": 0,
+                        "datatopub": 2,
+                        "transparency": [{
+                            "domain": "platform2domain.com",
+                            "dsaparams": [1, 2, 3]
+                        }]
+                    }
+                }
+            }
+        }
+    },
+    "outgoingRequests": {
+        "appnexus": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [{
+                        "id": "my-imp-id",
+                        "video": {
+                            "mimes": [
+                                "video/mp4"
+                            ]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "placementId": 1
+                            }
+                        }
+                    }],
+                    "regs": {
+                        "ext": {
+                            "dsa": {
+                                "dsarequired": 1,
+                                "pubrender": 0,
+                                "datatopub": 2,
+                                "transparency": [{
+                                    "domain": "platform2domain.com",
+                                    "dsaparams": [1, 2, 3]
+                                }]
+                            }
+                        }
+                    }
+                }
+            },
+            "mockResponse": {
+                "pbsSeatBids": [{
+                    "pbsBids": [{
+                        "ortbBid": {
+                            "id": "apn-bid",
+                            "impid": "my-imp-id",
+                            "price": 0.3,
+                            "w": 200,
+                            "h": 250,
+                            "crid": "creative-1",
+                            "ext": {
+                                "someField": "someValue",
+                                "origbidcpm": 0.3,
+                                "dsa": {
+                                    "behalf": "Advertiser",
+                                    "paid": "Advertiser",
+                                    "transparency": [{
+                                        "domain": "dsp1domain.com",
+                                        "dsaparams": [1, 2]
+                                    }],
+                                    "adrender": 1
+                                }
+                            }
+                        },
+                        "bidType": "video"
+                    }],
+                    "seat": "appnexus"
+                }]
+            }
+        }
+    },
+    "response": {
+        "bids": {
+            "id": "some-request-id",
+            "seatbid": [{
+                "seat": "appnexus",
+                "bid": [{
+                    "id": "apn-bid",
+                    "impid": "my-imp-id",
+                    "price": 0.3,
+                    "w": 200,
+                    "h": 250,
+                    "crid": "creative-1",
+                    "ext": {
+                        "someField": "someValue",
+                        "origbidcpm": 0.3,
+                        "prebid": {
+                            "meta": {
+                                "adaptercode": "appnexus"
+                            },
+                            "type": "video"
+                        },
+                        "dsa": {
+                            "behalf": "Advertiser",
+                            "paid": "Advertiser",
+                            "transparency": [{
+                                "domain": "dsp1domain.com",
+                                "dsaparams": [1, 2]
+                            }],
+                            "adrender": 1
+                        }
+                    }
+                }]
+            }]
+        }
+    }
+}
\ No newline at end of file
diff --git a/exchange/exchangetest/dsa-default.json b/exchange/exchangetest/dsa-default.json
new file mode 100644
index 00000000..efac1441
--- /dev/null
+++ b/exchange/exchangetest/dsa-default.json
@@ -0,0 +1,206 @@
+{
+    "accountPrivacy": {
+        "dsa": {
+            "default": "{\"dsarequired\":2,\"pubrender\":1,\"datatopub\":2,\"transparency\":[{\"domain\":\"platform1domain.com\",\"dsaparams\":[1]},{\"domain\":\"SSP2domain.com\",\"dsaparams\":[1,2]}]}"
+        }
+    },
+    "incomingRequest": {
+        "ortbRequest": {
+            "id": "some-request-id",
+            "site": {
+                "page": "test.somepage.com"
+            },
+            "imp": [{
+                "id": "my-imp-id",
+                "video": {
+                    "mimes": [
+                        "video/mp4"
+                    ]
+                },
+                "ext": {
+                    "prebid": {
+                        "bidder": {
+                            "appnexus": {
+                                "placementId": 1
+                            },
+                            "audienceNetwork": {
+                                "placementId": "some-placement"
+                            }
+                        }
+                    }
+                }
+            }]
+        }
+    },
+    "outgoingRequests": {
+        "appnexus": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [{
+                        "id": "my-imp-id",
+                        "video": {
+                            "mimes": [
+                                "video/mp4"
+                            ]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "placementId": 1
+                            }
+                        }
+                    }],
+                    "regs": {
+                        "ext": {
+                            "dsa": {
+                                "dsarequired": 2,
+                                "pubrender": 1,
+                                "datatopub": 2,
+                                "transparency": [{
+                                        "domain": "platform1domain.com",
+                                        "dsaparams": [1]
+                                    },
+                                    {
+                                        "domain": "SSP2domain.com",
+                                        "dsaparams": [1, 2]
+                                    }
+                                ]
+                            }
+                        }
+                    }
+                }
+            },
+            "mockResponse": {
+                "pbsSeatBids": [{
+                    "pbsBids": [{
+                        "ortbBid": {
+                            "id": "apn-bid",
+                            "impid": "my-imp-id",
+                            "price": 0.3,
+                            "w": 200,
+                            "h": 250,
+                            "crid": "creative-1",
+                            "ext": {
+                                "someField": "someValue",
+                                "origbidcpm": 0.3,
+                                "dsa": {
+                                    "behalf": "Advertiser",
+                                    "paid": "Advertiser",
+                                    "transparency": [{
+                                        "domain": "dsp1domain.com",
+                                        "dsaparams": [1, 2]
+                                    }],
+                                    "adrender": 1
+                                }
+                            }
+                        },
+                        "bidType": "video"
+                    }],
+                    "seat": "appnexus"
+                }]
+            }
+        },
+        "audienceNetwork": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [{
+                        "id": "my-imp-id",
+                        "video": {
+                            "mimes": [
+                                "video/mp4"
+                            ]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "placementId": "some-placement"
+                            }
+                        }
+                    }],
+                    "regs": {
+                        "ext": {
+                            "dsa": {
+                                "dsarequired": 2,
+                                "pubrender": 1,
+                                "datatopub": 2,
+                                "transparency": [{
+                                        "domain": "platform1domain.com",
+                                        "dsaparams": [1]
+                                    },
+                                    {
+                                        "domain": "SSP2domain.com",
+                                        "dsaparams": [1, 2]
+                                    }
+                                ]
+                            }
+                        }
+                    }
+                }
+            },
+            "mockResponse": {
+                "pbsSeatBids": [{
+                    "pbsBids": [{
+                        "ortbBid": {
+                            "id": "apn-bid",
+                            "impid": "my-imp-id",
+                            "price": 0.3,
+                            "w": 200,
+                            "h": 250,
+                            "crid": "creative-1",
+                            "ext": {
+                                "someField": "someValue",
+                                "origbidcpm": 0.3
+                            }
+                        },
+                        "bidType": "video"
+                    }],
+                    "seat": "audienceNetwork"
+                }]
+            }
+        }
+    },
+    "response": {
+        "bids": {
+            "id": "some-request-id",
+            "seatbid": [{
+                "seat": "appnexus",
+                "bid": [{
+                    "id": "apn-bid",
+                    "impid": "my-imp-id",
+                    "price": 0.3,
+                    "w": 200,
+                    "h": 250,
+                    "crid": "creative-1",
+                    "ext": {
+                        "someField": "someValue",
+                        "origbidcpm": 0.3,
+                        "prebid": {
+                            "meta": {
+                                "adaptercode": "appnexus"
+                            },
+                            "type": "video"
+                        },
+                        "dsa": {
+                            "behalf": "Advertiser",
+                            "paid": "Advertiser",
+                            "transparency": [{
+                                "domain": "dsp1domain.com",
+                                "dsaparams": [1, 2]
+                            }],
+                            "adrender": 1
+                        }
+                    }
+                }]
+            }, {
+                "seat": "audienceNetwork",
+                "bid": []
+            }]
+        }
+    }
+}
\ No newline at end of file
diff --git a/exchange/utils_test.go b/exchange/utils_test.go
index 2354e17b..3b723d4e 100644
--- a/exchange/utils_test.go
+++ b/exchange/utils_test.go
@@ -3219,6 +3219,7 @@ func TestCleanOpenRTBRequestsBidAdjustment(t *testing.T) {
 			BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req},
 			UserSyncs:         &emptyUsersync{},
 			Account:           accountConfig,
+			TCF2Config:        gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
 		}
 		gdprPermissionsBuilder := fakePermissionsBuilder{
 			permissions: &permissionsMock{
@@ -4666,6 +4667,7 @@ func TestCleanOpenRTBRequestsActivities(t *testing.T) {
 						AnonKeepBits: 16,
 					},
 				}},
+				TCF2Config: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
 			}
 
 			bidderToSyncerKey := map[string]string{}
diff --git a/floors/floors_test.go b/floors/floors_test.go
index f5f71df4..b9618bb8 100644
--- a/floors/floors_test.go
+++ b/floors/floors_test.go
@@ -91,7 +91,7 @@ func TestEnrichWithPriceFloors(t *testing.T) {
 		expFloorVal       float64
 		expFloorCur       string
 		expPriceFlrLoc    string
-		expSchemaVersion  string
+		expSchemaVersion  int
 	}{
 		{
 			name: "Floors disabled in account config",
@@ -178,7 +178,7 @@ func TestEnrichWithPriceFloors(t *testing.T) {
 						Publisher: &openrtb2.Publisher{Domain: "www.website.com"},
 					},
 					Imp: []openrtb2.Imp{{ID: "1234", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}}},
-					Ext: json.RawMessage(`{"prebid":{"floors":{"floormin":11,"floormincur":"USD","data":{"currency":"USD","floorsschemaversion":"2","modelgroups":[{"modelweight":50,"modelversion":"version2","schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|*|*":11.01,"*|*|www.website1.com":17.01},"default":21},{"modelweight":50,"modelversion":"version11","skiprate":110,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|300x250|*":11.01,"*|300x250|www.website1.com":100.01},"default":21}]},"enforcement":{"enforcepbs":true,"floordeals":true},"enabled":true}}}`),
+					Ext: json.RawMessage(`{"prebid":{"floors":{"floormin":11,"floormincur":"USD","data":{"currency":"USD","floorsschemaversion":2,"modelgroups":[{"modelweight":50,"modelversion":"version2","schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|*|*":11.01,"*|*|www.website1.com":17.01},"default":21},{"modelweight":50,"modelversion":"version11","skiprate":110,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|300x250|*":11.01,"*|300x250|www.website1.com":100.01},"default":21}]},"enforcement":{"enforcepbs":true,"floordeals":true},"enabled":true}}}`),
 				},
 			},
 			account:          testAccountConfig,
@@ -186,7 +186,7 @@ func TestEnrichWithPriceFloors(t *testing.T) {
 			expFloorVal:      11.01,
 			expFloorCur:      "USD",
 			expPriceFlrLoc:   openrtb_ext.RequestLocation,
-			expSchemaVersion: "2",
+			expSchemaVersion: 2,
 		},
 		{
 			name: "Rule selection with Site object, banner|300x600|www.website.com",
@@ -385,7 +385,7 @@ func TestEnrichWithPriceFloors(t *testing.T) {
 						assert.Equal(t, *requestExt.GetPrebid().Floors.Skipped, tc.Skipped, tc.name)
 					} else {
 						assert.Equal(t, requestExt.GetPrebid().Floors.PriceFloorLocation, tc.expPriceFlrLoc, tc.name)
-						if tc.expSchemaVersion != "" {
+						if tc.expSchemaVersion != 0 {
 							assert.Equal(t, requestExt.GetPrebid().Floors.Data.FloorsSchemaVersion, tc.expSchemaVersion, tc.name)
 						}
 					}
diff --git a/floors/validate_test.go b/floors/validate_test.go
index 59d08afc..ccb75c7a 100644
--- a/floors/validate_test.go
+++ b/floors/validate_test.go
@@ -56,13 +56,12 @@ func TestValidateFloorParams(t *testing.T) {
 			Err:      errors.New("Invalid FloorMin = '-10', value should be >= 0"),
 		},
 		{
-			name: "Invalid FloorSchemaVersion ",
+			name: "Invalid FloorSchemaVersion 2",
 			floorExt: &openrtb_ext.PriceFloorRules{Data: &openrtb_ext.PriceFloorData{
-				FloorsSchemaVersion: "1",
+				FloorsSchemaVersion: 1,
 				ModelGroups: []openrtb_ext.PriceFloorModelGroup{{
 					ModelVersion: "Version 1",
-
-					Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}, Delimiter: "|"},
+					Schema:       openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}, Delimiter: "|"},
 					Values: map[string]float64{
 						"banner|300x250|www.website.com": 1.01,
 						"banner|300x600|*":               4.01,
@@ -70,6 +69,46 @@ func TestValidateFloorParams(t *testing.T) {
 				}}},
 			Err: errors.New("Invalid FloorsSchemaVersion = '1', supported version 2"),
 		},
+		{
+			name: "Invalid FloorSchemaVersion -2",
+			floorExt: &openrtb_ext.PriceFloorRules{Data: &openrtb_ext.PriceFloorData{
+				FloorsSchemaVersion: -2,
+				ModelGroups: []openrtb_ext.PriceFloorModelGroup{{
+					ModelVersion: "Version 1",
+					Schema:       openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}, Delimiter: "|"},
+					Values: map[string]float64{
+						"banner|300x250|www.website.com": 1.01,
+						"banner|300x600|*":               4.01,
+					}, Default: 0.01},
+				}}},
+			Err: errors.New("Invalid FloorsSchemaVersion = '-2', supported version 2"),
+		},
+		{
+			name: "Valid FloorSchemaVersion 0",
+			floorExt: &openrtb_ext.PriceFloorRules{Data: &openrtb_ext.PriceFloorData{
+				FloorsSchemaVersion: 0,
+				ModelGroups: []openrtb_ext.PriceFloorModelGroup{{
+					ModelVersion: "Version 1",
+					Schema:       openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}, Delimiter: "|"},
+					Values: map[string]float64{
+						"banner|300x250|www.website.com": 1.01,
+						"banner|300x600|*":               4.01,
+					}, Default: 0.01},
+				}}},
+		},
+		{
+			name: "Valid FloorSchemaVersion 2",
+			floorExt: &openrtb_ext.PriceFloorRules{Data: &openrtb_ext.PriceFloorData{
+				FloorsSchemaVersion: 2,
+				ModelGroups: []openrtb_ext.PriceFloorModelGroup{{
+					ModelVersion: "Version 1",
+					Schema:       openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}, Delimiter: "|"},
+					Values: map[string]float64{
+						"banner|300x250|www.website.com": 1.01,
+						"banner|300x600|*":               4.01,
+					}, Default: 0.01},
+				}}},
+		},
 	}
 	for _, tc := range tt {
 		t.Run(tc.name, func(t *testing.T) {
diff --git a/openrtb_ext/floors_test.go b/openrtb_ext/floors_test.go
index d6c35b9f..687101bf 100644
--- a/openrtb_ext/floors_test.go
+++ b/openrtb_ext/floors_test.go
@@ -327,7 +327,7 @@ func TestFloorRulesDeepCopy(t *testing.T) {
 				Data: &PriceFloorData{
 					Currency:            "INR",
 					SkipRate:            0,
-					FloorsSchemaVersion: "2",
+					FloorsSchemaVersion: 2,
 					ModelTimestamp:      123,
 					ModelGroups: []PriceFloorModelGroup{
 						{
@@ -370,7 +370,7 @@ func TestFloorRulesDeepCopy(t *testing.T) {
 				Data: &PriceFloorData{
 					Currency:            "INR",
 					SkipRate:            0,
-					FloorsSchemaVersion: "2",
+					FloorsSchemaVersion: 2,
 					ModelTimestamp:      123,
 					ModelGroups: []PriceFloorModelGroup{
 						{
diff --git a/openrtb_ext/regs_test.go b/openrtb_ext/regs_test.go
new file mode 100644
index 00000000..a32c0362
--- /dev/null
+++ b/openrtb_ext/regs_test.go
@@ -0,0 +1,97 @@
+package openrtb_ext
+
+import (
+	"testing"
+
+	"github.com/prebid/prebid-server/v2/util/ptrutil"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestExtRegsDSAClone(t *testing.T) {
+	tests := []struct {
+		name       string
+		extRegsDSA *ExtRegsDSA
+	}{
+		{
+			name:       "nil",
+			extRegsDSA: nil,
+		},
+		{
+			name: "required_not_nil",
+			extRegsDSA: &ExtRegsDSA{
+				Required: ptrutil.ToPtr[int8](1),
+			},
+		},
+		{
+			name: "pubrender_not_nil",
+			extRegsDSA: &ExtRegsDSA{
+				PubRender: ptrutil.ToPtr[int8](1),
+			},
+		},
+		{
+			name: "datatopub_not_nil",
+			extRegsDSA: &ExtRegsDSA{
+				DataToPub: ptrutil.ToPtr[int8](1),
+			},
+		},
+		{
+			name: "transparency_empty",
+			extRegsDSA: &ExtRegsDSA{
+				Transparency: []ExtBidDSATransparency{},
+			},
+		},
+		{
+			name: "transparency_with_nil_params",
+			extRegsDSA: &ExtRegsDSA{
+				Transparency: []ExtBidDSATransparency{
+					{
+						Domain: "domain1",
+						Params: nil,
+					},
+				},
+			},
+		},
+		{
+			name: "transparency_with_params",
+			extRegsDSA: &ExtRegsDSA{
+				Required:  ptrutil.ToPtr[int8](1),
+				PubRender: ptrutil.ToPtr[int8](1),
+				DataToPub: ptrutil.ToPtr[int8](1),
+				Transparency: []ExtBidDSATransparency{
+					{
+						Domain: "domain1",
+						Params: []int{1, 2, 3},
+					},
+					{
+						Domain: "domain2",
+						Params: []int{4, 5, 6},
+					},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			clone := tt.extRegsDSA.Clone()
+			if tt.extRegsDSA == nil {
+				assert.Nil(t, clone)
+			} else {
+				assert.Equal(t, tt.extRegsDSA, clone)
+
+				if tt.extRegsDSA.Required != nil {
+					assert.NotSame(t, tt.extRegsDSA.Required, clone.Required)
+				}
+				if tt.extRegsDSA.PubRender != nil {
+					assert.NotSame(t, tt.extRegsDSA.PubRender, clone.PubRender)
+				}
+				if tt.extRegsDSA.DataToPub != nil {
+					assert.NotSame(t, tt.extRegsDSA.DataToPub, clone.DataToPub)
+				}
+				if tt.extRegsDSA.Transparency != nil {
+					assert.NotSame(t, tt.extRegsDSA.Transparency, clone.Transparency)
+				}
+			}
+		})
+	}
+}
diff --git a/privacysandbox/topics_test.go b/privacysandbox/topics_test.go
new file mode 100644
index 00000000..90a46f77
--- /dev/null
+++ b/privacysandbox/topics_test.go
@@ -0,0 +1,722 @@
+package privacysandbox
+
+import (
+	"encoding/json"
+	"sort"
+	"testing"
+
+	"github.com/prebid/openrtb/v20/openrtb2"
+	"github.com/prebid/prebid-server/v2/errortypes"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestParseTopicsFromHeader(t *testing.T) {
+	type args struct {
+		secBrowsingTopics string
+	}
+	tests := []struct {
+		name      string
+		args      args
+		wantTopic []Topic
+		wantError []error
+	}{
+		{
+			name:      "empty header",
+			args:      args{secBrowsingTopics: "	 "},
+			wantTopic: []Topic{},
+			wantError: nil,
+		},
+		{
+			name:      "invalid header value",
+			args:      args{secBrowsingTopics: "some-sec-cookie-value"},
+			wantTopic: []Topic{},
+			wantError: []error{
+				&errortypes.DebugWarning{
+					Message:     "Invalid field in Sec-Browsing-Topics header: some-sec-cookie-value",
+					WarningCode: errortypes.SecBrowsingTopicsWarningCode,
+				},
+			},
+		},
+		{
+			name:      "header with only finish padding",
+			args:      args{secBrowsingTopics: "();p=P0000000000000000000000000000000"},
+			wantTopic: []Topic{},
+			wantError: nil,
+		},
+		{
+			name: "header with one valid field",
+			args: args{secBrowsingTopics: "(1);v=chrome.1:1:2, ();p=P00000000000"},
+			wantTopic: []Topic{
+				{
+					SegTax:   600,
+					SegClass: "2",
+					SegIDs:   []int{1},
+				},
+			},
+			wantError: nil,
+		},
+		{
+			name: "header without finish padding",
+			args: args{secBrowsingTopics: "(1);v=chrome.1:1:2"},
+			wantTopic: []Topic{
+				{
+					SegTax:   600,
+					SegClass: "2",
+					SegIDs:   []int{1},
+				},
+			},
+			wantError: nil,
+		},
+		{
+			name: "header with more than 10 valid field, should return only 10",
+			args: args{secBrowsingTopics: "(1);v=chrome.1:1:2, (2);v=chrome.1:1:2, (3);v=chrome.1:1:2,  (4);v=chrome.1:1:2,  (5);v=chrome.1:1:2,  (6);v=chrome.1:1:2,  (7);v=chrome.1:1:2,  (8);v=chrome.1:1:2,  (9);v=chrome.1:1:2,  (10);v=chrome.1:1:2,  (11);v=chrome.1:1:2,  (12);v=chrome.1:1:2, ();p=P00000000000"},
+			wantTopic: []Topic{
+				{
+					SegTax:   600,
+					SegClass: "2",
+					SegIDs:   []int{1},
+				},
+				{
+					SegTax:   600,
+					SegClass: "2",
+					SegIDs:   []int{2},
+				},
+				{
+					SegTax:   600,
+					SegClass: "2",
+					SegIDs:   []int{3},
+				},
+				{
+					SegTax:   600,
+					SegClass: "2",
+					SegIDs:   []int{4},
+				},
+				{
+					SegTax:   600,
+					SegClass: "2",
+					SegIDs:   []int{5},
+				},
+				{
+					SegTax:   600,
+					SegClass: "2",
+					SegIDs:   []int{6},
+				},
+				{
+					SegTax:   600,
+					SegClass: "2",
+					SegIDs:   []int{7},
+				},
+				{
+					SegTax:   600,
+					SegClass: "2",
+					SegIDs:   []int{8},
+				},
+				{
+					SegTax:   600,
+					SegClass: "2",
+					SegIDs:   []int{9},
+				},
+				{
+					SegTax:   600,
+					SegClass: "2",
+					SegIDs:   []int{10},
+				},
+			},
+			wantError: []error{
+				&errortypes.DebugWarning{
+					Message:     "Invalid field in Sec-Browsing-Topics header: (11);v=chrome.1:1:2 discarded due to limit reached.",
+					WarningCode: errortypes.SecBrowsingTopicsWarningCode,
+				},
+				&errortypes.DebugWarning{
+					Message:     "Invalid field in Sec-Browsing-Topics header: (12);v=chrome.1:1:2 discarded due to limit reached.",
+					WarningCode: errortypes.SecBrowsingTopicsWarningCode,
+				},
+			},
+		},
+		{
+			name: "header with one valid field having multiple segIDs",
+			args: args{secBrowsingTopics: "(1 2);v=chrome.1:1:2, ();p=P00000000000"},
+			wantTopic: []Topic{
+				{
+					SegTax:   600,
+					SegClass: "2",
+					SegIDs:   []int{1, 2},
+				},
+			},
+			wantError: nil,
+		},
+		{
+			name: "header with two valid fields having different taxonomies",
+			args: args{secBrowsingTopics: "(1);v=chrome.1:1:2, (1);v=chrome.1:2:2, ();p=P0000000000"},
+			wantTopic: []Topic{
+				{
+					SegTax:   600,
+					SegClass: "2",
+					SegIDs:   []int{1},
+				},
+				{
+					SegTax:   601,
+					SegClass: "2",
+					SegIDs:   []int{1},
+				},
+			},
+			wantError: nil,
+		},
+		{
+			name: "header with one valid field and another invalid field (w/o segIDs), should return only one valid field",
+			args: args{secBrowsingTopics: "(1);v=chrome.1:2:3, ();v=chrome.1:2:3, ();p=P0000000000"},
+			wantTopic: []Topic{
+				{
+					SegTax:   601,
+					SegClass: "3",
+					SegIDs:   []int{1},
+				},
+			},
+			wantError: []error{
+				&errortypes.DebugWarning{
+					Message:     "Invalid field in Sec-Browsing-Topics header: ();v=chrome.1:2:3",
+					WarningCode: errortypes.SecBrowsingTopicsWarningCode,
+				},
+			},
+		},
+		{
+			name: "header with two valid fields having different model version",
+			args: args{secBrowsingTopics: "(1);v=chrome.1:2:3, (2);v=chrome.1:2:3, ();p=P0000000000"},
+			wantTopic: []Topic{
+				{
+					SegTax:   601,
+					SegClass: "3",
+					SegIDs:   []int{1},
+				},
+				{
+					SegTax:   601,
+					SegClass: "3",
+					SegIDs:   []int{2},
+				},
+			},
+			wantError: nil,
+		},
+		{
+			name: "header with one valid fields and two invalid fields (one with taxanomy < 0 and another with taxanomy > 10), should return only one valid field",
+			args: args{secBrowsingTopics: "(1);v=chrome.1:11:2, (1);v=chrome.1:5:6, (1);v=chrome.1:0:2, ();p=P0000000000"},
+			wantTopic: []Topic{
+				{
+					SegTax:   604,
+					SegClass: "6",
+					SegIDs:   []int{1},
+				},
+			},
+			wantError: []error{
+				&errortypes.DebugWarning{
+					Message:     "Invalid field in Sec-Browsing-Topics header: (1);v=chrome.1:11:2",
+					WarningCode: errortypes.SecBrowsingTopicsWarningCode,
+				},
+				&errortypes.DebugWarning{
+					Message:     "Invalid field in Sec-Browsing-Topics header: (1);v=chrome.1:0:2",
+					WarningCode: errortypes.SecBrowsingTopicsWarningCode,
+				},
+			},
+		},
+		{
+			name: "header with with valid fields having special characters (whitespaces, etc)",
+			args: args{secBrowsingTopics: "(1 2 4		6 7			4567	  ) ; v=chrome.1: 1 : 2, (1);v=chrome.1, ();p=P0000000000"},
+			wantTopic: []Topic{
+				{
+					SegTax:   600,
+					SegClass: "2",
+					SegIDs:   []int{1, 2, 4, 6, 7, 4567},
+				},
+			},
+			wantError: []error{
+				&errortypes.DebugWarning{
+					Message:     "Invalid field in Sec-Browsing-Topics header: (1);v=chrome.1",
+					WarningCode: errortypes.SecBrowsingTopicsWarningCode,
+				},
+			},
+		},
+		{
+			name:      "header with one valid field having a negative segId, drop field",
+			args:      args{secBrowsingTopics: "(1 -3);v=chrome.1:1:2, ();p=P00000000000"},
+			wantTopic: []Topic{},
+			wantError: []error{
+				&errortypes.DebugWarning{
+					Message:     "Invalid field in Sec-Browsing-Topics header: (1 -3);v=chrome.1:1:2",
+					WarningCode: errortypes.SecBrowsingTopicsWarningCode,
+				},
+			},
+		},
+		{
+			name:      "header with one valid field having a segId=0, drop field",
+			args:      args{secBrowsingTopics: "(1 0);v=chrome.1:1:2, ();p=P00000000000"},
+			wantTopic: []Topic{},
+			wantError: []error{
+				&errortypes.DebugWarning{
+					Message:     "Invalid field in Sec-Browsing-Topics header: (1 0);v=chrome.1:1:2",
+					WarningCode: errortypes.SecBrowsingTopicsWarningCode,
+				},
+			},
+		},
+		{
+			name:      "header with one valid field having a segId value more than MaxInt, drop field",
+			args:      args{secBrowsingTopics: "(1 9223372036854775808);v=chrome.1:1:2, ();p=P00000000000"},
+			wantTopic: []Topic{},
+			wantError: []error{
+				&errortypes.DebugWarning{
+					Message:     "Invalid field in Sec-Browsing-Topics header: (1 9223372036854775808);v=chrome.1:1:2",
+					WarningCode: errortypes.SecBrowsingTopicsWarningCode,
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotTopic, gotError := ParseTopicsFromHeader(tt.args.secBrowsingTopics)
+			assert.Equal(t, tt.wantTopic, gotTopic)
+			assert.Equal(t, tt.wantError, gotError)
+		})
+	}
+}
+
+func TestUpdateUserDataWithTopics(t *testing.T) {
+	type args struct {
+		userData     []openrtb2.Data
+		headerData   []Topic
+		topicsDomain string
+	}
+	tests := []struct {
+		name string
+		args args
+		want []openrtb2.Data
+	}{
+		{
+			name: "empty topics, empty user data, no change in user data",
+			args: args{
+				userData:   nil,
+				headerData: nil,
+			},
+			want: nil,
+		},
+		{
+			name: "empty topics, non-empty user data, no change in user data",
+			args: args{
+				userData: []openrtb2.Data{
+					{
+						ID:   "1",
+						Name: "data1",
+						Segment: []openrtb2.Segment{
+							{ID: "1"},
+							{ID: "2"},
+						},
+					},
+				},
+				headerData: nil,
+			},
+			want: []openrtb2.Data{
+				{
+					ID:   "1",
+					Name: "data1",
+					Segment: []openrtb2.Segment{
+						{ID: "1"},
+						{ID: "2"},
+					},
+				},
+			},
+		},
+		{
+			name: "topicsDomain empty, no change in user data",
+			args: args{
+				userData: []openrtb2.Data{
+					{
+						ID:   "1",
+						Name: "data1",
+						Segment: []openrtb2.Segment{
+							{ID: "1"},
+							{ID: "2"},
+						},
+					},
+				},
+				headerData: []Topic{
+					{
+						SegTax:   600,
+						SegClass: "2",
+						SegIDs:   []int{1, 2},
+					},
+				},
+				topicsDomain: "",
+			},
+			want: []openrtb2.Data{
+				{
+					ID:   "1",
+					Name: "data1",
+					Segment: []openrtb2.Segment{
+						{ID: "1"},
+						{ID: "2"},
+					},
+				},
+			},
+		},
+		{
+			name: "non-empty topics, empty user data, topics from header copied to user data",
+			args: args{
+				userData: nil,
+				headerData: []Topic{
+					{
+						SegTax:   600,
+						SegClass: "2",
+						SegIDs:   []int{1, 2},
+					},
+				},
+				topicsDomain: "ads.pubmatic.com",
+			},
+			want: []openrtb2.Data{
+				{
+					Name: "ads.pubmatic.com",
+					Segment: []openrtb2.Segment{
+						{ID: "1"},
+						{ID: "2"},
+					},
+					Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`),
+				},
+			},
+		},
+		{
+			name: "non-empty topics, non-empty user data, topics from header copied to user data",
+			args: args{
+				userData: []openrtb2.Data{
+					{
+						ID:   "1",
+						Name: "data1",
+						Segment: []openrtb2.Segment{
+							{ID: "1"},
+							{ID: "2"},
+						},
+					},
+				},
+				headerData: []Topic{
+					{
+						SegTax:   600,
+						SegClass: "2",
+						SegIDs:   []int{3, 4},
+					},
+				},
+				topicsDomain: "ads.pubmatic.com",
+			},
+			want: []openrtb2.Data{
+				{
+					ID:   "1",
+					Name: "data1",
+					Segment: []openrtb2.Segment{
+						{ID: "1"},
+						{ID: "2"},
+					},
+				},
+				{
+					Name: "ads.pubmatic.com",
+					Segment: []openrtb2.Segment{
+						{ID: "3"},
+						{ID: "4"},
+					},
+					Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`),
+				},
+			},
+		},
+		{
+			name: "non-empty topics, user data with invalid data.ext field, topics from header copied to user data",
+			args: args{
+				userData: []openrtb2.Data{
+					{
+						ID:   "1",
+						Name: "data1",
+						Segment: []openrtb2.Segment{
+							{ID: "1"},
+							{ID: "2"},
+						},
+						Ext: json.RawMessage(`{`),
+					},
+				},
+				headerData: []Topic{
+					{
+						SegTax:   600,
+						SegClass: "2",
+						SegIDs:   []int{3, 4},
+					},
+				},
+				topicsDomain: "ads.pubmatic.com",
+			},
+			want: []openrtb2.Data{
+				{
+					ID:   "1",
+					Name: "data1",
+					Segment: []openrtb2.Segment{
+						{ID: "1"},
+						{ID: "2"},
+					},
+					Ext: json.RawMessage(`{`),
+				},
+				{
+					Name: "ads.pubmatic.com",
+					Segment: []openrtb2.Segment{
+						{ID: "3"},
+						{ID: "4"},
+					},
+					Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`),
+				},
+			},
+		},
+		{
+			name: "non-empty topics, user data with invalid topic details (invalid segtax and segclass), topics from header copied to user data",
+			args: args{
+				userData: []openrtb2.Data{
+					{
+						ID:   "1",
+						Name: "chrome.com",
+						Segment: []openrtb2.Segment{
+							{ID: "1"},
+							{ID: "2"},
+						},
+						Ext: json.RawMessage(`{"segtax":0,"segclass":""}`),
+					},
+				},
+				headerData: []Topic{
+					{
+						SegTax:   600,
+						SegClass: "2",
+						SegIDs:   []int{3, 4},
+					},
+				},
+				topicsDomain: "ads.pubmatic.com",
+			},
+			want: []openrtb2.Data{
+				{
+					ID:   "1",
+					Name: "chrome.com",
+					Segment: []openrtb2.Segment{
+						{ID: "1"},
+						{ID: "2"},
+					},
+					Ext: json.RawMessage(`{"segtax":0,"segclass":""}`),
+				},
+				{
+					Name: "ads.pubmatic.com",
+					Segment: []openrtb2.Segment{
+						{ID: "3"},
+						{ID: "4"},
+					},
+					Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`),
+				},
+			},
+		},
+		{
+			name: "non-empty topics, user data with non matching topic details (different topicdomains, segtax and segclass), topics from header copied to user data",
+			args: args{
+				userData: []openrtb2.Data{
+					{
+						ID:   "1",
+						Name: "chrome.com",
+						Segment: []openrtb2.Segment{
+							{ID: "1"},
+							{ID: "2"},
+						},
+						Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`),
+					},
+					{
+						ID:   "2",
+						Name: "ads.pubmatic.com",
+						Segment: []openrtb2.Segment{
+							{ID: "5"},
+							{ID: "6"},
+						},
+						Ext: json.RawMessage(`{"segtax":601,"segclass":"3"}`),
+					},
+					{
+						ID:   "3",
+						Name: "ads.pubmatic.com",
+						Segment: []openrtb2.Segment{
+							{ID: "7"},
+							{ID: "8"},
+						},
+						Ext: json.RawMessage(`{"segtax":602,"segclass":"4"}`),
+					},
+				},
+				headerData: []Topic{
+					{
+						SegTax:   600,
+						SegClass: "2",
+						SegIDs:   []int{3, 4},
+					},
+					{
+						SegTax:   602,
+						SegClass: "2",
+						SegIDs:   []int{3, 4},
+					},
+				},
+				topicsDomain: "ads.pubmatic.com",
+			},
+			want: []openrtb2.Data{
+				{
+					ID:   "1",
+					Name: "chrome.com",
+					Segment: []openrtb2.Segment{
+						{ID: "1"},
+						{ID: "2"},
+					},
+					Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`),
+				},
+				{
+					ID:   "2",
+					Name: "ads.pubmatic.com",
+					Segment: []openrtb2.Segment{
+						{ID: "5"},
+						{ID: "6"},
+					},
+					Ext: json.RawMessage(`{"segtax":601,"segclass":"3"}`),
+				},
+				{
+					ID:   "3",
+					Name: "ads.pubmatic.com",
+					Segment: []openrtb2.Segment{
+						{ID: "7"},
+						{ID: "8"},
+					},
+					Ext: json.RawMessage(`{"segtax":602,"segclass":"4"}`),
+				},
+				{
+					Name: "ads.pubmatic.com",
+					Segment: []openrtb2.Segment{
+						{ID: "3"},
+						{ID: "4"},
+					},
+					Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`),
+				},
+				{
+					Name: "ads.pubmatic.com",
+					Segment: []openrtb2.Segment{
+						{ID: "3"},
+						{ID: "4"},
+					},
+					Ext: json.RawMessage(`{"segtax":602,"segclass":"2"}`),
+				},
+			},
+		},
+		{
+			name: "non-empty topics, user data with same topic details (matching segtax and segclass), topics from header merged with user data (filter unique segIDs)",
+			args: args{
+				userData: []openrtb2.Data{
+					{
+						ID:   "1",
+						Name: "ads.pubmatic.com",
+						Segment: []openrtb2.Segment{
+							{ID: "1"},
+							{ID: "2"},
+							{ID: "3"},
+						},
+						Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`),
+					},
+				},
+				headerData: []Topic{
+					{
+						SegTax:   600,
+						SegClass: "2",
+						SegIDs:   []int{2, 3, 4},
+					},
+				},
+				topicsDomain: "ads.pubmatic.com",
+			},
+			want: []openrtb2.Data{
+				{
+					ID:   "1",
+					Name: "ads.pubmatic.com",
+					Segment: []openrtb2.Segment{
+						{ID: "1"},
+						{ID: "2"},
+						{ID: "3"},
+						{ID: "4"},
+					},
+					Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`),
+				},
+			},
+		},
+		{
+			name: "non-empty topics, user data with duplicate topic details (matching segtax and segclass and segIDs), topics from header merged with user data (filter unique segIDs), user.data will not be deduped",
+			args: args{
+				userData: []openrtb2.Data{
+					{
+						ID:   "1",
+						Name: "ads.pubmatic.com",
+						Segment: []openrtb2.Segment{
+							{ID: "1"},
+							{ID: "2"},
+							{ID: "3"},
+						},
+						Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`),
+					},
+					{
+						ID:   "1",
+						Name: "ads.pubmatic.com",
+						Segment: []openrtb2.Segment{
+							{ID: "1"},
+							{ID: "2"},
+							{ID: "3"},
+						},
+						Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`),
+					},
+				},
+				headerData: []Topic{
+					{
+						SegTax:   600,
+						SegClass: "2",
+						SegIDs:   []int{2, 3, 4},
+					},
+				},
+				topicsDomain: "ads.pubmatic.com",
+			},
+			want: []openrtb2.Data{
+				{
+					ID:   "1",
+					Name: "ads.pubmatic.com",
+					Segment: []openrtb2.Segment{
+						{ID: "1"},
+						{ID: "2"},
+						{ID: "3"},
+						{ID: "4"},
+					},
+					Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`),
+				},
+				{
+					ID:   "1",
+					Name: "ads.pubmatic.com",
+					Segment: []openrtb2.Segment{
+						{ID: "1"},
+						{ID: "2"},
+						{ID: "3"},
+					},
+					Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`),
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := UpdateUserDataWithTopics(tt.args.userData, tt.args.headerData, tt.args.topicsDomain)
+			sort.Slice(got, func(i, j int) bool {
+				if got[i].Name == got[j].Name {
+					return string(got[i].Ext) < string(got[j].Ext)
+				}
+				return got[i].Name < got[j].Name
+			})
+			sort.Slice(tt.want, func(i, j int) bool {
+				if tt.want[i].Name == tt.want[j].Name {
+					return string(tt.want[i].Ext) < string(tt.want[j].Ext)
+				}
+				return tt.want[i].Name < tt.want[j].Name
+			})
+
+			for g := range got {
+				sort.Slice(got[g].Segment, func(i, j int) bool {
+					return got[g].Segment[i].ID < got[g].Segment[j].ID
+				})
+			}
+			assert.Equal(t, tt.want, got, tt.name)
+		})
+	}
+}