Repository: prebid/prebid-server
Pull request #3515 (2024-02-20 07:16:29)

Taboola: Fix gpp query param 


Patch:
diff --git a/Makefile b/Makefile
index 2d8aae6c..cf4ac52b 100644
--- a/Makefile
+++ b/Makefile
@@ -33,3 +33,7 @@ image:
 # format runs format
 format:
 	./scripts/format.sh -f true
+
+# formatcheck runs format for diagnostics, without modifying the code
+formatcheck:
+	./scripts/format.sh -f false
diff --git a/adapters/adoppler/adoppler.go b/adapters/adoppler/adoppler.go
index 07cfce53..1af0b720 100644
--- a/adapters/adoppler/adoppler.go
+++ b/adapters/adoppler/adoppler.go
@@ -65,7 +65,7 @@ func (ads *AdopplerAdapter) MakeRequests(
 	for _, imp := range req.Imp {
 		ext, err := unmarshalExt(imp.Ext)
 		if err != nil {
-			errs = append(errs, &errortypes.BadInput{err.Error()})
+			errs = append(errs, &errortypes.BadInput{Message: err.Error()})
 			continue
 		}
 
@@ -83,7 +83,7 @@ func (ads *AdopplerAdapter) MakeRequests(
 		if err != nil {
 			e := fmt.Sprintf("Unable to build bid URI: %s",
 				err.Error())
-			errs = append(errs, &errortypes.BadInput{e})
+			errs = append(errs, &errortypes.BadInput{Message: e})
 			continue
 		}
 		data := &adapters.RequestData{
@@ -110,11 +110,11 @@ func (ads *AdopplerAdapter) MakeBids(
 		return nil, nil
 	}
 	if resp.StatusCode == http.StatusBadRequest {
-		return nil, []error{&errortypes.BadInput{"bad request"}}
+		return nil, []error{&errortypes.BadInput{Message: "bad request"}}
 	}
 	if resp.StatusCode != http.StatusOK {
 		err := &errortypes.BadServerResponse{
-			fmt.Sprintf("unexpected status: %d", resp.StatusCode),
+			Message: fmt.Sprintf("unexpected status: %d", resp.StatusCode),
 		}
 		return nil, []error{err}
 	}
@@ -123,7 +123,7 @@ func (ads *AdopplerAdapter) MakeBids(
 	err := json.Unmarshal(resp.Body, &bidResp)
 	if err != nil {
 		err := &errortypes.BadServerResponse{
-			fmt.Sprintf("invalid body: %s", err.Error()),
+			Message: fmt.Sprintf("invalid body: %s", err.Error()),
 		}
 		return nil, []error{err}
 	}
@@ -132,7 +132,7 @@ func (ads *AdopplerAdapter) MakeBids(
 	for _, imp := range intReq.Imp {
 		if _, ok := impTypes[imp.ID]; ok {
 			return nil, []error{&errortypes.BadInput{
-				fmt.Sprintf("duplicate $.imp.id %s", imp.ID),
+				Message: fmt.Sprintf("duplicate $.imp.id %s", imp.ID),
 			}}
 		}
 		if imp.Banner != nil {
@@ -145,7 +145,7 @@ func (ads *AdopplerAdapter) MakeBids(
 			impTypes[imp.ID] = openrtb_ext.BidTypeNative
 		} else {
 			return nil, []error{&errortypes.BadInput{
-				"one of $.imp.banner, $.imp.video, $.imp.audio and $.imp.native field required",
+				Message: "one of $.imp.banner, $.imp.video, $.imp.audio and $.imp.native field required",
 			}}
 		}
 	}
@@ -156,7 +156,7 @@ func (ads *AdopplerAdapter) MakeBids(
 			tp, ok := impTypes[bid.ImpID]
 			if !ok {
 				err := &errortypes.BadServerResponse{
-					fmt.Sprintf("unknown impid: %s", bid.ImpID),
+					Message: fmt.Sprintf("unknown impid: %s", bid.ImpID),
 				}
 				return nil, []error{err}
 			}
@@ -165,11 +165,11 @@ func (ads *AdopplerAdapter) MakeBids(
 			if tp == openrtb_ext.BidTypeVideo {
 				adsExt, err := unmarshalAdsExt(bid.Ext)
 				if err != nil {
-					return nil, []error{&errortypes.BadServerResponse{err.Error()}}
+					return nil, []error{&errortypes.BadServerResponse{Message: err.Error()}}
 				}
 				if adsExt == nil || adsExt.Video == nil {
 					return nil, []error{&errortypes.BadServerResponse{
-						"$.seatbid.bid.ext.ads.video required",
+						Message: "$.seatbid.bid.ext.ads.video required",
 					}}
 				}
 				bidVideo = &openrtb_ext.ExtBidPrebidVideo{
diff --git a/adapters/adquery/adquery.go b/adapters/adquery/adquery.go
index 4da3f7a4..e14c52f1 100644
--- a/adapters/adquery/adquery.go
+++ b/adapters/adquery/adquery.go
@@ -41,7 +41,7 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, _ *adapters.ExtraRe
 	for _, imp := range request.Imp {
 		ext, err := parseExt(imp.Ext)
 		if err != nil {
-			errs = append(errs, &errortypes.BadInput{err.Error()})
+			errs = append(errs, &errortypes.BadInput{Message: err.Error()})
 			continue
 		}
 
diff --git a/adapters/seedingAlliance/seedingAlliance.go b/adapters/seedingAlliance/seedingAlliance.go
index 03b6853b..af48133d 100644
--- a/adapters/seedingAlliance/seedingAlliance.go
+++ b/adapters/seedingAlliance/seedingAlliance.go
@@ -33,11 +33,11 @@ func Builder(_ openrtb_ext.BidderName, config config.Adapter, server config.Serv
 }
 
 func (a *adapter) MakeRequests(request *openrtb2.BidRequest, extraRequestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
-	var seatId string
+	var accountId string
 	var err error
 
 	for i := range request.Imp {
-		if seatId, err = getExtInfo(&request.Imp[i]); err != nil {
+		if accountId, err = getExtInfo(&request.Imp[i]); err != nil {
 			return nil, []error{err}
 		}
 	}
@@ -51,7 +51,7 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, extraRequestInfo *a
 		return nil, []error{err}
 	}
 
-	url, err := macros.ResolveMacros(a.endpoint, macros.EndpointTemplateParams{AccountID: seatId})
+	url, err := macros.ResolveMacros(a.endpoint, macros.EndpointTemplateParams{AccountID: accountId})
 	if err != nil {
 		return nil, []error{err}
 	}
@@ -146,7 +146,7 @@ func getExtInfo(imp *openrtb2.Imp) (string, error) {
 	var ext adapters.ExtImpBidder
 	var extSA openrtb_ext.ImpExtSeedingAlliance
 
-	seatID := "pbs"
+	accountId := "pbs"
 
 	if err := json.Unmarshal(imp.Ext, &ext); err != nil {
 		return "", fmt.Errorf("could not unmarshal adapters.ExtImpBidder: %w", err)
@@ -159,8 +159,12 @@ func getExtInfo(imp *openrtb2.Imp) (string, error) {
 	imp.TagID = extSA.AdUnitID
 
 	if extSA.SeatID != "" {
-		seatID = extSA.SeatID
+		accountId = extSA.SeatID
 	}
 
-	return seatID, nil
+	if extSA.AccountID != "" {
+		accountId = extSA.AccountID
+	}
+
+	return accountId, nil
 }
diff --git a/adapters/stroeerCore/stroeercore.go b/adapters/stroeerCore/stroeercore.go
index 733d549e..f2ef4923 100644
--- a/adapters/stroeerCore/stroeercore.go
+++ b/adapters/stroeerCore/stroeercore.go
@@ -23,17 +23,22 @@ type response struct {
 }
 
 type bidResponse struct {
-	ID     string  `json:"id"`
-	BidID  string  `json:"bidId"`
-	CPM    float64 `json:"cpm"`
-	Width  int64   `json:"width"`
-	Height int64   `json:"height"`
-	Ad     string  `json:"ad"`
-	CrID   string  `json:"crid"`
-	Mtype  string  `json:"mtype"`
+	ID     string          `json:"id"`
+	BidID  string          `json:"bidId"`
+	CPM    float64         `json:"cpm"`
+	Width  int64           `json:"width"`
+	Height int64           `json:"height"`
+	Ad     string          `json:"ad"`
+	CrID   string          `json:"crid"`
+	Mtype  string          `json:"mtype"`
+	DSA    json.RawMessage `json:"dsa"`
 }
 
-func (b bidResponse) resolveMediaType() (mt openrtb2.MarkupType, bt openrtb_ext.BidType, err error) {
+type bidExt struct {
+	DSA json.RawMessage `json:"dsa,omitempty"`
+}
+
+func (b *bidResponse) resolveMediaType() (mt openrtb2.MarkupType, bt openrtb_ext.BidType, err error) {
 	switch b.Mtype {
 	case "banner":
 		return openrtb2.MarkupBanner, openrtb_ext.BidTypeBanner, nil
@@ -82,6 +87,15 @@ func (a *adapter) MakeBids(bidRequest *openrtb2.BidRequest, requestData *adapter
 			MType: markupType,
 		}
 
+		if bid.DSA != nil {
+			dsaJson, err := json.Marshal(bidExt{bid.DSA})
+			if err != nil {
+				errors = append(errors, err)
+			} else {
+				openRtbBid.Ext = dsaJson
+			}
+		}
+
 		bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{
 			Bid:     &openRtbBid,
 			BidType: bidType,
diff --git a/amp/parse.go b/amp/parse.go
index 54dcc8e0..12663ee9 100644
--- a/amp/parse.go
+++ b/amp/parse.go
@@ -73,7 +73,7 @@ func ReadPolicy(ampParams Params, pbsConfigGDPREnabled bool) (privacy.PolicyWrit
 			warningMsg = validateTCf2ConsentString(ampParams.Consent)
 		}
 	case ConsentUSPrivacy:
-		rv = ccpa.ConsentWriter{ampParams.Consent}
+		rv = ccpa.ConsentWriter{Consent: ampParams.Consent}
 		if ccpa.ValidateConsent(ampParams.Consent) {
 			if parseGdprApplies(ampParams.GdprApplies) == 1 {
 				// Log warning because AMP request comes with both a valid CCPA string and gdpr_applies set to true
@@ -85,7 +85,7 @@ func ReadPolicy(ampParams Params, pbsConfigGDPREnabled bool) (privacy.PolicyWrit
 		}
 	default:
 		if ccpa.ValidateConsent(ampParams.Consent) {
-			rv = ccpa.ConsentWriter{ampParams.Consent}
+			rv = ccpa.ConsentWriter{Consent: ampParams.Consent}
 			if parseGdprApplies(ampParams.GdprApplies) == 1 {
 				warningMsg = "AMP request gdpr_applies value was ignored because provided consent string is a CCPA consent string"
 			}
diff --git a/dsa/validate.go b/dsa/validate.go
new file mode 100644
index 00000000..f6ece66e
--- /dev/null
+++ b/dsa/validate.go
@@ -0,0 +1,51 @@
+package dsa
+
+import (
+	"github.com/prebid/prebid-server/v2/exchange/entities"
+	"github.com/prebid/prebid-server/v2/openrtb_ext"
+
+	"github.com/buger/jsonparser"
+)
+
+const (
+	// Required - bid responses without DSA object will not be accepted
+	Required = 2
+	// RequiredOnlinePlatform - bid responses without DSA object will not be accepted, Publisher is an Online Platform
+	RequiredOnlinePlatform = 3
+)
+
+// Validate determines whether a given bid is valid from a DSA perspective.
+// A bid is considered valid unless the bid request indicates that a DSA object is required
+// in bid responses and the object happens to be missing from the specified bid.
+func Validate(req *openrtb_ext.RequestWrapper, bid *entities.PbsOrtbBid) (valid bool) {
+	if !dsaRequired(req) {
+		return true
+	}
+	if bid == nil || bid.Bid == nil {
+		return false
+	}
+	_, dataType, _, err := jsonparser.Get(bid.Bid.Ext, "dsa")
+	if dataType == jsonparser.Object && err == nil {
+		return true
+	} else if err != nil && err != jsonparser.KeyPathNotFoundError {
+		return true
+	}
+	return false
+}
+
+// dsaRequired examines the bid request to determine if the dsarequired field indicates
+// that bid responses include a dsa object
+func dsaRequired(req *openrtb_ext.RequestWrapper) bool {
+	regExt, err := req.GetRegExt()
+	if regExt == nil || err != nil {
+		return false
+	}
+	regsDSA := regExt.GetDSA()
+	if regsDSA == nil {
+		return false
+	}
+	if regsDSA.Required == Required || regsDSA.Required == RequiredOnlinePlatform {
+		return true
+	}
+	return false
+}
diff --git a/errortypes/code.go b/errortypes/code.go
index 1de5e648..399dd663 100644
--- a/errortypes/code.go
+++ b/errortypes/code.go
@@ -31,6 +31,7 @@ const (
 	AdServerTargetingWarningCode
 	BidAdjustmentWarningCode
 	FloorBidRejectionWarningCode
+	InvalidBidResponseDSAWarningCode
 )
 
 // Coder provides an error or warning code with severity.
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 003ce559..b9dae672 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -21,6 +21,7 @@ import (
 	"github.com/prebid/prebid-server/v2/bidadjustment"
 	"github.com/prebid/prebid-server/v2/config"
 	"github.com/prebid/prebid-server/v2/currency"
+	"github.com/prebid/prebid-server/v2/dsa"
 	"github.com/prebid/prebid-server/v2/errortypes"
 	"github.com/prebid/prebid-server/v2/exchange/entities"
 	"github.com/prebid/prebid-server/v2/experiment/adscert"
@@ -494,7 +495,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog
 	e.bidValidationEnforcement.SetBannerCreativeMaxSize(r.Account.Validations)
 
 	// Build the response
-	bidResponse := e.buildBidResponse(ctx, liveAdapters, adapterBids, r.BidRequestWrapper.BidRequest, adapterExtra, auc, bidResponseExt, cacheInstructions.returnCreative, r.ImpExtInfoMap, r.PubID, errs, &seatNonBids)
+	bidResponse := e.buildBidResponse(ctx, liveAdapters, adapterBids, r.BidRequestWrapper, adapterExtra, auc, bidResponseExt, cacheInstructions.returnCreative, r.ImpExtInfoMap, r.PubID, errs, &seatNonBids)
 	bidResponse = adservertargeting.Apply(r.BidRequestWrapper, r.ResolvedBidRequest, bidResponse, r.QueryParams, bidResponseExt, r.Account.TruncateTargetAttribute)
 
 	bidResponse.Ext, err = encodeBidResponseExt(bidResponseExt)
@@ -890,7 +891,7 @@ func errsToBidderWarnings(errs []error) []openrtb_ext.ExtBidderMessage {
 }
 
 // This piece takes all the bids supplied by the adapters and crafts an openRTB response to send back to the requester
-func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ext.BidderName, adapterSeatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, bidRequest *openrtb2.BidRequest, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, bidResponseExt *openrtb_ext.ExtBidResponse, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, pubID string, errList []error, seatNonBids *nonBids) *openrtb2.BidResponse {
+func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ext.BidderName, adapterSeatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, bidRequest *openrtb_ext.RequestWrapper, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, bidResponseExt *openrtb_ext.ExtBidResponse, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, pubID string, errList []error, seatNonBids *nonBids) *openrtb2.BidResponse {
 	bidResponse := new(openrtb2.BidResponse)
 
 	bidResponse.ID = bidRequest.ID
@@ -905,7 +906,7 @@ func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_
 	for a, adapterSeatBids := range adapterSeatBids {
 		//while processing every single bib, do we need to handle categories here?
 		if adapterSeatBids != nil && len(adapterSeatBids.Bids) > 0 {
-			sb := e.makeSeatBid(adapterSeatBids, a, adapterExtra, auc, returnCreative, impExtInfoMap, bidResponseExt, pubID, seatNonBids)
+			sb := e.makeSeatBid(adapterSeatBids, a, adapterExtra, auc, returnCreative, impExtInfoMap, bidRequest, bidResponseExt, pubID, seatNonBids)
 			seatBids = append(seatBids, *sb)
 			bidResponse.Cur = adapterSeatBids.Currency
 		}
@@ -1224,14 +1225,14 @@ func (e *exchange) makeExtBidResponse(adapterBids map[openrtb_ext.BidderName]*en
 
 // Return an openrtb seatBid for a bidder
 // buildBidResponse is responsible for ensuring nil bid seatbids are not included
-func (e *exchange) makeSeatBid(adapterBid *entities.PbsOrtbSeatBid, adapter openrtb_ext.BidderName, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, bidResponseExt *openrtb_ext.ExtBidResponse, pubID string, seatNonBids *nonBids) *openrtb2.SeatBid {
+func (e *exchange) makeSeatBid(adapterBid *entities.PbsOrtbSeatBid, adapter openrtb_ext.BidderName, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, bidRequest *openrtb_ext.RequestWrapper, bidResponseExt *openrtb_ext.ExtBidResponse, pubID string, seatNonBids *nonBids) *openrtb2.SeatBid {
 	seatBid := &openrtb2.SeatBid{
 		Seat:  adapter.String(),
 		Group: 0, // Prebid cannot support roadblocking
 	}
 
 	var errList []error
-	seatBid.Bid, errList = e.makeBid(adapterBid.Bids, auc, returnCreative, impExtInfoMap, bidResponseExt, adapter, pubID, seatNonBids)
+	seatBid.Bid, errList = e.makeBid(adapterBid.Bids, auc, returnCreative, impExtInfoMap, bidRequest, bidResponseExt, adapter, pubID, seatNonBids)
 	if len(errList) > 0 {
 		adapterExtra[adapter].Errors = append(adapterExtra[adapter].Errors, errsToBidderErrors(errList)...)
 	}
@@ -1239,11 +1240,21 @@ func (e *exchange) makeSeatBid(adapterBid *entities.PbsOrtbSeatBid, adapter open
 	return seatBid
 }
 
-func (e *exchange) makeBid(bids []*entities.PbsOrtbBid, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, bidResponseExt *openrtb_ext.ExtBidResponse, adapter openrtb_ext.BidderName, pubID string, seatNonBids *nonBids) ([]openrtb2.Bid, []error) {
+func (e *exchange) makeBid(bids []*entities.PbsOrtbBid, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, bidRequest *openrtb_ext.RequestWrapper, bidResponseExt *openrtb_ext.ExtBidResponse, adapter openrtb_ext.BidderName, pubID string, seatNonBids *nonBids) ([]openrtb2.Bid, []error) {
 	result := make([]openrtb2.Bid, 0, len(bids))
 	errs := make([]error, 0, 1)
 
 	for _, bid := range bids {
+		if !dsa.Validate(bidRequest, bid) {
+			RequiredError := openrtb_ext.ExtBidderMessage{
+				Code:    errortypes.InvalidBidResponseDSAWarningCode,
+				Message: "bid response rejected: DSA object missing when required",
+			}
+			bidResponseExt.Warnings[adapter] = append(bidResponseExt.Warnings[adapter], RequiredError)
+
+			seatNonBids.addBid(bid, int(ResponseRejectedGeneral), adapter.String())
+			continue // Don't add bid to result
+		}
 		if e.bidValidationEnforcement.BannerCreativeMaxSize == config.ValidationEnforce && bid.BidType == openrtb_ext.BidTypeBanner {
 			if !e.validateBannerCreativeSize(bid, bidResponseExt, adapter, pubID, e.bidValidationEnforcement.BannerCreativeMaxSize) {
 				seatNonBids.addBid(bid, int(ResponseRejectedCreativeSizeNotAllowed), adapter.String())
diff --git a/exchange/non_bid_reason.go b/exchange/non_bid_reason.go
index 4d817a67..2dff2dfb 100644
--- a/exchange/non_bid_reason.go
+++ b/exchange/non_bid_reason.go
@@ -6,7 +6,8 @@ package exchange
 type NonBidReason int
 
 const (
-	NoBidUnknownError                      NonBidReason = 0   // No Bid - General
+	NoBidUnknownError                      NonBidReason = 0 // No Bid - General
+	ResponseRejectedGeneral                NonBidReason = 300
 	ResponseRejectedCategoryMappingInvalid NonBidReason = 303 // Response Rejected - Category Mapping Invalid
 	ResponseRejectedCreativeSizeNotAllowed NonBidReason = 351 // Response Rejected - Invalid Creative (Size Not Allowed)
 	ResponseRejectedCreativeNotSecure      NonBidReason = 352 // Response Rejected - Invalid Creative (Not Secure)
diff --git a/openrtb_ext/imp_seedingAlliance.go b/openrtb_ext/imp_seedingAlliance.go
index 0f594d3c..d383ad39 100644
--- a/openrtb_ext/imp_seedingAlliance.go
+++ b/openrtb_ext/imp_seedingAlliance.go
@@ -1,6 +1,7 @@
 package openrtb_ext
 
 type ImpExtSeedingAlliance struct {
-	AdUnitID string `json:"adUnitId"`
-	SeatID   string `json:"seatId"`
+	AdUnitID  string `json:"adUnitId"`
+	SeatID    string `json:"seatId"`
+	AccountID string `json:"accountId"`
 }
diff --git a/openrtb_ext/regs.go b/openrtb_ext/regs.go
index 56a5179d..8057ce2c 100644
--- a/openrtb_ext/regs.go
+++ b/openrtb_ext/regs.go
@@ -2,6 +2,8 @@ package openrtb_ext
 
 // 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
+	DSA *ExtRegsDSA `json:"dsa,omitempty"`
 
 	// GDPR should be "1" if the caller believes the user is subject to GDPR laws, "0" if not, and undefined
 	// if it's unknown. For more info on this parameter, see: https://iabtechlab.com/wp-content/uploads/2018/02/OpenRTB_Advisory_GDPR_2018-02.pdf
@@ -10,3 +12,9 @@ type ExtRegs struct {
 	// USPrivacy should be a four character string, see: https://iabtechlab.com/wp-content/uploads/2019/11/OpenRTB-Extension-U.S.-Privacy-IAB-Tech-Lab.pdf
 	USPrivacy string `json:"us_privacy,omitempty"`
 }
+
+// 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"`
+}
diff --git a/openrtb_ext/request_wrapper.go b/openrtb_ext/request_wrapper.go
index d8fe02ea..d7ff5acc 100644
--- a/openrtb_ext/request_wrapper.go
+++ b/openrtb_ext/request_wrapper.go
@@ -53,6 +53,7 @@ const (
 	consentedProvidersSettingsListKey   = "consented_providers_settings"
 	consentKey                          = "consent"
 	ampKey                              = "amp"
+	dsaKey                              = "dsa"
 	eidsKey                             = "eids"
 	gdprKey                             = "gdpr"
 	prebidKey                           = "prebid"
@@ -1165,6 +1166,8 @@ func (de *DOOHExt) Clone() *DOOHExt {
 type RegExt struct {
 	ext            map[string]json.RawMessage
 	extDirty       bool
+	dsa            *ExtRegsDSA
+	dsaDirty       bool
 	gdpr           *int8
 	gdprDirty      bool
 	usPrivacy      string
@@ -1186,6 +1189,16 @@ func (re *RegExt) unmarshal(extJson json.RawMessage) error {
 		return err
 	}
 
+	dsaJson, hasDSA := re.ext[dsaKey]
+	if hasDSA {
+		re.dsa = &ExtRegsDSA{}
+	}
+	if dsaJson != nil {
+		if err := jsonutil.Unmarshal(dsaJson, re.dsa); err != nil {
+			return err
+		}
+	}
+
 	gdprJson, hasGDPR := re.ext[gdprKey]
 	if hasGDPR && gdprJson != nil {
 		if err := jsonutil.Unmarshal(gdprJson, &re.gdpr); err != nil {
@@ -1204,6 +1217,19 @@ func (re *RegExt) unmarshal(extJson json.RawMessage) error {
 }
 
 func (re *RegExt) marshal() (json.RawMessage, error) {
+	if re.dsaDirty {
+		if re.dsa != nil {
+			rawjson, err := jsonutil.Marshal(re.dsa)
+			if err != nil {
+				return nil, err
+			}
+			re.ext[dsaKey] = rawjson
+		} else {
+			delete(re.ext, dsaKey)
+		}
+		re.dsaDirty = false
+	}
+
 	if re.gdprDirty {
 		if re.gdpr != nil {
 			rawjson, err := jsonutil.Marshal(re.gdpr)
@@ -1238,7 +1264,7 @@ func (re *RegExt) marshal() (json.RawMessage, error) {
 }
 
 func (re *RegExt) Dirty() bool {
-	return re.extDirty || re.gdprDirty || re.usPrivacyDirty
+	return re.extDirty || re.dsaDirty || re.gdprDirty || re.usPrivacyDirty
 }
 
 func (re *RegExt) GetExt() map[string]json.RawMessage {
@@ -1254,9 +1280,25 @@ func (re *RegExt) SetExt(ext map[string]json.RawMessage) {
 	re.extDirty = true
 }
 
+func (re *RegExt) GetDSA() *ExtRegsDSA {
+	if re.dsa == nil {
+		return nil
+	}
+	dsa := *re.dsa
+	return &dsa
+}
+
+func (re *RegExt) SetDSA(dsa *ExtRegsDSA) {
+	re.dsa = dsa
+	re.dsaDirty = true
+}
+
 func (re *RegExt) GetGDPR() *int8 {
-	gdpr := re.gdpr
-	return gdpr
+	if re.gdpr == nil {
+		return nil
+	}
+	gdpr := *re.gdpr
+	return &gdpr
 }
 
 func (re *RegExt) SetGDPR(gdpr *int8) {
diff --git a/static/bidder-info/adf.yaml b/static/bidder-info/adf.yaml
index 2310f346..d7d9eccf 100644
--- a/static/bidder-info/adf.yaml
+++ b/static/bidder-info/adf.yaml
@@ -1,4 +1,12 @@
+# Please uncomment the appropriate endpoint URL for your datacenter
+# Europe
 endpoint: "https://adx.adform.net/adx/openrtb"
+# North/South America
+# endpoint: "https://adx2.adform.net/adx/openrtb"
+# APAC
+# endpoint: "https://adx3.adform.net/adx/openrtb"
+geoscope:
+  - global
 maintainer:
   email: "scope.sspp@adform.com"
 gvlVendorID: 50
diff --git a/static/bidder-info/bmtm.yaml b/static/bidder-info/bmtm.yaml
index 3d3f2ca6..27452195 100644
--- a/static/bidder-info/bmtm.yaml
+++ b/static/bidder-info/bmtm.yaml
@@ -1,6 +1,6 @@
 endpoint: "https://one.elitebidder.com/api/pbs"
 maintainer:
-  email: dev@brightmountainmedia.com
+  email: product@brightmountainmedia.com
 modifyingVastXmlAllowed: false
 capabilities:
   app:
diff --git a/static/bidder-info/minutemedia.yaml b/static/bidder-info/minutemedia.yaml
index efe58e57..55f5a2b7 100644
--- a/static/bidder-info/minutemedia.yaml
+++ b/static/bidder-info/minutemedia.yaml
@@ -14,5 +14,5 @@ capabilities:
       - video
 userSync:
   iframe:
-    url: https://pbs-cs.minutemedia-prebid.com/pbs-iframe?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}}
+    url: https://pbs-cs.minutemedia-prebid.com/pbs-iframe?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redirect={{.RedirectURL}}
     userMacro: "[PBS_UID]"
diff --git a/static/bidder-info/rise.yaml b/static/bidder-info/rise.yaml
index 72e56c33..79b64d5a 100644
--- a/static/bidder-info/rise.yaml
+++ b/static/bidder-info/rise.yaml
@@ -14,5 +14,5 @@ capabilities:
       - video
 userSync:
   iframe:
-    url: https://pbs-cs.yellowblue.io/pbs-iframe?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}}
+    url: https://pbs-cs.yellowblue.io/pbs-iframe?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redirect={{.RedirectURL}}
     userMacro: "[PBS_UID]"
diff --git a/static/bidder-info/taboola.yaml b/static/bidder-info/taboola.yaml
index 436f7469..0fccee14 100644
--- a/static/bidder-info/taboola.yaml
+++ b/static/bidder-info/taboola.yaml
@@ -13,8 +13,8 @@ capabilities:
       - native
 userSync:
   redirect:
-    url: https://trc.taboola.com/sg/ps/1/cm?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}}
+    url: "https://trc.taboola.com/sg/ps/1/cm?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redirect={{.RedirectURL}}"
     userMacro: "<TUID>"
   iframe:
-    url: https://cdn.taboola.com/scripts/ps-sync.html?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}}
+    url: "https://cdn.taboola.com/scripts/ps-sync.html?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redirect={{.RedirectURL}}"
     userMacro: "<TUID>"
diff --git a/static/bidder-params/seedingAlliance.json b/static/bidder-params/seedingAlliance.json
index 5fe463b6..d7208623 100644
--- a/static/bidder-params/seedingAlliance.json
+++ b/static/bidder-params/seedingAlliance.json
@@ -11,7 +11,11 @@
         },
         "seatId": {
             "type": "string",
-            "description": "Seat ID"
+            "description": "Deprecated, please use accountId"
+        },
+        "accountId": {
+            "type": "string",
+            "description": "Account ID of partner"
         }
     },
     "required": [
diff --git a/static/index.html b/static/index.html
index a3518ce2..ac79079f 100644
--- a/static/index.html
+++ b/static/index.html
@@ -3,7 +3,7 @@
         <title>Prebid Server</title>
     </head>
     <body>
-        Prebid Server is a server-to-server proxy for <a href="http://prebid.org">Prebid.js</a> users.
+        Prebid Server is a server-to-server proxy for <a href="https://prebid.org">Prebid.js</a> users.
         The host is not responsible for the content or advertising delivered through this proxy. For more information, 
         please contact prebid-server - at - prebid.org.
     </body>


Test patch:
diff --git a/adapters/ix/ixtest/supplemental/dsa-request.json b/adapters/ix/ixtest/supplemental/dsa-request.json
new file mode 100644
index 00000000..a51f2927
--- /dev/null
+++ b/adapters/ix/ixtest/supplemental/dsa-request.json
@@ -0,0 +1,193 @@
+{
+    "mockBidRequest": {
+      "id": "test-request-id",
+      "imp": [
+        {
+          "id": "test-imp-id",
+          "banner": {
+            "format": [
+              {
+                "w": 300,
+                "h": 250
+              }
+            ]
+          },
+          "ext": {
+            "bidder": {
+              "siteId": "569749"
+            }
+          }
+        }
+      ],
+      "regs": {
+        "ext": {
+          "dsa": {
+            "dsarequired": 3,
+            "pubrender": 0,
+            "datatopub": 2,
+            "transparency": [
+              {
+                "domain": "platform1domain.com",
+                "dsaparams": [
+                  1
+                ]
+              },
+              {
+                "domain": "SSP2domain.com",
+                "dsaparams": [
+                  1,
+                  2
+                ]
+              }
+            ]
+          }
+        }
+      },
+      "site": {
+        "page": "https://www.example.com/"
+      }
+    },
+    "httpCalls": [
+      {
+        "expectedRequest": {
+          "uri": "http://host/endpoint",
+          "body": {
+            "id": "test-request-id",
+            "imp": [
+              {
+                "id": "test-imp-id",
+                "banner": {
+                  "format": [
+                    {
+                      "w": 300,
+                      "h": 250
+                    }
+                  ],
+                  "w": 300,
+                  "h": 250
+                },
+                "ext": {
+                  "bidder": {
+                    "siteId": "569749"
+                  }
+                }
+              }
+            ],
+            "site": {
+              "page": "https://www.example.com/",
+              "publisher": {
+                "id": "569749"
+              }
+            },
+            "regs": {
+              "ext": {
+                "dsa": {
+                  "dsarequired": 3,
+                  "pubrender": 0,
+                  "datatopub": 2,
+                  "transparency": [
+                    {
+                      "domain": "platform1domain.com",
+                      "dsaparams": [
+                        1
+                      ]
+                    },
+                    {
+                      "domain": "SSP2domain.com",
+                      "dsaparams": [
+                        1,
+                        2
+                      ]
+                    }
+                  ]
+                }
+              }
+            }
+          }
+        },
+        "mockResponse": {
+          "status": 200,
+          "body": {
+            "id": "test-request-id",
+            "seatbid": [
+              {
+                "seat": "958",
+                "bid": [
+                  {
+                    "id": "7706636740145184841",
+                    "impid": "test-imp-id",
+                    "price": 0.5,
+                    "adid": "29681110",
+                    "adm": "some-test-ad",
+                    "adomain": [
+                      "https://advertiser.example.com"
+                    ],
+                    "cid": "958",
+                    "crid": "29681110",
+                    "h": 250,
+                    "w": 300,
+                    "ext": {
+                      "ix": {},
+                      "dsa": {
+                        "behalf": "Advertiser",
+                        "paid": "Advertiser",
+                        "transparency": {
+                          "domain": "dsp1domain.com",
+                          "params": [
+                            1,
+                            2
+                          ]
+                        },
+                        "adrender": 1
+                      }
+                    }
+                  }
+                ]
+              }
+            ],
+            "bidid": "5778926625248726496",
+            "cur": "USD"
+          }
+        }
+      }
+    ],
+    "expectedBidResponses": [
+      {
+        "currency": "USD",
+        "bids": [
+          {
+            "bid": {
+              "id": "7706636740145184841",
+              "impid": "test-imp-id",
+              "price": 0.5,
+              "adm": "some-test-ad",
+              "adid": "29681110",
+              "adomain": [
+                "https://advertiser.example.com"
+              ],
+              "cid": "958",
+              "crid": "29681110",
+              "w": 300,
+              "h": 250,
+              "ext": {
+                "ix": {},
+                "dsa": {
+                  "behalf": "Advertiser",
+                  "paid": "Advertiser",
+                  "transparency": {
+                    "domain": "dsp1domain.com",
+                    "params": [
+                      1,
+                      2
+                    ]
+                  },
+                  "adrender": 1
+                }
+              }
+            },
+            "type": "banner"
+          }
+        ]
+      }
+    ]
+  }
\ No newline at end of file
diff --git a/adapters/seedingAlliance/seedingAlliance_test.go b/adapters/seedingAlliance/seedingAlliance_test.go
index 6d9460b8..ae9b888e 100644
--- a/adapters/seedingAlliance/seedingAlliance_test.go
+++ b/adapters/seedingAlliance/seedingAlliance_test.go
@@ -136,24 +136,27 @@ func TestGetMediaTypeForBid(t *testing.T) {
 
 func TestGetExtInfo(t *testing.T) {
 	type args struct {
-		adUnitId string
-		seatId   string
+		adUnitId  string
+		seatId    string
+		accountId string
 	}
 	tests := []struct {
-		name             string
-		expectedAdUnitID string
-		expectedSeatID   string
-		data             args
-		wantErr          bool
+		name              string
+		expectedAdUnitID  string
+		expectedAccountId string
+		data              args
+		wantErr           bool
 	}{
 		{"regular case", "abc123", "pbs", args{adUnitId: "abc123"}, false},
 		{"nil case", "", "pbs", args{adUnitId: ""}, false},
 		{"unmarshal err case", "", "pbs", args{adUnitId: ""}, true},
 		{"seatId case", "abc123", "seat1", args{adUnitId: "abc123", seatId: "seat1"}, false},
+		{"accountId case", "abc123", "account1", args{adUnitId: "abc123", accountId: "account1"}, false},
+		{"accountId and seatId case", "abc123", "account1", args{adUnitId: "abc123", accountId: "account1", seatId: "seat1"}, false},
 	}
 
 	for _, test := range tests {
-		extSA, err := json.Marshal(openrtb_ext.ImpExtSeedingAlliance{AdUnitID: test.data.adUnitId, SeatID: test.data.seatId})
+		extSA, err := json.Marshal(openrtb_ext.ImpExtSeedingAlliance{AdUnitID: test.data.adUnitId, SeatID: test.data.seatId, AccountID: test.data.accountId})
 		if err != nil {
 			t.Fatalf("unexpected error %v", err)
 		}
@@ -168,7 +171,7 @@ func TestGetExtInfo(t *testing.T) {
 		}
 
 		ortbImp := openrtb2.Imp{Ext: extBidder}
-		seatID, err := getExtInfo(&ortbImp)
+		accountId, err := getExtInfo(&ortbImp)
 		if err != nil {
 			if test.wantErr {
 				continue
@@ -180,8 +183,8 @@ func TestGetExtInfo(t *testing.T) {
 			t.Fatalf("want: %v, got: %v", test.expectedAdUnitID, ortbImp.TagID)
 		}
 
-		if test.expectedSeatID != seatID {
-			t.Fatalf("want: %v, got: %v", test.expectedSeatID, seatID)
+		if test.expectedAccountId != accountId {
+			t.Fatalf("want: %v, got: %v", test.expectedAccountId, accountId)
 		}
 	}
 }
diff --git a/adapters/seedingAlliance/seedingAlliancetest/exemplary/banner_with_account.json b/adapters/seedingAlliance/seedingAlliancetest/exemplary/banner_with_account.json
new file mode 100644
index 00000000..fc3678ea
--- /dev/null
+++ b/adapters/seedingAlliance/seedingAlliancetest/exemplary/banner_with_account.json
@@ -0,0 +1,142 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "site": {
+            "publisher": {
+                "id": "foo",
+                "name": "foo"
+            }
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "adUnitId": "example-tag-id",
+                        "accountId": "123"
+                    }
+                }
+            }
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "https://mockup.seeding-alliance.de/?ssp=123",
+                "body": {
+                    "cur": [
+                        "EUR"
+                    ],
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 300,
+                                        "h": 250
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    }
+                                ]
+                            },
+                            "tagid": "example-tag-id",
+                            "ext": {
+                                "bidder": {
+                                    "adUnitId": "example-tag-id",
+                                    "accountId": "123"
+                                }
+                            }
+                        }
+                    ],
+                    "site": {
+                        "publisher": {
+                            "id": "foo",
+                            "name": "foo"
+                        }
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "seat": "123",
+                            "bid": [
+                                {
+                                    "id": "7706636740145184841",
+                                    "impid": "test-imp-id",
+                                    "price": 0.5,
+                                    "adid": "12341234",
+                                    "adm": "some-test-ad",
+                                    "adomain": [
+                                        "domain.com"
+                                    ],
+                                    "iurl": "http://abc.com/cr?id=12341234",
+                                    "cid": "123",
+                                    "crid": "12341234",
+                                    "h": 250,
+                                    "w": 300,
+                                    "ext": {
+                                        "prebid": {
+                                            "type": "banner"
+                                        }
+                                    }
+                                }
+                            ]
+                        }
+                    ],
+                    "bidid": "5778926625248726496",
+                    "cur": "EUR"
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "currency": "EUR",
+            "bids": [
+                {
+                    "bid": {
+                        "id": "7706636740145184841",
+                        "impid": "test-imp-id",
+                        "price": 0.5,
+                        "adm": "some-test-ad",
+                        "adid": "12341234",
+                        "adomain": [
+                            "domain.com"
+                        ],
+                        "iurl": "http://abc.com/cr?id=12341234",
+                        "cid": "123",
+                        "crid": "12341234",
+                        "w": 300,
+                        "h": 250,
+                        "ext": {
+                            "prebid": {
+                                "type": "banner"
+                            }
+                        }
+                    },
+                    "type": "banner"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/seedingAlliance/seedingAlliancetest/exemplary/banner_with_account_and_seat.json b/adapters/seedingAlliance/seedingAlliancetest/exemplary/banner_with_account_and_seat.json
new file mode 100644
index 00000000..3f010e60
--- /dev/null
+++ b/adapters/seedingAlliance/seedingAlliancetest/exemplary/banner_with_account_and_seat.json
@@ -0,0 +1,144 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "site": {
+            "publisher": {
+                "id": "foo",
+                "name": "foo"
+            }
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "adUnitId": "example-tag-id",
+                        "seatId": "ignored",
+                        "accountId": "123"
+                    }
+                }
+            }
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "https://mockup.seeding-alliance.de/?ssp=123",
+                "body": {
+                    "cur": [
+                        "EUR"
+                    ],
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 300,
+                                        "h": 250
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    }
+                                ]
+                            },
+                            "tagid": "example-tag-id",
+                            "ext": {
+                                "bidder": {
+                                    "adUnitId": "example-tag-id",
+                                    "seatId": "ignored",
+                                    "accountId": "123"
+                                }
+                            }
+                        }
+                    ],
+                    "site": {
+                        "publisher": {
+                            "id": "foo",
+                            "name": "foo"
+                        }
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "seat": "123",
+                            "bid": [
+                                {
+                                    "id": "7706636740145184841",
+                                    "impid": "test-imp-id",
+                                    "price": 0.5,
+                                    "adid": "12341234",
+                                    "adm": "some-test-ad",
+                                    "adomain": [
+                                        "domain.com"
+                                    ],
+                                    "iurl": "http://abc.com/cr?id=12341234",
+                                    "cid": "123",
+                                    "crid": "12341234",
+                                    "h": 250,
+                                    "w": 300,
+                                    "ext": {
+                                        "prebid": {
+                                            "type": "banner"
+                                        }
+                                    }
+                                }
+                            ]
+                        }
+                    ],
+                    "bidid": "5778926625248726496",
+                    "cur": "EUR"
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "currency": "EUR",
+            "bids": [
+                {
+                    "bid": {
+                        "id": "7706636740145184841",
+                        "impid": "test-imp-id",
+                        "price": 0.5,
+                        "adm": "some-test-ad",
+                        "adid": "12341234",
+                        "adomain": [
+                            "domain.com"
+                        ],
+                        "iurl": "http://abc.com/cr?id=12341234",
+                        "cid": "123",
+                        "crid": "12341234",
+                        "w": 300,
+                        "h": 250,
+                        "ext": {
+                            "prebid": {
+                                "type": "banner"
+                            }
+                        }
+                    },
+                    "type": "banner"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/stroeerCore/stroeercoretest/exemplary/dsa.json b/adapters/stroeerCore/stroeercoretest/exemplary/dsa.json
new file mode 100644
index 00000000..70baef5f
--- /dev/null
+++ b/adapters/stroeerCore/stroeercoretest/exemplary/dsa.json
@@ -0,0 +1,202 @@
+{
+  "mockBidRequest": {
+    "id": "auction-id",
+    "cur": [
+      "EUR"
+    ],
+    "imp": [
+      {
+        "id": "1",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "sid": "123456"
+          }
+        }
+      }
+    ],
+    "device": {
+      "ip": "123.123.123.123"
+    },
+    "site": {
+      "domain": "www.publisher.com",
+      "page": "http://www.publisher.com/some/path"
+    },
+    "user": {
+      "buyeruid": "test-buyer-user-id"
+    },
+    "regs": {
+      "ext": {
+        "dsa": {
+          "dsarequired": 3,
+          "pubrender": 0,
+          "datatopub": 2,
+          "transparency": [
+            {
+              "domain": "example-platform.com",
+              "dsaparams": [
+                1
+              ]
+            },
+            {
+              "domain": "example-ssp.com",
+              "dsaparams": [
+                1,
+                2
+              ]
+            }
+          ]
+        }
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "headers": {
+          "Accept": [
+            "application/json"
+          ],
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ]
+        },
+        "uri": "http://localhost/s2sdsh",
+        "body": {
+          "id": "auction-id",
+          "cur": [
+            "EUR"
+          ],
+          "imp": [
+            {
+              "id": "1",
+              "tagid": "123456",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "sid": "123456"
+                }
+              }
+            }
+          ],
+          "device": {
+            "ip": "123.123.123.123"
+          },
+          "site": {
+            "domain": "www.publisher.com",
+            "page": "http://www.publisher.com/some/path"
+          },
+          "user": {
+            "buyeruid": "test-buyer-user-id"
+          },
+          "regs": {
+            "ext": {
+              "dsa": {
+                "dsarequired": 3,
+                "pubrender": 0,
+                "datatopub": 2,
+                "transparency": [
+                  {
+                    "domain": "example-platform.com",
+                    "dsaparams": [
+                      1
+                    ]
+                  },
+                  {
+                    "domain": "example-ssp.com",
+                    "dsaparams": [
+                      1,
+                      2
+                    ]
+                  }
+                ]
+              }
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "bids": [
+            {
+              "id": "1829273982920-01",
+              "bidId": "1",
+              "cpm": 2,
+              "width": 300,
+              "height": 600,
+              "ad": "advert",
+              "crid": "wasd",
+              "mtype": "banner",
+              "dsa": {
+                "behalf": "Advertiser A",
+                "paid": "Advertiser B",
+                "transparency": [
+                  {
+                    "domain": "example-domain.com",
+                    "dsaparams": [
+                      1,
+                      2
+                    ]
+                  }
+                ],
+                "adrender": 1
+              }
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "EUR",
+      "bids": [
+        {
+          "bid": {
+            "id": "1829273982920-01",
+            "impid": "1",
+            "price": 2,
+            "adm": "advert",
+            "w": 300,
+            "h": 600,
+            "crid": "wasd",
+            "mtype": 1,
+            "ext": {
+              "dsa": {
+                "behalf": "Advertiser A",
+                "paid": "Advertiser B",
+                "transparency": [
+                  {
+                    "domain": "example-domain.com",
+                    "dsaparams": [
+                      1,
+                      2
+                    ]
+                  }
+                ],
+                "adrender": 1
+              }
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/amp/parse_test.go b/amp/parse_test.go
index a377c471..f2f09728 100644
--- a/amp/parse_test.go
+++ b/amp/parse_test.go
@@ -284,18 +284,24 @@ func TestPrivacyReader(t *testing.T) {
 						ampParams: Params{Consent: "1YYY"},
 					},
 					expected: expectedResults{
-						policyWriter: ccpa.ConsentWriter{"1YYY"},
+						policyWriter: ccpa.ConsentWriter{Consent: "1YYY"},
 						warning:      nil,
 					},
 				},
 				{
 					desc: "No consent type, valid CCPA consent string and gdpr_applies set to true: expect a CCPA consent writer and a warning",
 					in: testInput{
-						ampParams: Params{Consent: "1YYY", GdprApplies: &boolTrue},
+						ampParams: Params{
+							Consent:     "1YYY",
+							GdprApplies: &boolTrue,
+						},
 					},
 					expected: expectedResults{
-						policyWriter: ccpa.ConsentWriter{"1YYY"},
-						warning:      &errortypes.Warning{Message: "AMP request gdpr_applies value was ignored because provided consent string is a CCPA consent string", WarningCode: errortypes.InvalidPrivacyConsentWarningCode},
+						policyWriter: ccpa.ConsentWriter{Consent: "1YYY"},
+						warning: &errortypes.Warning{
+							Message:     "AMP request gdpr_applies value was ignored because provided consent string is a CCPA consent string",
+							WarningCode: errortypes.InvalidPrivacyConsentWarningCode,
+						},
 					},
 				},
 				{
@@ -304,8 +310,11 @@ func TestPrivacyReader(t *testing.T) {
 						ampParams: Params{Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA"},
 					},
 					expected: expectedResults{
-						policyWriter: gdpr.ConsentWriter{"CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", &int8One},
-						warning:      nil,
+						policyWriter: gdpr.ConsentWriter{
+							Consent:    "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA",
+							RegExtGDPR: &int8One,
+						},
+						warning: nil,
 					},
 				},
 			},
@@ -316,41 +325,63 @@ func TestPrivacyReader(t *testing.T) {
 				{
 					desc: "Unrecognized consent type was specified and invalid consent string provided: expect nil policy writer and a warning",
 					in: testInput{
-						ampParams: Params{ConsentType: 101, Consent: "NOT_CCPA_NOR_GDPR_TCF2"},
+						ampParams: Params{
+							ConsentType: 101,
+							Consent:     "NOT_CCPA_NOR_GDPR_TCF2",
+						},
 					},
 					expected: expectedResults{
 						policyWriter: privacy.NilPolicyWriter{},
-						warning:      &errortypes.Warning{Message: "Consent string 'NOT_CCPA_NOR_GDPR_TCF2' is not recognized as one of the supported formats CCPA or TCF2.", WarningCode: errortypes.InvalidPrivacyConsentWarningCode},
+						warning: &errortypes.Warning{
+							Message:     "Consent string 'NOT_CCPA_NOR_GDPR_TCF2' is not recognized as one of the supported formats CCPA or TCF2.",
+							WarningCode: errortypes.InvalidPrivacyConsentWarningCode,
+						},
 					},
 				},
 				{
 					desc: "Unrecognized consent type specified but query params come with a valid CCPA consent string: expect a CCPA consent writer and no error nor warning",
 					in: testInput{
-						ampParams: Params{ConsentType: 101, Consent: "1YYY"},
+						ampParams: Params{
+							ConsentType: 101,
+							Consent:     "1YYY",
+						},
 					},
 					expected: expectedResults{
-						policyWriter: ccpa.ConsentWriter{"1YYY"},
+						policyWriter: ccpa.ConsentWriter{Consent: "1YYY"},
 						warning:      nil,
 					},
 				},
 				{
 					desc: "Unrecognized consent type, valid CCPA consent string and gdpr_applies set to true: expect a CCPA consent writer and a warning",
 					in: testInput{
-						ampParams: Params{ConsentType: 101, Consent: "1YYY", GdprApplies: &boolTrue},
+						ampParams: Params{
+							ConsentType: 101,
+							Consent:     "1YYY",
+							GdprApplies: &boolTrue,
+						},
 					},
 					expected: expectedResults{
-						policyWriter: ccpa.ConsentWriter{"1YYY"},
-						warning:      &errortypes.Warning{Message: "AMP request gdpr_applies value was ignored because provided consent string is a CCPA consent string", WarningCode: errortypes.InvalidPrivacyConsentWarningCode},
+						policyWriter: ccpa.ConsentWriter{Consent: "1YYY"},
+						warning: &errortypes.Warning{
+							Message:     "AMP request gdpr_applies value was ignored because provided consent string is a CCPA consent string",
+							WarningCode: errortypes.InvalidPrivacyConsentWarningCode,
+						},
 					},
 				},
 				{
 					desc: "Unrecognized consent type, valid TCF2 consent string and gdpr_applies not set: expect GDPR consent writer and no error nor warning",
 					in: testInput{
-						ampParams: Params{ConsentType: 101, Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA"},
+						ampParams: Params{
+							ConsentType: 101,
+							Consent:     "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA",
+						},
 					},
 					expected: expectedResults{
-						policyWriter: gdpr.ConsentWriter{"CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", &int8One},
-						warning:      nil,
+						policyWriter: gdpr.ConsentWriter{
+							Consent:    "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA",
+							RegExtGDPR: &int8One,
+						},
+						warning: nil,
 					},
 				},
 			},
@@ -361,51 +392,91 @@ func TestPrivacyReader(t *testing.T) {
 				{
 					desc: "GDPR consent string is invalid, but consent type is TCF2: return a valid GDPR writer and warn about the GDPR string being invalid",
 					in: testInput{
-						ampParams: Params{Consent: "INVALID_GDPR", ConsentType: ConsentTCF2, GdprApplies: nil},
+						ampParams: Params{
+							Consent:     "INVALID_GDPR",
+							ConsentType: ConsentTCF2,
+							GdprApplies: nil,
+						},
 					},
 					expected: expectedResults{
-						policyWriter: gdpr.ConsentWriter{"INVALID_GDPR", &int8One},
-						warning:      &errortypes.Warning{Message: "Consent string 'INVALID_GDPR' is not a valid TCF2 consent string.", WarningCode: errortypes.InvalidPrivacyConsentWarningCode},
+						policyWriter: gdpr.ConsentWriter{
+							Consent:    "INVALID_GDPR",
+							RegExtGDPR: &int8One,
+						},
+						warning: &errortypes.Warning{
+							Message:     "Consent string 'INVALID_GDPR' is not a valid TCF2 consent string.",
+							WarningCode: errortypes.InvalidPrivacyConsentWarningCode,
+						},
 					},
 				},
 				{
 					desc: "GDPR consent string is invalid, consent type is TCF2, gdpr_applies is set to true: return a valid GDPR writer and warn about the GDPR string being invalid",
 					in: testInput{
-						ampParams: Params{Consent: "INVALID_GDPR", ConsentType: ConsentTCF2, GdprApplies: &boolFalse},
+						ampParams: Params{
+							Consent:     "INVALID_GDPR",
+							ConsentType: ConsentTCF2,
+							GdprApplies: &boolFalse,
+						},
 					},
 					expected: expectedResults{
-						policyWriter: gdpr.ConsentWriter{"INVALID_GDPR", &int8Zero},
-						warning:      &errortypes.Warning{Message: "Consent string 'INVALID_GDPR' is not a valid TCF2 consent string.", WarningCode: errortypes.InvalidPrivacyConsentWarningCode},
+						policyWriter: gdpr.ConsentWriter{
+							Consent:    "INVALID_GDPR",
+							RegExtGDPR: &int8Zero,
+						},
+						warning: &errortypes.Warning{
+							Message:     "Consent string 'INVALID_GDPR' is not a valid TCF2 consent string.",
+							WarningCode: errortypes.InvalidPrivacyConsentWarningCode,
+						},
 					},
 				},
 				{
 					desc: "Valid GDPR consent string, gdpr_applies is set to false, return a valid GDPR writer, no warning",
 					in: testInput{
-						ampParams: Params{Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", ConsentType: ConsentTCF2, GdprApplies: &boolFalse},
+						ampParams: Params{
+							Consent:     "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA",
+							ConsentType: ConsentTCF2,
+							GdprApplies: &boolFalse,
+						},
 					},
 					expected: expectedResults{
-						policyWriter: gdpr.ConsentWriter{"CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", &int8Zero},
-						warning:      nil,
+						policyWriter: gdpr.ConsentWriter{
+							Consent:    "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA",
+							RegExtGDPR: &int8Zero,
+						},
+						warning: nil,
 					},
 				},
 				{
 					desc: "Valid GDPR consent string, gdpr_applies is set to true, return a valid GDPR writer and no warning",
 					in: testInput{
-						ampParams: Params{Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", ConsentType: ConsentTCF2, GdprApplies: &boolTrue},
+						ampParams: Params{
+							Consent:     "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA",
+							ConsentType: ConsentTCF2,
+							GdprApplies: &boolTrue,
+						},
 					},
 					expected: expectedResults{
-						policyWriter: gdpr.ConsentWriter{"CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", &int8One},
-						warning:      nil,
+						policyWriter: gdpr.ConsentWriter{
+							Consent:    "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA",
+							RegExtGDPR: &int8One,
+						},
+						warning: nil,
 					},
 				},
 				{
 					desc: "Valid GDPR consent string, return a valid GDPR writer and no warning",
 					in: testInput{
-						ampParams: Params{Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", ConsentType: ConsentTCF2},
+						ampParams: Params{
+							Consent:     "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA",
+							ConsentType: ConsentTCF2,
+						},
 					},
 					expected: expectedResults{
-						policyWriter: gdpr.ConsentWriter{"CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", &int8One},
-						warning:      nil,
+						policyWriter: gdpr.ConsentWriter{
+							Consent:    "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA",
+							RegExtGDPR: &int8One,
+						},
+						warning: nil,
 					},
 				},
 			},
@@ -416,30 +487,46 @@ func TestPrivacyReader(t *testing.T) {
 				{
 					desc: "CCPA consent string is invalid: return a valid writer a warning about the string being invalid",
 					in: testInput{
-						ampParams: Params{Consent: "XXXX", ConsentType: ConsentUSPrivacy},
+						ampParams: Params{
+							Consent:     "XXXX",
+							ConsentType: ConsentUSPrivacy,
+						},
 					},
 					expected: expectedResults{
-						policyWriter: ccpa.ConsentWriter{"XXXX"},
-						warning:      &errortypes.Warning{Message: "Consent string 'XXXX' is not a valid CCPA consent string.", WarningCode: errortypes.InvalidPrivacyConsentWarningCode},
+						policyWriter: ccpa.ConsentWriter{Consent: "XXXX"},
+						warning: &errortypes.Warning{
+							Message:     "Consent string 'XXXX' is not a valid CCPA consent string.",
+							WarningCode: errortypes.InvalidPrivacyConsentWarningCode,
+						},
 					},
 				},
 				{
 					desc: "Valid CCPA consent string, gdpr_applies is set to true: return a valid GDPR writer and warn about the gdpr_applies value.",
 					in: testInput{
-						ampParams: Params{Consent: "1YYY", ConsentType: ConsentUSPrivacy, GdprApplies: &boolTrue},
+						ampParams: Params{
+							Consent:     "1YYY",
+							ConsentType: ConsentUSPrivacy,
+							GdprApplies: &boolTrue,
+						},
 					},
 					expected: expectedResults{
-						policyWriter: ccpa.ConsentWriter{"1YYY"},
-						warning:      &errortypes.Warning{Message: "AMP request gdpr_applies value was ignored because provided consent string is a CCPA consent string", WarningCode: errortypes.InvalidPrivacyConsentWarningCode},
+						policyWriter: ccpa.ConsentWriter{Consent: "1YYY"},
+						warning: &errortypes.Warning{
+							Message:     "AMP request gdpr_applies value was ignored because provided consent string is a CCPA consent string",
+							WarningCode: errortypes.InvalidPrivacyConsentWarningCode,
+						},
 					},
 				},
 				{
 					desc: "Valid CCPA consent string, return a valid GDPR writer and no warning",
 					in: testInput{
-						ampParams: Params{Consent: "1YYY", ConsentType: ConsentUSPrivacy},
+						ampParams: Params{
+							Consent:     "1YYY",
+							ConsentType: ConsentUSPrivacy,
+						},
 					},
 					expected: expectedResults{
-						policyWriter: ccpa.ConsentWriter{"1YYY"},
+						policyWriter: ccpa.ConsentWriter{Consent: "1YYY"},
 						warning:      nil,
 					},
 				},
@@ -469,19 +556,34 @@ func TestBuildGdprTCF2ConsentWriter(t *testing.T) {
 		expectedWriter gdpr.ConsentWriter
 	}{
 		{
-			desc:           "gdpr_applies not set",
-			inParams:       Params{Consent: consentString},
-			expectedWriter: gdpr.ConsentWriter{consentString, &int8One},
+			desc:     "gdpr_applies not set",
+			inParams: Params{Consent: consentString},
+			expectedWriter: gdpr.ConsentWriter{
+				Consent:    consentString,
+				RegExtGDPR: &int8One,
+			},
 		},
 		{
-			desc:           "gdpr_applies set to false",
-			inParams:       Params{Consent: consentString, GdprApplies: &boolFalse},
-			expectedWriter: gdpr.ConsentWriter{consentString, &int8Zero},
+			desc: "gdpr_applies set to false",
+			inParams: Params{
+				Consent:     consentString,
+				GdprApplies: &boolFalse,
+			},
+			expectedWriter: gdpr.ConsentWriter{
+				Consent:    consentString,
+				RegExtGDPR: &int8Zero,
+			},
 		},
 		{
-			desc:           "gdpr_applies set to true",
-			inParams:       Params{Consent: consentString, GdprApplies: &boolTrue},
-			expectedWriter: gdpr.ConsentWriter{consentString, &int8One},
+			desc: "gdpr_applies set to true",
+			inParams: Params{
+				Consent:     consentString,
+				GdprApplies: &boolTrue,
+			},
+			expectedWriter: gdpr.ConsentWriter{
+				Consent:    consentString,
+				RegExtGDPR: &int8One,
+			},
 		},
 	}
 	for _, tc := range testCases {
diff --git a/dsa/validate_test.go b/dsa/validate_test.go
new file mode 100644
index 00000000..74d428c3
--- /dev/null
+++ b/dsa/validate_test.go
@@ -0,0 +1,178 @@
+package dsa
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/prebid/openrtb/v20/openrtb2"
+	"github.com/prebid/prebid-server/v2/exchange/entities"
+	"github.com/prebid/prebid-server/v2/openrtb_ext"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestValidate(t *testing.T) {
+	tests := []struct {
+		name        string
+		giveRequest *openrtb_ext.RequestWrapper
+		giveBid     *entities.PbsOrtbBid
+		wantValid   bool
+	}{
+		{
+			name: "not_required",
+			giveRequest: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Regs: &openrtb2.Regs{
+						Ext: json.RawMessage(`{"dsa": {"dsarequired": 0}}`),
+					},
+				},
+			},
+			giveBid:   nil,
+			wantValid: true,
+		},
+		{
+			name: "required_and_bid_is_nil",
+			giveRequest: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Regs: &openrtb2.Regs{
+						Ext: json.RawMessage(`{"dsa": {"dsarequired": 2}}`),
+					},
+				},
+			},
+			giveBid:   nil,
+			wantValid: false,
+		},
+		{
+			name: "required_and_bid.bid_is_nil",
+			giveRequest: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Regs: &openrtb2.Regs{
+						Ext: json.RawMessage(`{"dsa": {"dsarequired": 2}}`),
+					},
+				},
+			},
+			giveBid:   &entities.PbsOrtbBid{},
+			wantValid: false,
+		},
+		{
+			name: "required_and_bid.ext.dsa_not_present",
+			giveRequest: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Regs: &openrtb2.Regs{
+						Ext: json.RawMessage(`{"dsa": {"dsarequired": 2}}`),
+					},
+				},
+			},
+			giveBid: &entities.PbsOrtbBid{
+				Bid: &openrtb2.Bid{
+					Ext: json.RawMessage(`{}`),
+				},
+			},
+			wantValid: false,
+		},
+		{
+			name: "required_and_bid.ext.dsa_present",
+			giveRequest: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Regs: &openrtb2.Regs{
+						Ext: json.RawMessage(`{"dsa": {"dsarequired": 2}}`),
+					},
+				},
+			},
+			giveBid: &entities.PbsOrtbBid{
+				Bid: &openrtb2.Bid{
+					Ext: json.RawMessage(`{"dsa": {}}`),
+				},
+			},
+			wantValid: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			valid := Validate(tt.giveRequest, tt.giveBid)
+			assert.Equal(t, tt.wantValid, valid)
+		})
+	}
+}
+
+func TestDSARequired(t *testing.T) {
+	tests := []struct {
+		name         string
+		giveRequest  *openrtb_ext.RequestWrapper
+		wantRequired bool
+	}{
+		{
+			name: "not_required_and_reg.ext.dsa_is_nil",
+			giveRequest: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Regs: &openrtb2.Regs{
+						Ext: json.RawMessage(`{}`),
+					},
+				},
+			},
+			wantRequired: false,
+		},
+		{
+			name: "not_required_and_reg.ext.dsa_is_empty",
+			giveRequest: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Regs: &openrtb2.Regs{
+						Ext: json.RawMessage(`{"dsa": {}}`),
+					},
+				},
+			},
+			wantRequired: false,
+		},
+		{
+			name: "required_and_reg.ext.dsa_is_0",
+			giveRequest: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Regs: &openrtb2.Regs{
+						Ext: json.RawMessage(`{"dsa": {"dsarequired": 0}}`),
+					},
+				},
+			},
+			wantRequired: false,
+		},
+		{
+			name: "required_and_reg.ext.dsa_is_1",
+			giveRequest: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Regs: &openrtb2.Regs{
+						Ext: json.RawMessage(`{"dsa": {"dsarequired": 1}}`),
+					},
+				},
+			},
+			wantRequired: false,
+		},
+		{
+			name: "required_and_reg.ext.dsa_is_2",
+			giveRequest: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Regs: &openrtb2.Regs{
+						Ext: json.RawMessage(`{"dsa": {"dsarequired": 2}}`),
+					},
+				},
+			},
+			wantRequired: true,
+		},
+		{
+			name: "required_and_reg.ext.dsa_is_3",
+			giveRequest: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Regs: &openrtb2.Regs{
+						Ext: json.RawMessage(`{"dsa": {"dsarequired": 3}}`),
+					},
+				},
+			},
+			wantRequired: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			required := dsaRequired(tt.giveRequest)
+			assert.Equal(t, tt.wantRequired, required)
+		})
+	}
+}
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index 6cd48198..e484e21a 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -144,18 +144,20 @@ func TestCharacterEscape(t *testing.T) {
 	adapterBids["appnexus"] = &entities.PbsOrtbSeatBid{Currency: "USD"}
 
 	//An openrtb2.BidRequest struct as specified in https://github.com/prebid/prebid-server/issues/465
-	bidRequest := &openrtb2.BidRequest{
-		ID: "some-request-id",
-		Imp: []openrtb2.Imp{{
-			ID:     "some-impression-id",
-			Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}},
-			Ext:    json.RawMessage(`{"appnexus": {"placementId": 1}}`),
-		}},
-		Site:   &openrtb2.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)},
-		Device: &openrtb2.Device{UA: "curl/7.54.0", IP: "::1"},
-		AT:     1,
-		TMax:   500,
-		Ext:    json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 1}}}],"tmax": 500}`),
+	bidRequest := &openrtb_ext.RequestWrapper{
+		BidRequest: &openrtb2.BidRequest{
+			ID: "some-request-id",
+			Imp: []openrtb2.Imp{{
+				ID:     "some-impression-id",
+				Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}},
+				Ext:    json.RawMessage(`{"appnexus": {"placementId": 1}}`),
+			}},
+			Site:   &openrtb2.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)},
+			Device: &openrtb2.Device{UA: "curl/7.54.0", IP: "::1"},
+			AT:     1,
+			TMax:   500,
+			Ext:    json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 1}}}],"tmax": 500}`),
+		},
 	}
 
 	//adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra,
@@ -1303,34 +1305,36 @@ func TestGetBidCacheInfoEndToEnd(t *testing.T) {
 			},
 		},
 	}
-	bidRequest := &openrtb2.BidRequest{
-		ID:   "some-request-id",
-		TMax: 1000,
-		Imp: []openrtb2.Imp{
-			{
-				ID:     "test-div",
-				Secure: openrtb2.Int8Ptr(0),
-				Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}},
-				Ext: json.RawMessage(` {
-    "rubicon": {
-        "accountId": 1001,
-        "siteId": 113932,
-        "zoneId": 535510
-    },
-    "appnexus": { "placementId": 1 },
-    "pubmatic": { "publisherId": "156209", "adSlot": "pubmatic_test2@300x250" },
-    "pulsepoint": { "cf": "300X250", "cp": 512379, "ct": 486653 },
-    "conversant": { "site_id": "108060" },
-    "ix": { "siteId": "287415" }
-}`),
+	bidRequest := &openrtb_ext.RequestWrapper{
+		BidRequest: &openrtb2.BidRequest{
+			ID:   "some-request-id",
+			TMax: 1000,
+			Imp: []openrtb2.Imp{
+				{
+					ID:     "test-div",
+					Secure: openrtb2.Int8Ptr(0),
+					Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}},
+					Ext: json.RawMessage(` {
+						"rubicon": {
+							"accountId": 1001,
+							"siteId": 113932,
+							"zoneId": 535510
+						},
+						"appnexus": { "placementId": 1 },
+						"pubmatic": { "publisherId": "156209", "adSlot": "pubmatic_test2@300x250" },
+						"pulsepoint": { "cf": "300X250", "cp": 512379, "ct": 486653 },
+						"conversant": { "site_id": "108060" },
+						"ix": { "siteId": "287415" }
+					}`),
+				},
 			},
+			Site: &openrtb2.Site{
+				Page:      "http://rubitest.com/index.html",
+				Publisher: &openrtb2.Publisher{ID: "1001"},
+			},
+			Test: 1,
+			Ext:  json.RawMessage(`{"prebid": { "cache": { "bids": {}, "vastxml": {} }, "targeting": { "pricegranularity": "med", "includewinners": true, "includebidderkeys": false } }}`),
 		},
-		Site: &openrtb2.Site{
-			Page:      "http://rubitest.com/index.html",
-			Publisher: &openrtb2.Publisher{ID: "1001"},
-		},
-		Test: 1,
-		Ext:  json.RawMessage(`{"prebid": { "cache": { "bids": {}, "vastxml": {} }, "targeting": { "pricegranularity": "med", "includewinners": true, "includebidderkeys": false } }}`),
 	}
 
 	var errList []error
@@ -1426,7 +1430,7 @@ func TestBidReturnsCreative(t *testing.T) {
 
 	//Run tests
 	for _, test := range testCases {
-		resultingBids, resultingErrs := e.makeBid(sampleBids, sampleAuction, test.inReturnCreative, nil, nil, "", "", &nonBids{})
+		resultingBids, resultingErrs := e.makeBid(sampleBids, sampleAuction, test.inReturnCreative, nil, &openrtb_ext.RequestWrapper{}, nil, "", "", &nonBids{})
 
 		assert.Equal(t, 0, len(resultingErrs), "%s. Test should not return errors \n", test.description)
 		assert.Equal(t, test.expectedCreativeMarkup, resultingBids[0].AdM, "%s. Ad markup string doesn't match expected \n", test.description)
@@ -1594,18 +1598,20 @@ func TestBidResponseCurrency(t *testing.T) {
 	liveAdapters := make([]openrtb_ext.BidderName, 1)
 	liveAdapters[0] = "appnexus"
 
-	bidRequest := &openrtb2.BidRequest{
-		ID: "some-request-id",
-		Imp: []openrtb2.Imp{{
-			ID:     "some-impression-id",
-			Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}},
-			Ext:    json.RawMessage(`{"appnexus": {"placementId": 10433394}}`),
-		}},
-		Site:   &openrtb2.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)},
-		Device: &openrtb2.Device{UA: "curl/7.54.0", IP: "::1"},
-		AT:     1,
-		TMax:   500,
-		Ext:    json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 10433394}}}],"tmax": 500}`),
+	bidRequest := &openrtb_ext.RequestWrapper{
+		BidRequest: &openrtb2.BidRequest{
+			ID: "some-request-id",
+			Imp: []openrtb2.Imp{{
+				ID:     "some-impression-id",
+				Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}},
+				Ext:    json.RawMessage(`{"appnexus": {"placementId": 10433394}}`),
+			}},
+			Site:   &openrtb2.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)},
+			Device: &openrtb2.Device{UA: "curl/7.54.0", IP: "::1"},
+			AT:     1,
+			TMax:   500,
+			Ext:    json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 10433394}}}],"tmax": 500}`),
+		},
 	}
 
 	adapterExtra := map[openrtb_ext.BidderName]*seatResponseExtra{
@@ -1740,14 +1746,16 @@ func TestBidResponseImpExtInfo(t *testing.T) {
 	liveAdapters := make([]openrtb_ext.BidderName, 1)
 	liveAdapters[0] = "appnexus"
 
-	bidRequest := &openrtb2.BidRequest{
-		ID: "some-request-id",
-		Imp: []openrtb2.Imp{{
-			ID:    "some-impression-id",
-			Video: &openrtb2.Video{},
-			Ext:   json.RawMessage(`{"appnexus": {"placementId": 10433394}}`),
-		}},
-		Ext: json.RawMessage(``),
+	bidRequest := &openrtb_ext.RequestWrapper{
+		BidRequest: &openrtb2.BidRequest{
+			ID: "some-request-id",
+			Imp: []openrtb2.Imp{{
+				ID:    "some-impression-id",
+				Video: &openrtb2.Video{},
+				Ext:   json.RawMessage(`{"appnexus": {"placementId": 10433394}}`),
+			}},
+			Ext: json.RawMessage(``),
+		},
 	}
 
 	var errList []error
@@ -4735,17 +4743,42 @@ func TestMakeBidWithValidation(t *testing.T) {
 	sampleAd := "<?xml version=\"1.0\" encoding=\"UTF-8\"?><VAST ...></VAST>"
 	sampleOpenrtbBid := &openrtb2.Bid{ID: "some-bid-id", AdM: sampleAd}
 
-	// Define test cases
 	testCases := []struct {
-		description       string
-		givenValidations  config.Validations
-		givenBids         []*entities.PbsOrtbBid
-		givenSeat         openrtb_ext.BidderName
-		expectedNumOfBids int
-		expectedNonBids   *nonBids
+		name                     string
+		givenBidRequestExt       json.RawMessage
+		givenValidations         config.Validations
+		givenBids                []*entities.PbsOrtbBid
+		givenSeat                openrtb_ext.BidderName
+		expectedNumOfBids        int
+		expectedNonBids          *nonBids
+		expectedNumDebugErrors   int
+		expectedNumDebugWarnings int
 	}{
 		{
-			description:       "Validation is enforced, and one bid out of the two is invalid based on dimensions",
+			name:               "One_of_two_bids_is_invalid_based_on_DSA_object_presence",
+			givenBidRequestExt: json.RawMessage(`{"dsa": {"dsarequired": 2}}`),
+			givenValidations:   config.Validations{},
+			givenBids:          []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{Ext: json.RawMessage(`{"dsa": {}}`)}}, {Bid: &openrtb2.Bid{}}},
+			givenSeat:          "pubmatic",
+			expectedNumOfBids:  1,
+			expectedNonBids: &nonBids{
+				seatNonBidsMap: map[string][]openrtb_ext.NonBid{
+					"pubmatic": {
+						{
+							StatusCode: 300,
+							Ext: openrtb_ext.NonBidExt{
+								Prebid: openrtb_ext.ExtResponseNonBidPrebid{
+									Bid: openrtb_ext.NonBidObject{},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedNumDebugWarnings: 1,
+		},
+		{
+			name:              "Creative_size_validation_enforced,_one_of_two_bids_has_invalid_dimensions",
 			givenValidations:  config.Validations{BannerCreativeMaxSize: config.ValidationEnforce, MaxCreativeWidth: 100, MaxCreativeHeight: 100},
 			givenBids:         []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{W: 200, H: 200}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{W: 50, H: 50}, BidType: openrtb_ext.BidTypeBanner}},
 			givenSeat:         "pubmatic",
@@ -4767,17 +4800,19 @@ func TestMakeBidWithValidation(t *testing.T) {
 					},
 				},
 			},
+			expectedNumDebugErrors: 1,
 		},
 		{
-			description:       "Validation is warned, so no bids should be removed (Validating CreativeMaxSize) ",
-			givenValidations:  config.Validations{BannerCreativeMaxSize: config.ValidationWarn, MaxCreativeWidth: 100, MaxCreativeHeight: 100},
-			givenBids:         []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{W: 200, H: 200}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{W: 50, H: 50}, BidType: openrtb_ext.BidTypeBanner}},
-			givenSeat:         "pubmatic",
-			expectedNumOfBids: 2,
-			expectedNonBids:   &nonBids{},
+			name:                   "Creative_size_validation_warned,_one_of_two_bids_has_invalid_dimensions",
+			givenValidations:       config.Validations{BannerCreativeMaxSize: config.ValidationWarn, MaxCreativeWidth: 100, MaxCreativeHeight: 100},
+			givenBids:              []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{W: 200, H: 200}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{W: 50, H: 50}, BidType: openrtb_ext.BidTypeBanner}},
+			givenSeat:              "pubmatic",
+			expectedNumOfBids:      2,
+			expectedNonBids:        &nonBids{},
+			expectedNumDebugErrors: 1,
 		},
 		{
-			description:       "Validation is enforced, and one bid out of the two is invalid based on AdM",
+			name:              "AdM_validation_enforced,_one_of_two_bids_has_invalid_AdM",
 			givenValidations:  config.Validations{SecureMarkup: config.ValidationEnforce},
 			givenBids:         []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{AdM: "http://domain.com/invalid", ImpID: "1"}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{AdM: "https://domain.com/valid", ImpID: "2"}, BidType: openrtb_ext.BidTypeBanner}},
 			givenSeat:         "pubmatic",
@@ -4792,17 +4827,19 @@ func TestMakeBidWithValidation(t *testing.T) {
 					},
 				},
 			},
+			expectedNumDebugErrors: 1,
 		},
 		{
-			description:       "Validation is warned so no bids should be removed (Validating SecureMarkup)",
-			givenValidations:  config.Validations{SecureMarkup: config.ValidationWarn},
-			givenBids:         []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{AdM: "http://domain.com/invalid", ImpID: "1"}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{AdM: "https://domain.com/valid", ImpID: "2"}, BidType: openrtb_ext.BidTypeBanner}},
-			givenSeat:         "pubmatic",
-			expectedNumOfBids: 2,
-			expectedNonBids:   &nonBids{},
+			name:                   "AdM_validation_warned,_one_of_two_bids_has_invalid_AdM",
+			givenValidations:       config.Validations{SecureMarkup: config.ValidationWarn},
+			givenBids:              []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{AdM: "http://domain.com/invalid", ImpID: "1"}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{AdM: "https://domain.com/valid", ImpID: "2"}, BidType: openrtb_ext.BidTypeBanner}},
+			givenSeat:              "pubmatic",
+			expectedNumOfBids:      2,
+			expectedNonBids:        &nonBids{},
+			expectedNumDebugErrors: 1,
 		},
 		{
-			description:       "Adm validation is skipped, creative size validation is enforced, one Adm is invalid, but because we skip, no bids should be removed",
+			name:              "Adm_validation_skipped,_creative_size_validation_enforced,_one_of_two_bids_has_invalid_AdM",
 			givenValidations:  config.Validations{SecureMarkup: config.ValidationSkip, BannerCreativeMaxSize: config.ValidationEnforce},
 			givenBids:         []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{AdM: "http://domain.com/invalid"}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{AdM: "https://domain.com/valid"}, BidType: openrtb_ext.BidTypeBanner}},
 			givenSeat:         "pubmatic",
@@ -4810,8 +4847,8 @@ func TestMakeBidWithValidation(t *testing.T) {
 			expectedNonBids:   &nonBids{},
 		},
 		{
-			description:       "Creative Size Validation is skipped, Adm Validation is enforced, one Creative Size is invalid, but because we skip, no bids should be removed",
-			givenValidations:  config.Validations{BannerCreativeMaxSize: config.ValidationWarn, MaxCreativeWidth: 100, MaxCreativeHeight: 100},
+			name:              "Creative_size_validation_skipped,_Adm_Validation_enforced,_one_of_two_bids_has_invalid_dimensions",
+			givenValidations:  config.Validations{BannerCreativeMaxSize: config.ValidationSkip, MaxCreativeWidth: 100, MaxCreativeHeight: 100},
 			givenBids:         []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{W: 200, H: 200}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{W: 50, H: 50}, BidType: openrtb_ext.BidTypeBanner}},
 			givenSeat:         "pubmatic",
 			expectedNumOfBids: 2,
@@ -4844,23 +4881,34 @@ func TestMakeBidWithValidation(t *testing.T) {
 
 	e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0))
 
-	bidExtResponse := &openrtb_ext.ExtBidResponse{Errors: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage)}
-
 	ImpExtInfoMap := make(map[string]ImpExtInfo)
 	ImpExtInfoMap["1"] = ImpExtInfo{}
 	ImpExtInfoMap["2"] = ImpExtInfo{}
 
 	//Run tests
 	for _, test := range testCases {
-		t.Run(test.description, func(t *testing.T) {
+		t.Run(test.name, func(t *testing.T) {
+			bidExtResponse := &openrtb_ext.ExtBidResponse{
+				Errors:   make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage),
+				Warnings: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage),
+			}
+			bidRequest := &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Regs: &openrtb2.Regs{
+						Ext: test.givenBidRequestExt,
+					},
+				},
+			}
 			e.bidValidationEnforcement = test.givenValidations
 			sampleBids := test.givenBids
 			nonBids := &nonBids{}
-			resultingBids, resultingErrs := e.makeBid(sampleBids, sampleAuction, true, ImpExtInfoMap, bidExtResponse, test.givenSeat, "", nonBids)
+			resultingBids, resultingErrs := e.makeBid(sampleBids, sampleAuction, true, ImpExtInfoMap, bidRequest, bidExtResponse, test.givenSeat, "", nonBids)
 
-			assert.Equal(t, 0, len(resultingErrs), "%s. Test should not return errors \n", test.description)
-			assert.Equal(t, test.expectedNumOfBids, len(resultingBids), "%s. Test returns more valid bids than expected\n", test.description)
-			assert.Equal(t, test.expectedNonBids, nonBids, "%s. Test returns incorrect nonBids\n", test.description)
+			assert.Equal(t, 0, len(resultingErrs))
+			assert.Equal(t, test.expectedNumOfBids, len(resultingBids))
+			assert.Equal(t, test.expectedNonBids, nonBids)
+			assert.Equal(t, test.expectedNumDebugErrors, len(bidExtResponse.Errors))
+			assert.Equal(t, test.expectedNumDebugWarnings, len(bidExtResponse.Warnings))
 		})
 	}
 }
diff --git a/exchange/exchangetest/dsa-not-required.json b/exchange/exchangetest/dsa-not-required.json
new file mode 100644
index 00000000..f2a1f821
--- /dev/null
+++ b/exchange/exchangetest/dsa-not-required.json
@@ -0,0 +1,134 @@
+{
+    "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": "platform1domain.com",
+                                "dsaparams": [1]
+                            },
+                            {
+                                "domain": "SSP2domain.com",
+                                "dsaparams": [1, 2]
+                            }
+                        ]
+                    }
+                }
+            }
+        }
+    },
+    "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": "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": "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"
+                        }
+                    }
+                }]
+            }]
+        }
+    }
+}
\ No newline at end of file
diff --git a/exchange/exchangetest/dsa-required.json b/exchange/exchangetest/dsa-required.json
new file mode 100644
index 00000000..d25efdc1
--- /dev/null
+++ b/exchange/exchangetest/dsa-required.json
@@ -0,0 +1,219 @@
+{
+    "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"
+                            }
+                        }
+                    }
+                }
+            }],
+            "regs": {
+                "ext": {
+                    "dsa": {
+                        "dsarequired": 2,
+                        "pubrender": 0,
+                        "datatopub": 2,
+                        "transparency": [{
+                                "domain": "platform1domain.com",
+                                "dsaparams": [1]
+                            },
+                            {
+                                "domain": "SSP2domain.com",
+                                "dsaparams": [1, 2]
+                            }
+                        ]
+                    }
+                }
+            }
+        }
+    },
+    "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": 0,
+                                "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": 0,
+                                "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/openrtb_ext/request_wrapper_test.go b/openrtb_ext/request_wrapper_test.go
index 6c318e37..f04a51a4 100644
--- a/openrtb_ext/request_wrapper_test.go
+++ b/openrtb_ext/request_wrapper_test.go
@@ -2049,3 +2049,292 @@ func TestCloneImpExt(t *testing.T) {
 		})
 	}
 }
+
+func TestRebuildRegExt(t *testing.T) {
+	strA := "a"
+	strB := "b"
+
+	tests := []struct {
+		name            string
+		request         openrtb2.BidRequest
+		regExt          RegExt
+		expectedRequest openrtb2.BidRequest
+	}{
+		{
+			name:            "req_regs_nil_-_not_dirty_-_no_change",
+			request:         openrtb2.BidRequest{},
+			regExt:          RegExt{},
+			expectedRequest: openrtb2.BidRequest{},
+		},
+		{
+			name:    "req_regs_nil_-_dirty_and_different_-_change",
+			request: openrtb2.BidRequest{},
+			regExt:  RegExt{dsa: &ExtRegsDSA{Required: 1}, dsaDirty: true, gdpr: ptrutil.ToPtr[int8](1), gdprDirty: true, usPrivacy: strA, usPrivacyDirty: true},
+			expectedRequest: openrtb2.BidRequest{
+				Regs: &openrtb2.Regs{
+					Ext: json.RawMessage(`{"dsa":{"dsarequired":1},"gdpr":1,"us_privacy":"a"}`),
+				},
+			},
+		},
+		{
+			name:            "req_regs_ext_nil_-_not_dirty_-_no_change",
+			request:         openrtb2.BidRequest{Regs: &openrtb2.Regs{}},
+			regExt:          RegExt{},
+			expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{}},
+		},
+		{
+			name:    "req_regs_ext_nil_-_dirty_and_different_-_change",
+			request: openrtb2.BidRequest{Regs: &openrtb2.Regs{}},
+			regExt:  RegExt{dsa: &ExtRegsDSA{Required: 1}, dsaDirty: true, gdpr: ptrutil.ToPtr[int8](1), gdprDirty: true, usPrivacy: strA, usPrivacyDirty: true},
+			expectedRequest: openrtb2.BidRequest{
+				Regs: &openrtb2.Regs{
+					Ext: json.RawMessage(`{"dsa":{"dsarequired":1},"gdpr":1,"us_privacy":"a"}`),
+				},
+			},
+		},
+		{
+			name:            "req_regs_dsa_populated_-_not_dirty_-_no_change",
+			request:         openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"dsa":{"dsarequired":1}}`)}},
+			regExt:          RegExt{},
+			expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"dsa":{"dsarequired":1}}`)}},
+		},
+		{
+			name:            "req_regs_dsa_populated_-_dirty_and_different-_change",
+			request:         openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"dsa":{"dsarequired":1}}`)}},
+			regExt:          RegExt{dsa: &ExtRegsDSA{Required: 2}, dsaDirty: true},
+			expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"dsa":{"dsarequired":2}}`)}},
+		},
+		{
+			name:            "req_regs_dsa_populated_-_dirty_and_same_-_no_change",
+			request:         openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"dsa":{"dsarequired":1}}`)}},
+			regExt:          RegExt{dsa: &ExtRegsDSA{Required: 1}, dsaDirty: true},
+			expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"dsa":{"dsarequired":1}}`)}},
+		},
+		{
+			name:            "req_regs_dsa_populated_-_dirty_and_nil_-_cleared",
+			request:         openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{}`)}},
+			regExt:          RegExt{dsa: nil, dsaDirty: true},
+			expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{}},
+		},
+		{
+			name:            "req_regs_gdpr_populated_-_not_dirty_-_no_change",
+			request:         openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr":1}`)}},
+			regExt:          RegExt{},
+			expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr":1}`)}},
+		},
+		{
+			name:            "req_regs_gdpr_populated_-_dirty_and_different-_change",
+			request:         openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr":1}`)}},
+			regExt:          RegExt{gdpr: ptrutil.ToPtr[int8](0), gdprDirty: true},
+			expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr":0}`)}},
+		},
+		{
+			name:            "req_regs_gdpr_populated_-_dirty_and_same_-_no_change",
+			request:         openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr":1}`)}},
+			regExt:          RegExt{gdpr: ptrutil.ToPtr[int8](1), gdprDirty: true},
+			expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr":1}`)}},
+		},
+		{
+			name:            "req_regs_gdpr_populated_-_dirty_and_nil_-_cleared",
+			request:         openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{}`)}},
+			regExt:          RegExt{gdpr: nil, gdprDirty: true},
+			expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{}},
+		},
+		{
+			name:            "req_regs_usprivacy_populated_-_not_dirty_-_no_change",
+			request:         openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"a"}`)}},
+			regExt:          RegExt{},
+			expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"a"}`)}},
+		},
+		{
+			name:            "req_regs_usprivacy_populated_-_dirty_and_different-_change",
+			request:         openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"a"}`)}},
+			regExt:          RegExt{usPrivacy: strB, usPrivacyDirty: true},
+			expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"b"}`)}},
+		},
+		{
+			name:            "req_regs_usprivacy_populated_-_dirty_and_same_-_no_change",
+			request:         openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"a"}`)}},
+			regExt:          RegExt{usPrivacy: strA, usPrivacyDirty: true},
+			expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"a"}`)}},
+		},
+		{
+			name:            "req_regs_usprivacy_populated_-_dirty_and_nil_-_cleared",
+			request:         openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"a"}`)}},
+			regExt:          RegExt{usPrivacy: "", usPrivacyDirty: true},
+			expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{}},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			tt.regExt.ext = make(map[string]json.RawMessage)
+
+			w := RequestWrapper{BidRequest: &tt.request, regExt: &tt.regExt}
+			w.RebuildRequest()
+			assert.Equal(t, tt.expectedRequest, *w.BidRequest)
+		})
+	}
+}
+
+func TestRegExtUnmarshal(t *testing.T) {
+	tests := []struct {
+		name            string
+		regExt          *RegExt
+		extJson         json.RawMessage
+		expectDSA       *ExtRegsDSA
+		expectGDPR      *int8
+		expectUSPrivacy string
+		expectError     bool
+	}{
+		{
+			name: "RegExt.ext_not_empty_and_not_dirtyr",
+			regExt: &RegExt{
+				ext: map[string]json.RawMessage{"dsa": json.RawMessage(`{}`)},
+			},
+			extJson:     json.RawMessage{},
+			expectError: false,
+		},
+		{
+			name:        "RegExt.ext_empty_and_dirty",
+			regExt:      &RegExt{extDirty: true},
+			extJson:     json.RawMessage(`{"dsa":{"dsarequired":1}}`),
+			expectError: false,
+		},
+		{
+			name: "nothing_to_unmarshal",
+			regExt: &RegExt{
+				ext: map[string]json.RawMessage{},
+			},
+			extJson:     json.RawMessage{},
+			expectError: false,
+		},
+		// DSA
+		{
+			name:    "valid_dsa_json",
+			regExt:  &RegExt{},
+			extJson: json.RawMessage(`{"dsa":{"dsarequired":1}}`),
+			expectDSA: &ExtRegsDSA{
+				Required: 1,
+			},
+			expectError: false,
+		},
+		{
+			name:    "malformed_dsa_json",
+			regExt:  &RegExt{},
+			extJson: json.RawMessage(`{"dsa":{"dsarequired":""}}`),
+			expectDSA: &ExtRegsDSA{
+				Required: 0,
+			},
+			expectError: true,
+		},
+		// GDPR
+		{
+			name:        "valid_gdpr_json",
+			regExt:      &RegExt{},
+			extJson:     json.RawMessage(`{"gdpr":1}`),
+			expectGDPR:  ptrutil.ToPtr[int8](1),
+			expectError: false,
+		},
+		{
+			name:        "malformed_gdpr_json",
+			regExt:      &RegExt{},
+			extJson:     json.RawMessage(`{"gdpr":""}`),
+			expectGDPR:  ptrutil.ToPtr[int8](0),
+			expectError: true,
+		},
+		// us_privacy
+		{
+			name:            "valid_usprivacy_json",
+			regExt:          &RegExt{},
+			extJson:         json.RawMessage(`{"us_privacy":"consent"}`),
+			expectUSPrivacy: "consent",
+			expectError:     false,
+		},
+		{
+			name:        "malformed_usprivacy_json",
+			regExt:      &RegExt{},
+			extJson:     json.RawMessage(`{"us_privacy":1}`),
+			expectError: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.regExt.unmarshal(tt.extJson)
+			if tt.expectError {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+			}
+			assert.Equal(t, tt.expectDSA, tt.regExt.dsa)
+			assert.Equal(t, tt.expectGDPR, tt.regExt.gdpr)
+			assert.Equal(t, tt.expectUSPrivacy, tt.regExt.usPrivacy)
+		})
+	}
+}
+
+func TestRegExtGetExtSetExt(t *testing.T) {
+	regExt := &RegExt{}
+	regExtJSON := regExt.GetExt()
+	assert.Equal(t, regExtJSON, map[string]json.RawMessage{})
+	assert.False(t, regExt.Dirty())
+
+	rawJSON := map[string]json.RawMessage{
+		"dsa":       json.RawMessage(`{}`),
+		"gdpr":      json.RawMessage(`1`),
+		"usprivacy": json.RawMessage(`"consent"`),
+	}
+	regExt.SetExt(rawJSON)
+	assert.True(t, regExt.Dirty())
+
+	regExtJSON = regExt.GetExt()
+	assert.Equal(t, regExtJSON, rawJSON)
+	assert.NotSame(t, regExtJSON, rawJSON)
+}
+
+func TestRegExtGetDSASetDSA(t *testing.T) {
+	regExt := &RegExt{}
+	regExtDSA := regExt.GetDSA()
+	assert.Nil(t, regExtDSA)
+	assert.False(t, regExt.Dirty())
+
+	dsa := &ExtRegsDSA{
+		Required: 2,
+	}
+	regExt.SetDSA(dsa)
+	assert.True(t, regExt.Dirty())
+
+	regExtDSA = regExt.GetDSA()
+	assert.Equal(t, regExtDSA, dsa)
+	assert.NotSame(t, regExtDSA, dsa)
+}
+
+func TestRegExtGetUSPrivacySetUSPrivacy(t *testing.T) {
+	regExt := &RegExt{}
+	regExtUSPrivacy := regExt.GetUSPrivacy()
+	assert.Equal(t, regExtUSPrivacy, "")
+	assert.False(t, regExt.Dirty())
+
+	usprivacy := "consent"
+	regExt.SetUSPrivacy(usprivacy)
+	assert.True(t, regExt.Dirty())
+
+	regExtUSPrivacy = regExt.GetUSPrivacy()
+	assert.Equal(t, regExtUSPrivacy, usprivacy)
+	assert.NotSame(t, regExtUSPrivacy, usprivacy)
+}
+
+func TestRegExtGetGDPRSetGDPR(t *testing.T) {
+	regExt := &RegExt{}
+	regExtGDPR := regExt.GetGDPR()
+	assert.Nil(t, regExtGDPR)
+	assert.False(t, regExt.Dirty())
+
+	gdpr := ptrutil.ToPtr[int8](1)
+	regExt.SetGDPR(gdpr)
+	assert.True(t, regExt.Dirty())
+
+	regExtGDPR = regExt.GetGDPR()
+	assert.Equal(t, regExtGDPR, gdpr)
+	assert.NotSame(t, regExtGDPR, gdpr)
+}