Repository: prebid/prebid-server
Pull request #3600 (2024-03-28 04:33:50)

Local stored response fetching
*Problem*
1. make the local stored response fetcher a separate and independent PR according to the feed back from https://github.com/prebid/prebid-server/pull/3326#issuecomment-1841905156
```
We also think fetcher change should be a separate PR with unit tests, not a part of this PR. I also understand stored responses set up will not work without File System fetcher changes. Maybe fetcher PR should go first.
```

this PR is to unblock the hello world example framework i try to build

2. add unit test coverage for the stored response




zhongshixi: cc @bretg for visibility 
cc @VeronikaSolovei9 @guscarreon for code review
zhongshixi: cc @SyntaxNode for visibility & code review ( sorry i forgot to mention you)

Patch:
diff --git a/README.md b/README.md
index 77443318..c5b0e003 100644
--- a/README.md
+++ b/README.md
@@ -98,7 +98,7 @@ Bid Adapters transform OpenRTB requests and responses for communicating with a b
 Analytics Modules enable business intelligence tools to collect data from Prebid Server to provide publishers and hosts with valuable insights into their header bidding traffic. We welcome you to contribute a module for your platform. Refer to our guide on [building an analytics module](https://docs.prebid.org/prebid-server/developers/pbs-build-an-analytics-adapter.html) for further information.
 
 ### Auction Module
-Auction Modules allow hosts to extend the behavior of Prebid Server at specfic spots in the auction pipeline using existing modules or by developing custom functionality. Auction Modules may provide creative validation, traffic optimization, and real time data services amoung many other potential uses. We welcome vendors and community members to contribute modules that publishers and hosts may find useful. Consult our guide on [building an auction module](https://docs.prebid.org/prebid-server/developers/add-a-module.html) for more information.
+Auction Modules allow hosts to extend the behavior of Prebid Server at specfic spots in the auction pipeline using existing modules or by developing custom functionality. Auction Modules may provide creative validation, traffic optimization, and real time data services among many other potential uses. We welcome vendors and community members to contribute modules that publishers and hosts may find useful. Consult our guide on [building an auction module](https://docs.prebid.org/prebid-server/developers/add-a-module.html) for more information.
 
 ### Feature
 We welcome everyone to contribute to this project by implementing a specification or by proposing a new feature. Please review the [prioritized project board](https://github.com/orgs/prebid/projects/4), where you can select an issue labeled "Ready For Dev". To avoid redundant effort, kindly leave a comment on the issue stating your intention to take it on. To propose a feature, [open a new issue](https://github.com/prebid/prebid-server/issues/new/choose) with as much detail as possible for consideration by the Prebid Server Committee.
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..4f014b96 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)
 	}
 
@@ -1817,6 +1833,7 @@ func (deps *endpointDeps) validateUser(req *openrtb_ext.RequestWrapper, aliases
 	if err != nil {
 		return append(errL, fmt.Errorf("request.user.ext object is not valid: %v", err))
 	}
+
 	// Check if the buyeruids are valid
 	prebid := userExt.GetPrebid()
 	if prebid != nil {
@@ -1833,19 +1850,15 @@ func (deps *endpointDeps) validateUser(req *openrtb_ext.RequestWrapper, aliases
 			}
 		}
 	}
+
 	// Check Universal User ID
 	eids := userExt.GetEid()
 	if eids != nil {
 		eidsValue := *eids
-		uniqueSources := make(map[string]struct{}, len(eidsValue))
 		for eidIndex, eid := range eidsValue {
 			if eid.Source == "" {
 				return append(errL, fmt.Errorf("request.user.ext.eids[%d] missing required field: \"source\"", eidIndex))
 			}
-			if _, ok := uniqueSources[eid.Source]; ok {
-				return append(errL, errors.New("request.user.ext.eids must contain unique sources"))
-			}
-			uniqueSources[eid.Source] = struct{}{}
 
 			if len(eid.UIDs) == 0 {
 				return append(errL, fmt.Errorf("request.user.ext.eids[%d].uids must contain at least one element or be undefined", eidIndex))
@@ -1919,7 +1932,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 +2042,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 +2054,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 +2064,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 +2074,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/invalid-whole/user-ext-eids-source-duplicate.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-source-duplicate.json
deleted file mode 100644
index 05cd7f34..00000000
--- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-source-duplicate.json
+++ /dev/null
@@ -1,49 +0,0 @@
-{
-  "description": "Bid request where more than one request.user.ext.eids array elements share the same source field value",
-  "mockBidRequest": {
-    "id": "anyRequestID",
-    "site": {
-      "page": "prebid.org",
-      "publisher": {
-        "id": "anyPublisher"
-      }
-    },
-    "imp": [{
-      "id": "anyImpID",
-      "ext": {
-        "appnexus": {
-          "placementId": 42
-        }
-      },
-      "banner": {
-        "format": [{
-            "w": 300,
-            "h": 250
-          },
-          {
-            "w": 300,
-            "h": 300
-          }
-        ]
-      }
-    }],
-    "tmax": 1000,
-    "user": {
-      "ext": {
-        "eids": [{
-          "source": "source1",
-          "uids": [{
-            "id": "A"
-          }]
-        }, {
-          "source": "source1",
-          "uids": [{
-            "id": "B"
-          }]
-        }]
-      }
-    }
-  },
-  "expectedReturnCode": 400,
-  "expectedErrorMessage": "Invalid request: request.user.ext.eids must contain unique sources\n"
-}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/user-ext-eids-empty.json b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/user-ext-eids-empty.json
index 9f047613..ab633724 100644
--- a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/user-ext-eids-empty.json
+++ b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/user-ext-eids-empty.json
@@ -1,56 +1,59 @@
 {
-    "description": "Bid request with empty eids array in user.ext",
-    "mockBidRequest": {
-        "id": "some-request-id",
-        "site": {
-            "page": "prebid.org"
+  "description": "Bid request with empty eids array in user.ext",
+  "mockBidRequest": {
+    "id": "anyRequestID",
+    "site": {
+      "page": "prebid.org",
+      "publisher": {
+        "id": "anyPublisher"
+      }
+    },
+    "imp": [
+      {
+        "id": "anyImpID",
+        "ext": {
+          "appnexus": {
+            "placementId": 42
+          }
         },
-        "imp": [
+        "banner": {
+          "format": [
             {
-                "id": "some-impression-id",
-                "ext": {
-                    "appnexus": {
-                        "placementId": 123456
-                    }
-                },
-                "banner": {
-                    "format": [
-                        {
-                            "w": 300,
-                            "h": 250
-                        },
-                        {
-                            "w": 300,
-                            "h": 600
-                        }
-                    ]
-                }
-            }
-        ],
-        "tmax": 1000,
-        "user": {
-            "ext": {
-                "eids": []
-            }
-        }
-    },
-    "expectedBidResponse": {
-        "id": "some-request-id",
-        "seatbid": [
+              "w": 300,
+              "h": 250
+            },
             {
-                "bid": [
-                    {
-                        "id": "appnexus-bid",
-                        "impid": "some-impression-id",
-                        "price": 0
-                    }
-                ],
-                "seat": "appnexus"
+              "w": 300,
+              "h": 600
             }
+          ]
+        }
+      }
+    ],
+    "tmax": 1000,
+    "user": {
+      "ext": {
+        "eids": []
+      }
+    }
+  },
+  "expectedBidResponse": {
+    "id": "anyRequestID",
+    "seatbid": [
+      {
+        "bid": [
+          {
+            "id": "appnexus-bid",
+            "impid": "anyImpID",
+            "price": 0
+          }
         ],
-        "bidid": "test bid id",
-        "cur": "USD",
-        "nbr": 0
-    },
-    "expectedReturnCode": 200
+        "seat": "appnexus"
+      }
+    ],
+    "bidid": "test bid id",
+    "cur": "USD",
+    "nbr": 0
+  },
+  "expectedReturnCode": 200
 }
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/user-ext-eids-source-duplicate.json b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/user-ext-eids-source-duplicate.json
new file mode 100644
index 00000000..e4ee6c72
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/user-ext-eids-source-duplicate.json
@@ -0,0 +1,76 @@
+{
+  "description": "Bid request where more than one request.user.ext.eids array elements share the same source field value",
+  "mockBidRequest": {
+    "id": "anyRequestID",
+    "site": {
+      "page": "prebid.org",
+      "publisher": {
+        "id": "anyPublisher"
+      }
+    },
+    "imp": [
+      {
+        "id": "anyImpID",
+        "ext": {
+          "appnexus": {
+            "placementId": 42
+          }
+        },
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        }
+      }
+    ],
+    "tmax": 1000,
+    "user": {
+      "ext": {
+        "eids": [
+          {
+            "source": "source1",
+            "uids": [
+              {
+                "id": "A"
+              }
+            ]
+          },
+          {
+            "source": "source1",
+            "uids": [
+              {
+                "id": "B"
+              }
+            ]
+          }
+        ]
+      }
+    }
+  },
+  "expectedBidResponse": {
+    "id": "anyRequestID",
+    "seatbid": [
+      {
+        "bid": [
+          {
+            "id": "appnexus-bid",
+            "impid": "anyImpID",
+            "price": 0
+          }
+        ],
+        "seat": "appnexus"
+      }
+    ],
+    "bidid": "test bid id",
+    "cur": "USD",
+    "nbr": 0
+  },
+  "expectedReturnCode": 200
+}
\ No newline at end of file
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"]
+}
diff --git a/stored_requests/backends/file_fetcher/fetcher.go b/stored_requests/backends/file_fetcher/fetcher.go
index 21a54039..523e9bb5 100644
--- a/stored_requests/backends/file_fetcher/fetcher.go
+++ b/stored_requests/backends/file_fetcher/fetcher.go
@@ -3,6 +3,7 @@ package file_fetcher
 import (
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"os"
 	"strings"
@@ -35,8 +36,15 @@ func (fetcher *eagerFetcher) FetchRequests(ctx context.Context, requestIDs []str
 	return storedRequests, storedImpressions, errs
 }
 
+// Fetch Responses - Implements the interface to read the stored response information from the fetcher's FileSystem, the directory name is "stored_responses"
 func (fetcher *eagerFetcher) FetchResponses(ctx context.Context, ids []string) (data map[string]json.RawMessage, errs []error) {
-	return nil, nil
+	storedRespFS, found := fetcher.FileSystem.Directories["stored_responses"]
+	if !found {
+		return nil, append(errs, errors.New(`no "stored_responses" directory found`))
+	}
+
+	data = storedRespFS.Files
+	return data, appendErrors("Response", ids, data, nil)
 }
 
 // FetchAccount fetches the host account configuration for a publisher


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 7d87b653..6e6e1ac8 100644
--- a/endpoints/openrtb2/amp_auction_test.go
+++ b/endpoints/openrtb2/amp_auction_test.go
@@ -10,6 +10,7 @@ import (
 	"net/url"
 	"os"
 	"strconv"
+	"strings"
 	"testing"
 	"time"
 
@@ -1028,20 +1029,39 @@ func TestAMPSiteExt(t *testing.T) {
 
 // TestBadRequests makes sure we return 400's on bad requests.
 func TestAmpBadRequests(t *testing.T) {
-	dir := "sample-requests/invalid-whole"
+	dir := "sample-requests/invalid-whole/"
 	files, err := os.ReadDir(dir)
 	assert.NoError(t, err, "Failed to read folder: %s", dir)
 
-	badRequests := make(map[string]json.RawMessage, len(files))
+	mockAmpStoredReq := make(map[string]json.RawMessage, len(files))
+	badRequests := make(map[string]testCase, len(files))
 	for index, file := range files {
-		badRequests[strconv.Itoa(100+index)] = readFile(t, "sample-requests/invalid-whole/"+file.Name())
+		filename := file.Name()
+		fileData := readFile(t, dir+filename)
+
+		test, err := parseTestData(fileData, filename)
+		if !assert.NoError(t, err) {
+			return
+		}
+
+		if skipAmpTest(test) {
+			continue
+		}
+
+		requestID := strconv.Itoa(100 + index)
+		test.Query = fmt.Sprintf("account=test_pub&tag_id=%s", requestID)
+
+		badRequests[requestID] = test
+		mockAmpStoredReq[requestID] = test.BidRequest
 	}
 
+	addAmpBadRequests(badRequests, mockAmpStoredReq)
+
 	endpoint, _ := NewAmpEndpoint(
 		fakeUUIDGenerator{},
 		&mockAmpExchange{},
 		newParamsValidator(t),
-		&mockAmpStoredReqFetcher{badRequests},
+		&mockAmpStoredReqFetcher{data: mockAmpStoredReq},
 		empty_fetcher.EmptyFetcher{},
 		&config.Configuration{MaxRequestSize: maxSize},
 		&metricsConfig.NilMetricsEngine{},
@@ -1053,16 +1073,71 @@ func TestAmpBadRequests(t *testing.T) {
 		hooks.EmptyPlanBuilder{},
 		nil,
 	)
-	for requestID := range badRequests {
-		request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=%s", requestID), nil)
+
+	for _, test := range badRequests {
+		request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?%s", test.Query), nil)
 		recorder := httptest.NewRecorder()
 
 		endpoint(recorder, request, nil)
 
-		if recorder.Code != http.StatusBadRequest {
-			t.Errorf("Expected status %d. Got %d. Input was: %s", http.StatusBadRequest, recorder.Code, fmt.Sprintf("/openrtb2/auction/amp?config=%s", requestID))
+		response := recorder.Body.String()
+		assert.Equal(t, test.ExpectedReturnCode, recorder.Code, test.Description)
+		assert.Contains(t, response, test.ExpectedErrorMessage, "Actual: %s \nExpected: %s. Description: %s \n", response, test.ExpectedErrorMessage, test.Description)
+	}
+}
+
+func skipAmpTest(test testCase) bool {
+	bidRequest := openrtb2.BidRequest{}
+	if err := json.Unmarshal(test.BidRequest, &bidRequest); err == nil {
+		// request.app must not exist in AMP
+		if bidRequest.App != nil {
+			return true
 		}
+
+		// data for tag_id='%s' does not define the required imp array
+		// Invalid request: data for tag_id '%s' includes %d imp elements. Only one is allowed
+		if len(bidRequest.Imp) == 0 || len(bidRequest.Imp) > 1 {
+			return true
+		}
+
+		if bidRequest.Device != nil && strings.Contains(string(bidRequest.Device.Ext), "interstitial") {
+			return true
+		}
+	}
+
+	// request.ext.prebid.cache is initialised in AMP if it is not present in request
+	if strings.Contains(test.ExpectedErrorMessage, `Invalid request: request.ext is invalid: request.ext.prebid.cache requires one of the "bids" or "vastxml" properties`) ||
+		strings.Contains(test.ExpectedErrorMessage, `Invalid request: ext.prebid.storedrequest.id must be a string`) {
+		return true
+	}
+
+	return false
+}
+
+func addAmpBadRequests(mapBadRequests map[string]testCase, mockAmpStoredReq map[string]json.RawMessage) {
+	mapBadRequests["201"] = testCase{
+		Description:          "missing-tag-id",
+		Query:                "account=test_pub",
+		ExpectedReturnCode:   http.StatusBadRequest,
+		ExpectedErrorMessage: "Invalid request: AMP requests require an AMP tag_id\n",
+	}
+	mockAmpStoredReq["201"] = json.RawMessage(`{}`)
+
+	mapBadRequests["202"] = testCase{
+		Description:          "request.app-present",
+		Query:                "account=test_pub&tag_id=202",
+		ExpectedReturnCode:   http.StatusBadRequest,
+		ExpectedErrorMessage: "Invalid request: request.app must not exist in AMP stored requests.\n",
+	}
+	mockAmpStoredReq["202"] = json.RawMessage(`{"imp":[{}],"app":{}}`)
+
+	mapBadRequests["203"] = testCase{
+		Description:          "request-with-2-imps",
+		Query:                "account=test_pub&tag_id=203",
+		ExpectedReturnCode:   http.StatusBadRequest,
+		ExpectedErrorMessage: "Invalid request: data for tag_id '203' includes 2 imp elements. Only one is allowed",
 	}
+	mockAmpStoredReq["203"] = json.RawMessage(`{"imp":[{},{}]}`)
 }
 
 // TestAmpDebug makes sure we get debug information back when requested
@@ -1977,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)
@@ -2404,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..ee82ffdf 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"
@@ -3768,51 +3769,51 @@ func TestValidateEidPermissions(t *testing.T) {
 	knownAliases := map[string]string{"b": "b"}
 
 	testCases := []struct {
-		description   string
+		name          string
 		request       *openrtb_ext.ExtRequest
 		expectedError error
 	}{
 		{
-			description:   "Valid - Empty ext",
+			name:          "valid-empty-ext",
 			request:       &openrtb_ext.ExtRequest{},
 			expectedError: nil,
 		},
 		{
-			description:   "Valid - Nil ext.prebid.data",
+			name:          "valid-nil-ext.prebid.data",
 			request:       &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{}},
 			expectedError: nil,
 		},
 		{
-			description:   "Valid - Empty ext.prebid.data",
+			name:          "valid-empty-ext.prebid.data",
 			request:       &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Data: &openrtb_ext.ExtRequestPrebidData{}}},
 			expectedError: nil,
 		},
 		{
-			description:   "Valid - Nil ext.prebid.data.eidpermissions",
+			name:          "valid-nil-ext.prebid.data.eidpermissions",
 			request:       &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Data: &openrtb_ext.ExtRequestPrebidData{EidPermissions: nil}}},
 			expectedError: nil,
 		},
 		{
-			description:   "Valid - None",
+			name:          "valid-none",
 			request:       &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Data: &openrtb_ext.ExtRequestPrebidData{EidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{}}}},
 			expectedError: nil,
 		},
 		{
-			description: "Valid - One",
+			name: "valid-one",
 			request: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Data: &openrtb_ext.ExtRequestPrebidData{EidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{
 				{Source: "sourceA", Bidders: []string{"a"}},
 			}}}},
 			expectedError: nil,
 		},
 		{
-			description: "Valid - One - Case Insensitive",
+			name: "valid-one-case-insensitive",
 			request: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Data: &openrtb_ext.ExtRequestPrebidData{EidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{
 				{Source: "sourceA", Bidders: []string{"A"}},
 			}}}},
 			expectedError: nil,
 		},
 		{
-			description: "Valid - Many",
+			name: "valid-many",
 			request: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Data: &openrtb_ext.ExtRequestPrebidData{EidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{
 				{Source: "sourceA", Bidders: []string{"a"}},
 				{Source: "sourceB", Bidders: []string{"a"}},
@@ -3820,7 +3821,7 @@ func TestValidateEidPermissions(t *testing.T) {
 			expectedError: nil,
 		},
 		{
-			description: "Invalid - Missing Source",
+			name: "invalid-missing-source",
 			request: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Data: &openrtb_ext.ExtRequestPrebidData{EidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{
 				{Source: "sourceA", Bidders: []string{"a"}},
 				{Bidders: []string{"a"}},
@@ -3828,7 +3829,7 @@ func TestValidateEidPermissions(t *testing.T) {
 			expectedError: errors.New(`request.ext.prebid.data.eidpermissions[1] missing required field: "source"`),
 		},
 		{
-			description: "Invalid - Duplicate Source",
+			name: "invalid-duplicate-source",
 			request: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Data: &openrtb_ext.ExtRequestPrebidData{EidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{
 				{Source: "sourceA", Bidders: []string{"a"}},
 				{Source: "sourceA", Bidders: []string{"a"}},
@@ -3836,7 +3837,7 @@ func TestValidateEidPermissions(t *testing.T) {
 			expectedError: errors.New(`request.ext.prebid.data.eidpermissions[1] duplicate entry with field: "source"`),
 		},
 		{
-			description: "Invalid - Missing Bidders - Nil",
+			name: "invalid-missing-bidders-nil",
 			request: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Data: &openrtb_ext.ExtRequestPrebidData{EidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{
 				{Source: "sourceA", Bidders: []string{"a"}},
 				{Source: "sourceB"},
@@ -3844,7 +3845,7 @@ func TestValidateEidPermissions(t *testing.T) {
 			expectedError: errors.New(`request.ext.prebid.data.eidpermissions[1] missing or empty required field: "bidders"`),
 		},
 		{
-			description: "Invalid - Missing Bidders - Empty",
+			name: "invalid-missing-bidders-empty",
 			request: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Data: &openrtb_ext.ExtRequestPrebidData{EidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{
 				{Source: "sourceA", Bidders: []string{"a"}},
 				{Source: "sourceB", Bidders: []string{}},
@@ -3852,7 +3853,7 @@ func TestValidateEidPermissions(t *testing.T) {
 			expectedError: errors.New(`request.ext.prebid.data.eidpermissions[1] missing or empty required field: "bidders"`),
 		},
 		{
-			description: "Invalid - Invalid Bidders",
+			name: "invalid-invalid-bidders",
 			request: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Data: &openrtb_ext.ExtRequestPrebidData{EidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{
 				{Source: "sourceA", Bidders: []string{"a"}},
 				{Source: "sourceB", Bidders: []string{"z"}},
@@ -3860,7 +3861,7 @@ func TestValidateEidPermissions(t *testing.T) {
 			expectedError: errors.New(`request.ext.prebid.data.eidpermissions[1] contains unrecognized bidder "z"`),
 		},
 		{
-			description: "Valid - Alias Case Sensitive",
+			name: "invalid-alias-case-sensitive",
 			request: &openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{Data: &openrtb_ext.ExtRequestPrebidData{EidPermissions: []openrtb_ext.ExtRequestPrebidDataEidPermission{
 				{Source: "sourceA", Bidders: []string{"B"}},
 			}}}},
@@ -3870,8 +3871,10 @@ func TestValidateEidPermissions(t *testing.T) {
 
 	endpoint := &endpointDeps{bidderMap: knownBidders, normalizeBidderName: fakeNormalizeBidderName}
 	for _, test := range testCases {
-		result := endpoint.validateEidPermissions(test.request.Prebid.Data, knownAliases)
-		assert.Equal(t, test.expectedError, result, test.description)
+		t.Run(test.name, func(t *testing.T) {
+			result := endpoint.validateEidPermissions(test.request.Prebid.Data, knownAliases)
+			assert.Equal(t, test.expectedError, result)
+		})
 	}
 }
 
@@ -4660,13 +4663,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 +4678,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 +4732,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 +6077,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 +6309,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 +6465,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)
+		})
+	}
+}
diff --git a/stored_requests/backends/file_fetcher/fetcher_test.go b/stored_requests/backends/file_fetcher/fetcher_test.go
index b4a73870..cc1c0f50 100644
--- a/stored_requests/backends/file_fetcher/fetcher_test.go
+++ b/stored_requests/backends/file_fetcher/fetcher_test.go
@@ -12,11 +12,13 @@ import (
 )
 
 func TestFileFetcher(t *testing.T) {
+	// Load the test input files for testing
 	fetcher, err := NewFileFetcher("./test")
 	if err != nil {
 		t.Errorf("Failed to create a Fetcher: %v", err)
 	}
 
+	// Test stored request and stored imps
 	storedReqs, storedImps, errs := fetcher.FetchRequests(context.Background(), []string{"1", "2"}, []string{"some-imp"})
 	assertErrorCount(t, 0, errs)
 
@@ -25,6 +27,54 @@ func TestFileFetcher(t *testing.T) {
 	validateImp(t, storedImps)
 }
 
+func TestStoredResponseFileFetcher(t *testing.T) {
+	// grab the fetcher that do not have /test/stored_responses/stored_responses FS directory
+	directoryNotExistfetcher, err := NewFileFetcher("./test/stored_responses")
+	if err != nil {
+		t.Errorf("Failed to create a Fetcher: %v", err)
+	}
+
+	// we should receive 1 error since we do not have "stored_responses" directory in ./test/stored_responses
+	_, errs := directoryNotExistfetcher.FetchResponses(context.Background(), []string{})
+	assertErrorCount(t, 1, errs)
+
+	// grab the fetcher that has /test/stored_responses FS directory
+	fetcher, err := NewFileFetcher("./test")
+	if err != nil {
+		t.Errorf("Failed to create a Fetcher: %v", err)
+	}
+
+	// Test stored responses, we have 3 stored responses in ./test/stored_responses
+	storedResps, errs := fetcher.FetchResponses(context.Background(), []string{"bar", "escaped", "does_not_exist"})
+	// expect 1 error since we do not have "does_not_exist" stored response file from ./test
+	assertErrorCount(t, 1, errs)
+
+	validateStoredResponse[map[string]string](t, storedResps, "bar", func(val map[string]string) error {
+		if len(val) != 1 {
+			return fmt.Errorf("Unexpected value length. Expected %d, Got %d", 1, len(val))
+		}
+
+		data, hadKey := val["test"]
+		if !hadKey {
+			return fmt.Errorf(`missing key "test" in the value`)
+		}
+
+		expectedVal := "bar"
+		if data != expectedVal {
+			return fmt.Errorf(`Bad value for key "test". Expected "%s", Got "%s"`, expectedVal, data)
+		}
+		return nil
+	})
+
+	validateStoredResponse[string](t, storedResps, "escaped", func(val string) error {
+		expectedVal := `esca"ped`
+		if val != expectedVal {
+			return fmt.Errorf(`Bad data. Expected "%v", Got "%s"`, expectedVal, val)
+		}
+		return nil
+	})
+}
+
 func TestAccountFetcher(t *testing.T) {
 	fetcher, err := NewFileFetcher("./test")
 	assert.NoError(t, err, "Failed to create test fetcher")
@@ -179,3 +229,20 @@ func TestCategoriesFetcherNoCategoriesFile(t *testing.T) {
 	assert.Equal(t, fmt.Errorf("Unable to find mapping file for adserver: 'test', publisherId: 'not_exists'"),
 		fetchingErr, "Categories were loaded incorrectly")
 }
+
+// validateStoredResponse - reusable function in the stored response test to verify the actual data read from the fetcher
+func validateStoredResponse[T any](t *testing.T, storedInfo map[string]json.RawMessage, id string, verifyFunc func(outputVal T) error) {
+	storedValue, hasID := storedInfo[id]
+	if !hasID {
+		t.Fatalf(`Expected stored response data to have id: "%s"`, id)
+	}
+
+	var unmarshalledValue T
+	if err := jsonutil.UnmarshalValid(storedValue, &unmarshalledValue); err != nil {
+		t.Errorf(`Failed to unmarshal stored response data of id "%s": %v`, id, err)
+	}
+
+	if err := verifyFunc(unmarshalledValue); err != nil {
+		t.Errorf(`Bad data in stored response of id: "%s": %v`, id, err)
+	}
+}
diff --git a/stored_requests/backends/file_fetcher/test/stored_responses/bar.json b/stored_requests/backends/file_fetcher/test/stored_responses/bar.json
new file mode 100644
index 00000000..e1f4c568
--- /dev/null
+++ b/stored_requests/backends/file_fetcher/test/stored_responses/bar.json
@@ -0,0 +1,4 @@
+{
+    "test": "bar"
+}
+  
\ No newline at end of file
diff --git a/stored_requests/backends/file_fetcher/test/stored_responses/escaped.json b/stored_requests/backends/file_fetcher/test/stored_responses/escaped.json
new file mode 100644
index 00000000..d302ea50
--- /dev/null
+++ b/stored_requests/backends/file_fetcher/test/stored_responses/escaped.json
@@ -0,0 +1,1 @@
+"esca\"ped"