Repository: prebid/prebid-server
Pull request #3487 (2024-02-12 13:55:50)

DSA: Remove bids missing DSA object when required
Implements #3367 

For requests, pass through behavior was already supported but this adds an exchange test to verify.
For bid responses, validation is added and bids are discarded if DSA objects are required in bids.


Patch:
diff --git a/.github/workflows/adapter-code-coverage.yml b/.github/workflows/adapter-code-coverage.yml
index b5fbd557..4d5751c2 100644
--- a/.github/workflows/adapter-code-coverage.yml
+++ b/.github/workflows/adapter-code-coverage.yml
@@ -10,12 +10,12 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Install Go
-        uses: actions/setup-go@v4
+        uses: actions/setup-go@v5
         with:
           go-version: 1.20.5
 
       - name: Checkout pull request branch
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
           ref: ${{github.event.pull_request.head.ref}}
@@ -23,7 +23,7 @@ jobs:
 
       - name: Get adapter directories
         id: get_directories
-        uses: actions/github-script@v6
+        uses: actions/github-script@v7
         with:
           result-encoding: string
           script: |
@@ -68,7 +68,7 @@ jobs:
           rm -f -r ./*
 
       - name: Checkout coverage-preview branch
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
           ref: coverage-preview
@@ -94,7 +94,7 @@ jobs:
 
       - name: Add coverage summary to pull request
         if: steps.run_coverage.outputs.coverage_dir != '' && steps.commit_coverage.outputs.remote_coverage_preview_dir != ''
-        uses: actions/github-script@v6
+        uses: actions/github-script@v7
         with:
           script: |
             const utils = require('./.github/workflows/helpers/pull-request-utils.js')
diff --git a/.github/workflows/helpers/pull-request-utils.js b/.github/workflows/helpers/pull-request-utils.js
index 941ae5b6..73c80396 100644
--- a/.github/workflows/helpers/pull-request-utils.js
+++ b/.github/workflows/helpers/pull-request-utils.js
@@ -1,7 +1,9 @@
 const synchronizeEvent = "synchronize",
   openedEvent = "opened",
   completedStatus = "completed",
-  resultSize = 100
+  resultSize = 100,
+  adminPermission = "admin",
+  writePermission = "write"
 
 class diffHelper {
   constructor(input) {
@@ -407,8 +409,31 @@ class coverageHelper {
   }
 }
 
+class userHelper {
+  constructor(input) {
+    this.owner = input.context.repo.owner
+    this.repo = input.context.repo.repo
+    this.github = input.github
+    this.user = input.user
+  }
+
+  /*
+    Checks if the user has write permissions for the repository
+    @returns {boolean} - returns true if the user has write permissions, otherwise false
+  */
+  async hasWritePermissions() {
+    const { data } = await this.github.rest.repos.getCollaboratorPermissionLevel({
+      owner: this.owner,
+      repo: this.repo,
+      username: this.user,
+    })
+    return data.permission === writePermission || data.permission === adminPermission
+  }
+}
+
 module.exports = {
   diffHelper: (input) => new diffHelper(input),
   semgrepHelper: (input) => new semgrepHelper(input),
   coverageHelper: (input) => new coverageHelper(input),
+  userHelper: (input) => new userHelper(input),
 }
diff --git a/.github/workflows/issue_prioritization.yml b/.github/workflows/issue_prioritization.yml
index 3843507b..ec58073d 100644
--- a/.github/workflows/issue_prioritization.yml
+++ b/.github/workflows/issue_prioritization.yml
@@ -10,7 +10,7 @@ jobs:
     steps:
       - name: Generate token
         id: generate_token
-        uses: tibdex/github-app-token@36464acb844fc53b9b8b2401da68844f6b05ebb0
+        uses: tibdex/github-app-token@v2.1.0
         with:
           app_id: ${{ secrets.PBS_PROJECT_APP_ID }}
           private_key: ${{ secrets.PBS_PROJECT_APP_PEM }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index f1d2b10c..d3559732 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -25,13 +25,25 @@ jobs:
     permissions:
       contents: read
     steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+          repository: ${{ github.repository }}
+          ref: master
       - name: Check user permission
-        uses: actions-cool/check-user-permission@v2.2.0
+        uses: actions/github-script@v7
         id: check
         with:
-          require: 'write'
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          result-encoding: string
+          script: |
+            const utils = require('./.github/workflows/helpers/pull-request-utils.js')
+            const helper = utils.userHelper({github, context, user: '${{ github.actor }}'})
+            const hasPermission = await helper.hasWritePermissions()
+            return hasPermission
     outputs:
-      hasWritePermission: ${{ steps.check.outputs.require-result }}
+      hasWritePermission: ${{ steps.check.outputs.result }}
 
   build-master:
     name: Build master
@@ -40,7 +52,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
-        uses: actions/checkout@master
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
           repository: ${{ github.repository }}
@@ -52,13 +64,12 @@ jobs:
   publish-tag:
     name: Publish tag
     needs: build-master
-    if: contains(needs.check-permission.outputs.hasWritePermission, 'true')
     permissions:
       contents: write
     runs-on: ubuntu-latest
     steps:
       - name: Checkout Prebid Server
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
       - name: Create & publish tag
@@ -111,7 +122,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout Prebid Server
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
       - name: Build image
diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml
index baa10b93..09a0fd56 100644
--- a/.github/workflows/security.yml
+++ b/.github/workflows/security.yml
@@ -14,7 +14,7 @@ jobs:
     runs-on: ubuntu-20.04
     steps:
       - name: Checkout Code
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           # Resolves to empty string for push events and falls back to HEAD.
           ref: ${{ github.event.pull_request.head.sha }}
@@ -29,6 +29,6 @@ jobs:
           severity: 'CRITICAL,HIGH'
           
       - name: Upload Results To GitHub Security Tab
-        uses: github/codeql-action/upload-sarif@v2
+        uses: github/codeql-action/upload-sarif@v3
         with:
           sarif_file: 'trivy-results.sarif'
\ No newline at end of file
diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml
index 565bb9b8..b641d566 100644
--- a/.github/workflows/semgrep.yml
+++ b/.github/workflows/semgrep.yml
@@ -9,7 +9,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout repo
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
           ref: ${{github.event.pull_request.head.ref}}
@@ -17,7 +17,7 @@ jobs:
 
       - name: Calculate diff
         id: calculate_diff
-        uses: actions/github-script@v6
+        uses: actions/github-script@v7
         with:
           result-encoding: string
           script: |
@@ -52,7 +52,7 @@ jobs:
       - name: Add pull request comment
         id: add_pull_request_comment
         if: contains(steps.should_run_semgrep.outputs.hasChanges, 'true')
-        uses: actions/github-script@v6.4.1
+        uses: actions/github-script@v7
         with:
           github-token: ${{ secrets.GITHUB_TOKEN }}
           result-encoding: string
diff --git a/.github/workflows/validate-merge.yml b/.github/workflows/validate-merge.yml
index 07f1baca..c44b4002 100644
--- a/.github/workflows/validate-merge.yml
+++ b/.github/workflows/validate-merge.yml
@@ -10,12 +10,12 @@ jobs:
     
     steps:
     - name: Install Go
-      uses: actions/setup-go@v4
+      uses: actions/setup-go@v5
       with:
         go-version: 1.20.5
 
     - name: Checkout Merged Branch
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
 
     - name: Validate
       run: |
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index 9047e1f4..a3914c3c 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -16,12 +16,12 @@ jobs:
     
     steps:
     - name: Install Go
-      uses: actions/setup-go@v4
+      uses: actions/setup-go@v5
       with:
         go-version: ${{ matrix.go-version }}
 
     - name: Checkout Code
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
       with:
         # Resolves to empty string for push events and falls back to HEAD.
         ref: ${{ github.event.pull_request.head.sha }}
diff --git a/adapters/bidder.go b/adapters/bidder.go
index 2bbcb657..5f486771 100644
--- a/adapters/bidder.go
+++ b/adapters/bidder.go
@@ -45,7 +45,7 @@ type Bidder interface {
 type TimeoutBidder interface {
 	Bidder
 
-	// MakeTimeoutNotice functions much the same as MakeRequests, except it is fed the bidder request that timed out,
+	// MakeTimeoutNotification functions much the same as MakeRequests, except it is fed the bidder request that timed out,
 	// and expects that only one notification "request" will be generated. A use case for multiple timeout notifications
 	// has not been anticipated.
 	//
diff --git a/adapters/bwx/bwx.go b/adapters/bwx/bwx.go
new file mode 100644
index 00000000..bab1fb7d
--- /dev/null
+++ b/adapters/bwx/bwx.go
@@ -0,0 +1,158 @@
+package bwx
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"text/template"
+
+	"github.com/prebid/openrtb/v20/openrtb2"
+	"github.com/prebid/prebid-server/v2/adapters"
+	"github.com/prebid/prebid-server/v2/config"
+	"github.com/prebid/prebid-server/v2/errortypes"
+	"github.com/prebid/prebid-server/v2/macros"
+	"github.com/prebid/prebid-server/v2/openrtb_ext"
+)
+
+type bidType struct {
+	Type string `json:"type"`
+}
+
+type bidExt struct {
+	Prebid bidType `json:"prebid"`
+}
+
+type adapter struct {
+	endpoint *template.Template
+}
+
+func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
+	tmpl, err := template.New("endpointTemplate").Parse(config.Endpoint)
+	if err != nil {
+		return nil, fmt.Errorf("unable to parse endpoint URL template: %v", err)
+	}
+
+	bidder := &adapter{
+		endpoint: tmpl,
+	}
+
+	return bidder, nil
+}
+
+func (a *adapter) buildEndpointFromRequest(imp *openrtb2.Imp) (string, error) {
+	var impExt adapters.ExtImpBidder
+	if err := json.Unmarshal(imp.Ext, &impExt); err != nil {
+		return "", &errortypes.BadInput{
+			Message: fmt.Sprintf("Failed to deserialize bidder impression extension: %v", err),
+		}
+	}
+
+	var boldwinxExt openrtb_ext.ExtBWX
+	if err := json.Unmarshal(impExt.Bidder, &boldwinxExt); err != nil {
+		return "", &errortypes.BadInput{
+			Message: fmt.Sprintf("Failed to deserialize BoldwinX extension: %v", err),
+		}
+	}
+
+	endpointParams := macros.EndpointTemplateParams{
+		Host:     boldwinxExt.Env,
+		SourceId: boldwinxExt.Pid,
+	}
+
+	return macros.ResolveMacros(a.endpoint, endpointParams)
+}
+
+func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	var requests []*adapters.RequestData
+	var errs []error
+
+	headers := http.Header{}
+	headers.Add("Content-Type", "application/json;charset=utf-8")
+	headers.Add("Accept", "application/json")
+
+	requestCopy := *request
+	for _, imp := range request.Imp {
+		requestCopy.Imp = []openrtb2.Imp{imp}
+
+		endpoint, err := a.buildEndpointFromRequest(&imp)
+		if err != nil {
+			errs = append(errs, err)
+			continue
+		}
+
+		requestJSON, err := json.Marshal(requestCopy)
+		if err != nil {
+			errs = append(errs, err)
+			continue
+		}
+
+		request := &adapters.RequestData{
+			Method:  http.MethodPost,
+			Body:    requestJSON,
+			Uri:     endpoint,
+			Headers: headers,
+		}
+
+		requests = append(requests, request)
+	}
+
+	return requests, errs
+}
+
+func (a *adapter) MakeBids(openRTBRequest *openrtb2.BidRequest, requestToBidder *adapters.RequestData, bidderRawResponse *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	if adapters.IsResponseStatusCodeNoContent(bidderRawResponse) {
+		return nil, nil
+	}
+
+	if err := adapters.CheckResponseStatusCodeForErrors(bidderRawResponse); err != nil {
+		return nil, []error{err}
+	}
+
+	var bidResp openrtb2.BidResponse
+	if err := json.Unmarshal(bidderRawResponse.Body, &bidResp); err != nil {
+		return nil, []error{err}
+	}
+
+	if len(bidResp.SeatBid) == 0 {
+		return nil, []error{&errortypes.BadServerResponse{
+			Message: "Array SeatBid cannot be empty",
+		}}
+	}
+
+	return prepareBidResponse(bidResp.SeatBid)
+}
+
+func prepareBidResponse(seats []openrtb2.SeatBid) (*adapters.BidderResponse, []error) {
+	var errs []error
+	bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(seats))
+
+	for _, seatBid := range seats {
+		for idx, bid := range seatBid.Bid {
+			bidType, err := getMediaTypeForBid(bid)
+			if err != nil {
+				errs = append(errs, err)
+				continue
+			}
+
+			bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
+				Bid:     &seatBid.Bid[idx],
+				BidType: bidType,
+			})
+		}
+	}
+
+	return bidResponse, errs
+}
+
+func getMediaTypeForBid(bid openrtb2.Bid) (openrtb_ext.BidType, error) {
+	switch bid.MType {
+	case openrtb2.MarkupBanner:
+		return openrtb_ext.BidTypeBanner, nil
+	case openrtb2.MarkupVideo:
+		return openrtb_ext.BidTypeVideo, nil
+	case openrtb2.MarkupNative:
+		return openrtb_ext.BidTypeNative, nil
+	default:
+		return "", fmt.Errorf("failed to parse bid mtype (%d) for impression id \"%s\"", bid.MType, bid.ImpID)
+	}
+}
diff --git a/adapters/improvedigital/improvedigital.go b/adapters/improvedigital/improvedigital.go
index c00acfa7..3fd514be 100644
--- a/adapters/improvedigital/improvedigital.go
+++ b/adapters/improvedigital/improvedigital.go
@@ -287,13 +287,17 @@ func (a *ImprovedigitalAdapter) getAdditionalConsentProvidersUserExt(request ope
 	}
 	// End validating additional consent
 
-	// Check if string contain ~, then substring after ~ to end of string
-	consentStr := string(cpMapValue)
-	var tildaPosition int
-	if tildaPosition = strings.Index(consentStr, "~"); tildaPosition == -1 {
+	// Trim enclosing quotes after casting json.RawMessage to string
+	consentStr := strings.Trim((string)(cpMapValue), "\"")
+	// Split by ~ and take only the second string (if exists) as the consented providers spec
+	var consentStrParts = strings.Split(consentStr, "~")
+	if len(consentStrParts) < 2 {
+		return nil, nil
+	}
+	cpStr = strings.TrimSpace(consentStrParts[1])
+	if len(cpStr) == 0 {
 		return nil, nil
 	}
-	cpStr = consentStr[tildaPosition+1 : len(consentStr)-1]
 
 	// Prepare consent providers string
 	cpStr = fmt.Sprintf("[%s]", strings.Replace(cpStr, ".", ",", -1))
diff --git a/adapters/yieldlab/types.go b/adapters/yieldlab/types.go
index 90612700..9d3bc6a2 100644
--- a/adapters/yieldlab/types.go
+++ b/adapters/yieldlab/types.go
@@ -1,18 +1,60 @@
 package yieldlab
 
 import (
+	"github.com/prebid/prebid-server/v2/openrtb_ext"
 	"strconv"
 	"time"
 )
 
 type bidResponse struct {
-	ID         uint64 `json:"id"`
-	Price      uint   `json:"price"`
-	Advertiser string `json:"advertiser"`
-	Adsize     string `json:"adsize"`
-	Pid        uint64 `json:"pid"`
-	Did        uint64 `json:"did"`
-	Pvid       string `json:"pvid"`
+	ID         uint64       `json:"id"`
+	Price      uint         `json:"price"`
+	Advertiser string       `json:"advertiser"`
+	Adsize     string       `json:"adsize"`
+	Pid        uint64       `json:"pid"`
+	Did        uint64       `json:"did"`
+	Pvid       string       `json:"pvid"`
+	DSA        *dsaResponse `json:"dsa,omitempty"`
+}
+
+// dsaResponse defines Digital Service Act (DSA) parameters from Yieldlab yieldprobe response.
+type dsaResponse struct {
+	Behalf       string            `json:"behalf,omitempty"`
+	Paid         string            `json:"paid,omitempty"`
+	Adrender     *int              `json:"adrender,omitempty"`
+	Transparency []dsaTransparency `json:"transparency,omitempty"`
+}
+
+// openRTBExtRegsWithDSA defines the contract for bidrequest.regs.ext with the missing DSA property.
+//
+// The openrtb_ext.ExtRegs needs to be extended on yieldlab adapter level until DSA has been implemented
+// by the prebid server team (https://github.com/prebid/prebid-server/issues/3424).
+type openRTBExtRegsWithDSA struct {
+	openrtb_ext.ExtRegs
+	DSA *dsaRequest `json:"dsa,omitempty"`
+}
+
+// responseExtWithDSA defines seatbid.bid.ext with the DSA object.
+type responseExtWithDSA struct {
+	DSA dsaResponse `json:"dsa"`
+}
+
+// dsaRequest defines Digital Service Act (DSA) parameter
+// as specified by the OpenRTB 2.X DSA Transparency community extension.
+//
+// Should rather come from openrtb_ext package but will be defined here until DSA has been
+// implemented by the prebid server team (https://github.com/prebid/prebid-server/issues/3424).
+type dsaRequest struct {
+	Required     *int              `json:"dsarequired"`
+	PubRender    *int              `json:"pubrender"`
+	DataToPub    *int              `json:"datatopub"`
+	Transparency []dsaTransparency `json:"transparency"`
+}
+
+// dsaTransparency Digital Service Act (DSA) transparency object
+type dsaTransparency struct {
+	Domain string `json:"domain,omitempty"`
+	Params []int  `json:"dsaparams,omitempty"`
 }
 
 type cacheBuster func() string
diff --git a/adapters/yieldlab/yieldlab.go b/adapters/yieldlab/yieldlab.go
index 744a45ae..ed7c283e 100644
--- a/adapters/yieldlab/yieldlab.go
+++ b/adapters/yieldlab/yieldlab.go
@@ -96,11 +96,94 @@ func (a *YieldlabAdapter) makeEndpointURL(req *openrtb2.BidRequest, params *open
 		}
 	}
 
+	dsa, err := getDSA(req)
+	if err != nil {
+		return "", err
+	}
+	if dsa != nil {
+		if dsa.Required != nil {
+			q.Set("dsarequired", strconv.Itoa(*dsa.Required))
+		}
+		if dsa.PubRender != nil {
+			q.Set("dsapubrender", strconv.Itoa(*dsa.PubRender))
+		}
+		if dsa.DataToPub != nil {
+			q.Set("dsadatatopub", strconv.Itoa(*dsa.DataToPub))
+		}
+		if len(dsa.Transparency) != 0 {
+			transparencyParam := makeDSATransparencyURLParam(dsa.Transparency)
+			if len(transparencyParam) != 0 {
+				q.Set("dsatransparency", transparencyParam)
+			}
+		}
+	}
+
 	uri.RawQuery = q.Encode()
 
 	return uri.String(), nil
 }
 
+// getDSA extracts the Digital Service Act (DSA) properties from the request.
+func getDSA(req *openrtb2.BidRequest) (*dsaRequest, error) {
+	if req.Regs == nil || req.Regs.Ext == nil {
+		return nil, nil
+	}
+
+	var extRegs openRTBExtRegsWithDSA
+	err := json.Unmarshal(req.Regs.Ext, &extRegs)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse Regs.Ext object from Yieldlab response: %v", err)
+	}
+
+	return extRegs.DSA, nil
+}
+
+// makeDSATransparencyURLParam creates the transparency url parameter
+// as specified by the OpenRTB 2.X DSA Transparency community extension.
+//
+// Example result: platform1domain.com~1~~SSP2domain.com~1_2
+func makeDSATransparencyURLParam(transparencyObjects []dsaTransparency) string {
+	valueSeparator, itemSeparator, objectSeparator := "_", "~", "~~"
+
+	var b strings.Builder
+
+	concatParams := func(params []int) {
+		b.WriteString(strconv.Itoa(params[0]))
+		for _, param := range params[1:] {
+			b.WriteString(valueSeparator)
+			b.WriteString(strconv.Itoa(param))
+		}
+	}
+
+	concatTransparency := func(object dsaTransparency) {
+		if len(object.Domain) == 0 {
+			return
+		}
+
+		b.WriteString(object.Domain)
+		if len(object.Params) != 0 {
+			b.WriteString(itemSeparator)
+			concatParams(object.Params)
+		}
+	}
+
+	concatTransparencies := func(objects []dsaTransparency) {
+		if len(objects) == 0 {
+			return
+		}
+
+		concatTransparency(objects[0])
+		for _, obj := range objects[1:] {
+			b.WriteString(objectSeparator)
+			concatTransparency(obj)
+		}
+	}
+
+	concatTransparencies(transparencyObjects)
+
+	return b.String()
+}
+
 func (a *YieldlabAdapter) makeFormats(req *openrtb2.BidRequest) (bool, string) {
 	var formats []string
 	const sizesSeparator, adslotSizesSeparator = "|", ","
@@ -253,6 +336,7 @@ func (a *YieldlabAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externa
 		}
 	}
 
+	var bidErrors []error
 	for _, bid := range bids {
 		width, height, err := splitSize(bid.Adsize)
 		if err != nil {
@@ -269,7 +353,13 @@ func (a *YieldlabAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externa
 		if imp, exists := adslotToImpMap[strconv.FormatUint(bid.ID, 10)]; !exists {
 			continue
 		} else {
-			var bidType openrtb_ext.BidType
+			extJson, err := makeResponseExt(bid)
+			if err != nil {
+				bidErrors = append(bidErrors, err)
+				// skip as bids with missing ext.dsa will be discarded anyway
+				continue
+			}
+
 			responseBid := &openrtb2.Bid{
 				ID:     strconv.FormatUint(bid.ID, 10),
 				Price:  float64(bid.Price) / 100,
@@ -278,8 +368,10 @@ func (a *YieldlabAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externa
 				DealID: strconv.FormatUint(bid.Pid, 10),
 				W:      int64(width),
 				H:      int64(height),
+				Ext:    extJson,
 			}
 
+			var bidType openrtb_ext.BidType
 			if imp.Video != nil {
 				bidType = openrtb_ext.BidTypeVideo
 				responseBid.NURL = a.makeAdSourceURL(internalRequest, req, bid)
@@ -299,7 +391,18 @@ func (a *YieldlabAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externa
 		}
 	}
 
-	return bidderResponse, nil
+	return bidderResponse, bidErrors
+}
+
+func makeResponseExt(bid *bidResponse) (json.RawMessage, error) {
+	if bid.DSA != nil {
+		extJson, err := json.Marshal(responseExtWithDSA{*bid.DSA})
+		if err != nil {
+			return nil, fmt.Errorf("failed to make JSON for seatbid.bid.ext for adslotID %v. This is most likely a programming issue", bid.ID)
+		}
+		return extJson, nil
+	}
+	return nil, nil
 }
 
 func (a *YieldlabAdapter) findBidReq(adslotID uint64, params []*openrtb_ext.ExtImpYieldlab) *openrtb_ext.ExtImpYieldlab {
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/exchange/adapter_builders.go b/exchange/adapter_builders.go
index 639ea93a..e188d293 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -59,6 +59,7 @@ import (
 	"github.com/prebid/prebid-server/v2/adapters/bmtm"
 	"github.com/prebid/prebid-server/v2/adapters/boldwin"
 	"github.com/prebid/prebid-server/v2/adapters/brave"
+	"github.com/prebid/prebid-server/v2/adapters/bwx"
 	cadentaperturemx "github.com/prebid/prebid-server/v2/adapters/cadent_aperture_mx"
 	"github.com/prebid/prebid-server/v2/adapters/ccx"
 	"github.com/prebid/prebid-server/v2/adapters/coinzilla"
@@ -260,6 +261,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderBmtm:              bmtm.Builder,
 		openrtb_ext.BidderBoldwin:           boldwin.Builder,
 		openrtb_ext.BidderBrave:             brave.Builder,
+		openrtb_ext.BidderBWX:               bwx.Builder,
 		openrtb_ext.BidderCadentApertureMX:  cadentaperturemx.Builder,
 		openrtb_ext.BidderCcx:               ccx.Builder,
 		openrtb_ext.BidderCoinzilla:         coinzilla.Builder,
diff --git a/exchange/auction.go b/exchange/auction.go
index c02e9141..94f0121d 100644
--- a/exchange/auction.go
+++ b/exchange/auction.go
@@ -108,7 +108,7 @@ func (d *DebugLog) PutDebugLogError(cache prebid_cache_client.Client, timeout in
 
 func newAuction(seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, numImps int, preferDeals bool) *auction {
 	winningBids := make(map[string]*entities.PbsOrtbBid, numImps)
-	winningBidsByBidder := make(map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid, numImps)
+	allBidsByBidder := make(map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid, numImps)
 
 	for bidderName, seatBid := range seatBids {
 		if seatBid != nil {
@@ -118,10 +118,10 @@ func newAuction(seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, nu
 					winningBids[bid.Bid.ImpID] = bid
 				}
 
-				if bidMap, ok := winningBidsByBidder[bid.Bid.ImpID]; ok {
+				if bidMap, ok := allBidsByBidder[bid.Bid.ImpID]; ok {
 					bidMap[bidderName] = append(bidMap[bidderName], bid)
 				} else {
-					winningBidsByBidder[bid.Bid.ImpID] = map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+					allBidsByBidder[bid.Bid.ImpID] = map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 						bidderName: {bid},
 					}
 				}
@@ -130,8 +130,8 @@ func newAuction(seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, nu
 	}
 
 	return &auction{
-		winningBids:         winningBids,
-		winningBidsByBidder: winningBidsByBidder,
+		winningBids:     winningBids,
+		allBidsByBidder: allBidsByBidder,
 	}
 }
 
@@ -151,7 +151,7 @@ func isNewWinningBid(bid, wbid *openrtb2.Bid, preferDeals bool) bool {
 func (a *auction) validateAndUpdateMultiBid(adapterBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, preferDeals bool, accountDefaultBidLimit int) {
 	bidsSnipped := false
 	// sort bids for multibid targeting
-	for _, topBidsPerBidder := range a.winningBidsByBidder {
+	for _, topBidsPerBidder := range a.allBidsByBidder {
 		for bidder, topBids := range topBidsPerBidder {
 			sort.Slice(topBids, func(i, j int) bool {
 				return isNewWinningBid(topBids[i].Bid, topBids[j].Bid, preferDeals)
@@ -187,7 +187,7 @@ func (a *auction) validateAndUpdateMultiBid(adapterBids map[openrtb_ext.BidderNa
 
 func (a *auction) setRoundedPrices(targetingData targetData) {
 	roundedPrices := make(map[*entities.PbsOrtbBid]string, 5*len(a.winningBids))
-	for _, topBidsPerImp := range a.winningBidsByBidder {
+	for _, topBidsPerImp := range a.allBidsByBidder {
 		for _, topBidsPerBidder := range topBidsPerImp {
 			for _, topBid := range topBidsPerBidder {
 				roundedPrices[topBid] = GetPriceBucket(*topBid.Bid, targetingData)
@@ -225,7 +225,7 @@ func (a *auction) doCache(ctx context.Context, cache prebid_cache_client.Client,
 	for _, imp := range bidRequest.Imp {
 		expByImp[imp.ID] = imp.Exp
 	}
-	for impID, topBidsPerImp := range a.winningBidsByBidder {
+	for impID, topBidsPerImp := range a.allBidsByBidder {
 		for bidderName, topBidsPerBidder := range topBidsPerImp {
 			for _, topBid := range topBidsPerBidder {
 				isOverallWinner := a.winningBids[impID] == topBid
@@ -394,8 +394,8 @@ func defTTL(bidType openrtb_ext.BidType, defaultTTLs *config.DefaultTTLs) (ttl i
 type auction struct {
 	// winningBids is a map from imp.id to the highest overall CPM bid in that imp.
 	winningBids map[string]*entities.PbsOrtbBid
-	// winningBidsByBidder stores the highest bid on each imp by each bidder.
-	winningBidsByBidder map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid
+	// allBidsByBidder is map from ImpID to another map that maps bidderName to all bids from that bidder.
+	allBidsByBidder map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid
 	// roundedPrices stores the price strings rounded for each bid according to the price granularity.
 	roundedPrices map[*entities.PbsOrtbBid]string
 	// cacheIds stores the UUIDs from Prebid Cache for fetching the full bid JSON.
diff --git a/exchange/bidder.go b/exchange/bidder.go
index 96f8ac9b..8b54f984 100644
--- a/exchange/bidder.go
+++ b/exchange/bidder.go
@@ -523,8 +523,6 @@ func (bidder *bidderAdapter) doRequest(ctx context.Context, req *adapters.Reques
 }
 
 func (bidder *bidderAdapter) doRequestImpl(ctx context.Context, req *adapters.RequestData, logger util.LogMsg, bidderRequestStartTime time.Time, tmaxAdjustments *TmaxAdjustmentsPreprocessed) *httpCallInfo {
-	var requestBody []byte
-
 	requestBody, err := getRequestBody(req, bidder.config.EndpointCompression)
 	if err != nil {
 		return &httpCallInfo{
@@ -532,7 +530,7 @@ func (bidder *bidderAdapter) doRequestImpl(ctx context.Context, req *adapters.Re
 			err:     err,
 		}
 	}
-	httpReq, err := http.NewRequest(req.Method, req.Uri, bytes.NewBuffer(requestBody))
+	httpReq, err := http.NewRequest(req.Method, req.Uri, requestBody)
 	if err != nil {
 		return &httpCallInfo{
 			request: req,
@@ -746,18 +744,16 @@ func hasShorterDurationThanTmax(ctx bidderTmaxContext, tmaxAdjustments TmaxAdjus
 	return false
 }
 
-func getRequestBody(req *adapters.RequestData, endpointCompression string) ([]byte, error) {
-	var requestBody []byte
-
+func getRequestBody(req *adapters.RequestData, endpointCompression string) (*bytes.Buffer, error) {
 	switch strings.ToUpper(endpointCompression) {
 	case Gzip:
 		// Compress to GZIP
-		var b bytes.Buffer
+		b := bytes.NewBuffer(make([]byte, 0, len(req.Body)))
 
 		w := gzipWriterPool.Get().(*gzip.Writer)
 		defer gzipWriterPool.Put(w)
 
-		w.Reset(&b)
+		w.Reset(b)
 		_, err := w.Write(req.Body)
 		if err != nil {
 			return nil, err
@@ -766,14 +762,13 @@ func getRequestBody(req *adapters.RequestData, endpointCompression string) ([]by
 		if err != nil {
 			return nil, err
 		}
-		requestBody = b.Bytes()
 
 		// Set Header
 		req.Headers.Set("Content-Encoding", "gzip")
 
-		return requestBody, nil
+		return b, nil
 	default:
-		return req.Body, nil
+		return bytes.NewBuffer(req.Body), nil
 	}
 }
 
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 7c604886..ffd3b2e8 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)
@@ -574,7 +575,7 @@ func applyDealSupport(bidRequest *openrtb2.BidRequest, auc *auction, bidCategory
 	errs := []error{}
 	impDealMap := getDealTiers(bidRequest)
 
-	for impID, topBidsPerImp := range auc.winningBidsByBidder {
+	for impID, topBidsPerImp := range auc.allBidsByBidder {
 		impDeal := impDealMap[impID]
 		for bidder, topBidsPerBidder := range topBidsPerImp {
 			bidderNormalized, bidderFound := openrtb_ext.NormalizeBidderName(bidder.String())
@@ -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.BadServerResponseErrorCode,
+				Message: "bidResponse rejected: DSA object missing when required",
+			}
+			bidResponseExt.Warnings[adapter] = append(bidResponseExt.Warnings[adapter], RequiredError)
+
+			seatNonBids.addBid(bid, int(ResponseRejectedPrivacy), 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..f105d0ad 100644
--- a/exchange/non_bid_reason.go
+++ b/exchange/non_bid_reason.go
@@ -8,6 +8,7 @@ type NonBidReason int
 const (
 	NoBidUnknownError                      NonBidReason = 0   // No Bid - General
 	ResponseRejectedCategoryMappingInvalid NonBidReason = 303 // Response Rejected - Category Mapping Invalid
+	ResponseRejectedPrivacy                NonBidReason = 305
 	ResponseRejectedCreativeSizeNotAllowed NonBidReason = 351 // Response Rejected - Invalid Creative (Size Not Allowed)
 	ResponseRejectedCreativeNotSecure      NonBidReason = 352 // Response Rejected - Invalid Creative (Not Secure)
 )
diff --git a/exchange/targeting.go b/exchange/targeting.go
index 0047ba8b..d5ce7152 100644
--- a/exchange/targeting.go
+++ b/exchange/targeting.go
@@ -26,6 +26,7 @@ type targetData struct {
 	includeCacheVast          bool
 	includeFormat             bool
 	preferDeals               bool
+	alwaysIncludeDeals        bool
 	// cacheHost and cachePath exist to supply cache host and path as targeting parameters
 	cacheHost string
 	cachePath string
@@ -38,7 +39,7 @@ type targetData struct {
 // it's ok if those stay in the auction. For now, this method implements a very naive cache strategy.
 // In the future, we should implement a more clever retry & backoff strategy to balance the success rate & performance.
 func (targData *targetData) setTargeting(auc *auction, isApp bool, categoryMapping map[string]string, truncateTargetAttr *int, multiBidMap map[string]openrtb_ext.ExtMultiBid) {
-	for impId, topBidsPerImp := range auc.winningBidsByBidder {
+	for impId, topBidsPerImp := range auc.allBidsByBidder {
 		overallWinner := auc.winningBids[impId]
 		for originalBidderName, topBidsPerBidder := range topBidsPerImp {
 			targetingBidderCode := originalBidderName
@@ -61,40 +62,42 @@ func (targData *targetData) setTargeting(auc *auction, isApp bool, categoryMappi
 
 				isOverallWinner := overallWinner == topBid
 
+				bidHasDeal := len(topBid.Bid.DealID) > 0
+
 				targets := make(map[string]string, 10)
 				if cpm, ok := auc.roundedPrices[topBid]; ok {
-					targData.addKeys(targets, openrtb_ext.HbpbConstantKey, cpm, targetingBidderCode, isOverallWinner, truncateTargetAttr)
+					targData.addKeys(targets, openrtb_ext.HbpbConstantKey, cpm, targetingBidderCode, isOverallWinner, truncateTargetAttr, bidHasDeal)
 				}
-				targData.addKeys(targets, openrtb_ext.HbBidderConstantKey, string(targetingBidderCode), targetingBidderCode, isOverallWinner, truncateTargetAttr)
+				targData.addKeys(targets, openrtb_ext.HbBidderConstantKey, string(targetingBidderCode), targetingBidderCode, isOverallWinner, truncateTargetAttr, bidHasDeal)
 				if hbSize := makeHbSize(topBid.Bid); hbSize != "" {
-					targData.addKeys(targets, openrtb_ext.HbSizeConstantKey, hbSize, targetingBidderCode, isOverallWinner, truncateTargetAttr)
+					targData.addKeys(targets, openrtb_ext.HbSizeConstantKey, hbSize, targetingBidderCode, isOverallWinner, truncateTargetAttr, bidHasDeal)
 				}
 				if cacheID, ok := auc.cacheIds[topBid.Bid]; ok {
-					targData.addKeys(targets, openrtb_ext.HbCacheKey, cacheID, targetingBidderCode, isOverallWinner, truncateTargetAttr)
+					targData.addKeys(targets, openrtb_ext.HbCacheKey, cacheID, targetingBidderCode, isOverallWinner, truncateTargetAttr, bidHasDeal)
 				}
 				if vastID, ok := auc.vastCacheIds[topBid.Bid]; ok {
-					targData.addKeys(targets, openrtb_ext.HbVastCacheKey, vastID, targetingBidderCode, isOverallWinner, truncateTargetAttr)
+					targData.addKeys(targets, openrtb_ext.HbVastCacheKey, vastID, targetingBidderCode, isOverallWinner, truncateTargetAttr, bidHasDeal)
 				}
 				if targData.includeFormat {
-					targData.addKeys(targets, openrtb_ext.HbFormatKey, string(topBid.BidType), targetingBidderCode, isOverallWinner, truncateTargetAttr)
+					targData.addKeys(targets, openrtb_ext.HbFormatKey, string(topBid.BidType), targetingBidderCode, isOverallWinner, truncateTargetAttr, bidHasDeal)
 				}
 
 				if targData.cacheHost != "" {
-					targData.addKeys(targets, openrtb_ext.HbConstantCacheHostKey, targData.cacheHost, targetingBidderCode, isOverallWinner, truncateTargetAttr)
+					targData.addKeys(targets, openrtb_ext.HbConstantCacheHostKey, targData.cacheHost, targetingBidderCode, isOverallWinner, truncateTargetAttr, bidHasDeal)
 				}
 				if targData.cachePath != "" {
-					targData.addKeys(targets, openrtb_ext.HbConstantCachePathKey, targData.cachePath, targetingBidderCode, isOverallWinner, truncateTargetAttr)
+					targData.addKeys(targets, openrtb_ext.HbConstantCachePathKey, targData.cachePath, targetingBidderCode, isOverallWinner, truncateTargetAttr, bidHasDeal)
 				}
 
-				if deal := topBid.Bid.DealID; len(deal) > 0 {
-					targData.addKeys(targets, openrtb_ext.HbDealIDConstantKey, deal, targetingBidderCode, isOverallWinner, truncateTargetAttr)
+				if bidHasDeal {
+					targData.addKeys(targets, openrtb_ext.HbDealIDConstantKey, topBid.Bid.DealID, targetingBidderCode, isOverallWinner, truncateTargetAttr, bidHasDeal)
 				}
 
 				if isApp {
-					targData.addKeys(targets, openrtb_ext.HbEnvKey, openrtb_ext.HbEnvKeyApp, targetingBidderCode, isOverallWinner, truncateTargetAttr)
+					targData.addKeys(targets, openrtb_ext.HbEnvKey, openrtb_ext.HbEnvKeyApp, targetingBidderCode, isOverallWinner, truncateTargetAttr, bidHasDeal)
 				}
 				if len(categoryMapping) > 0 {
-					targData.addKeys(targets, openrtb_ext.HbCategoryDurationKey, categoryMapping[topBid.Bid.ID], targetingBidderCode, isOverallWinner, truncateTargetAttr)
+					targData.addKeys(targets, openrtb_ext.HbCategoryDurationKey, categoryMapping[topBid.Bid.ID], targetingBidderCode, isOverallWinner, truncateTargetAttr, bidHasDeal)
 				}
 				topBid.BidTargets = targets
 			}
@@ -102,7 +105,7 @@ func (targData *targetData) setTargeting(auc *auction, isApp bool, categoryMappi
 	}
 }
 
-func (targData *targetData) addKeys(keys map[string]string, key openrtb_ext.TargetingKey, value string, bidderName openrtb_ext.BidderName, overallWinner bool, truncateTargetAttr *int) {
+func (targData *targetData) addKeys(keys map[string]string, key openrtb_ext.TargetingKey, value string, bidderName openrtb_ext.BidderName, overallWinner bool, truncateTargetAttr *int, bidHasDeal bool) {
 	var maxLength int
 	if truncateTargetAttr != nil {
 		maxLength = *truncateTargetAttr
@@ -112,7 +115,7 @@ func (targData *targetData) addKeys(keys map[string]string, key openrtb_ext.Targ
 	} else {
 		maxLength = MaxKeyLength
 	}
-	if targData.includeBidderKeys {
+	if targData.includeBidderKeys || (targData.alwaysIncludeDeals && bidHasDeal) {
 		keys[key.BidderKey(bidderName, maxLength)] = value
 	}
 	if targData.includeWinners && overallWinner {
diff --git a/exchange/utils.go b/exchange/utils.go
index 87358068..676c015a 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -879,6 +879,7 @@ func getExtTargetData(requestExtPrebid *openrtb_ext.ExtRequestPrebid, cacheInstr
 			priceGranularity:          *requestExtPrebid.Targeting.PriceGranularity,
 			mediaTypePriceGranularity: requestExtPrebid.Targeting.MediaTypePriceGranularity,
 			preferDeals:               requestExtPrebid.Targeting.PreferDeals,
+			alwaysIncludeDeals:        requestExtPrebid.Targeting.AlwaysIncludeDeals,
 		}
 	}
 
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 91217f18..173120f7 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -75,6 +75,7 @@ var coreBidderNames []BidderName = []BidderName{
 	BidderBmtm,
 	BidderBoldwin,
 	BidderBrave,
+	BidderBWX,
 	BidderCadentApertureMX,
 	BidderCcx,
 	BidderCoinzilla,
@@ -354,6 +355,7 @@ const (
 	BidderBmtm              BidderName = "bmtm"
 	BidderBoldwin           BidderName = "boldwin"
 	BidderBrave             BidderName = "brave"
+	BidderBWX               BidderName = "bwx"
 	BidderCadentApertureMX  BidderName = "cadent_aperture_mx"
 	BidderCcx               BidderName = "ccx"
 	BidderCoinzilla         BidderName = "coinzilla"
diff --git a/openrtb_ext/imp_bwx.go b/openrtb_ext/imp_bwx.go
new file mode 100644
index 00000000..9181beb0
--- /dev/null
+++ b/openrtb_ext/imp_bwx.go
@@ -0,0 +1,6 @@
+package openrtb_ext
+
+type ExtBWX struct {
+	Env string `json:"env"`
+	Pid string `json:"pid"`
+}
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.go b/openrtb_ext/request.go
index 90a95a1b..f5b403d6 100644
--- a/openrtb_ext/request.go
+++ b/openrtb_ext/request.go
@@ -199,6 +199,7 @@ type ExtRequestTargeting struct {
 	DurationRangeSec          []int                     `json:"durationrangesec,omitempty"`
 	PreferDeals               bool                      `json:"preferdeals,omitempty"`
 	AppendBidderNames         bool                      `json:"appendbiddernames,omitempty"`
+	AlwaysIncludeDeals        bool                      `json:"alwaysincludedeals,omitempty"`
 }
 
 type ExtIncludeBrandCategory struct {
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/bwx.yaml b/static/bidder-info/bwx.yaml
new file mode 100644
index 00000000..cf716c38
--- /dev/null
+++ b/static/bidder-info/bwx.yaml
@@ -0,0 +1,19 @@
+endpoint: "http://rtb.boldwin.live?pid={{.SourceId}}&host={{.Host}}&pbs=1"
+maintainer:
+  email: "prebid@bold-win.com"
+capabilities:
+  app:
+    mediaTypes:
+      - banner
+      - video
+      - native
+  site:
+    mediaTypes:
+      - banner
+      - video
+      - native
+userSync:
+  # BoldwinX supports user syncing, but requires configuration by the host. contact this
+  # bidder directly at the email address in this file to ask about enabling user sync.
+  supports:
+    - redirect
diff --git a/static/bidder-info/kargo.yaml b/static/bidder-info/kargo.yaml
index e1022032..1a7a77eb 100644
--- a/static/bidder-info/kargo.yaml
+++ b/static/bidder-info/kargo.yaml
@@ -11,7 +11,7 @@ capabilities:
       - native
 userSync:
   redirect:
-    url: "https://crb.kargo.com/api/v1/dsync/PrebidServer?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r={{.RedirectURL}}"
+    url: "https://crb.kargo.com/api/v1/dsync/PrebidServer?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&r={{.RedirectURL}}"
     userMacro: "$UID"
 endpointCompression: "GZIP"
 openrtb:
diff --git a/static/bidder-info/medianet.yaml b/static/bidder-info/medianet.yaml
index ea47de2b..ad741e8f 100644
--- a/static/bidder-info/medianet.yaml
+++ b/static/bidder-info/medianet.yaml
@@ -3,6 +3,7 @@ extra_info: "https://medianet.golang.pbs.com"
 maintainer:
   email: "prebid-support@media.net"
 gvlVendorID: 142
+endpointCompression: gzip
 modifyingVastXmlAllowed: true
 capabilities:
   app:
@@ -17,5 +18,5 @@ capabilities:
       - native
 userSync:
   redirect:
-    url: https://hbx.media.net/cksync.php?cs=1&type=pbs&ovsid=setstatuscode&bidder=medianet&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}}
+    url: https://hbx.media.net/cksync.php?cs=1&type=pbs&ovsid=setstatuscode&bidder=medianet&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}
     userMacro: "<vsid>"
\ No newline at end of file
diff --git a/static/bidder-params/bwx.json b/static/bidder-params/bwx.json
new file mode 100644
index 00000000..873aadad
--- /dev/null
+++ b/static/bidder-params/bwx.json
@@ -0,0 +1,21 @@
+{
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "title": "BoldwinX Adapter Params",
+    "description": "A schema which validates params accepted by the BoldwinX adapter",
+    "type": "object",
+    "properties": {
+      "env": {
+        "type": "string",
+        "description": "BoldwinX environment",
+        "minLength": 1
+      },
+      "pid": {
+        "type": "string",
+        "description": "Unique placement ID",
+        "minLength": 1
+      }
+    },
+    "required": [
+      "pid"
+    ]
+  }


Test patch:
diff --git a/adapters/bwx/bwx_test.go b/adapters/bwx/bwx_test.go
new file mode 100644
index 00000000..4e984008
--- /dev/null
+++ b/adapters/bwx/bwx_test.go
@@ -0,0 +1,34 @@
+package bwx
+
+import (
+	"testing"
+
+	"github.com/prebid/prebid-server/v2/adapters/adapterstest"
+	"github.com/prebid/prebid-server/v2/config"
+	"github.com/prebid/prebid-server/v2/openrtb_ext"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestJsonSamples(t *testing.T) {
+	bidder, buildErr := Builder(
+		openrtb_ext.BidderBWX,
+		config.Adapter{
+			Endpoint: "http://rtb.boldwin.live/?pid={{.SourceId}}&host={{.Host}}&pbs=1",
+		},
+		config.Server{
+			ExternalUrl: "http://hosturl.com",
+			GvlID:       1,
+			DataCenter:  "2",
+		},
+	)
+
+	assert.NoError(t, buildErr)
+	adapterstest.RunJSONBidderTest(t, "bwxtest", bidder)
+}
+
+func TestEndpointTemplateMalformed(t *testing.T) {
+	_, buildErr := Builder(openrtb_ext.BidderBWX, config.Adapter{
+		Endpoint: "{{Malformed}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})
+
+	assert.Error(t, buildErr)
+}
diff --git a/adapters/bwx/bwxtest/exemplary/banner.json b/adapters/bwx/bwxtest/exemplary/banner.json
new file mode 100644
index 00000000..af82099d
--- /dev/null
+++ b/adapters/bwx/bwxtest/exemplary/banner.json
@@ -0,0 +1,281 @@
+{
+  "mockBidRequest": {
+    "id": "id",
+    "imp": [
+      {
+        "id": "1",
+        "secure": 1,
+        "bidfloor": 0.01,
+        "bidfloorcur": "USD",
+        "banner": {
+          "w": 300,
+          "h": 250,
+          "pos": 0
+        },
+        "ext": {
+          "bidder": {
+            "pid": "3163e2c9e034770c0daaa98c7613b573"
+          }
+        }
+      },
+      {
+        "id": "2",
+        "secure": 1,
+        "bidfloor": 0.2,
+        "bidfloorcur": "USD",
+        "banner": {
+          "w": 300,
+          "h": 250,
+          "pos": 4
+        },
+        "ext": {
+          "bidder": {
+            "env": "boldwinx-stage",
+            "pid": "3163e2c9e034770c0daaa98c7613b573"
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "UA",
+      "ip": "123.3.4.123"
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 0
+      }
+    },
+    "user": {
+      "id": "userid"
+    },
+    "site": {
+      "id": "id",
+      "domain": "test,com",
+      "cat": ["IAB12"],
+      "publisher": {
+        "id": "pubid"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://rtb.boldwin.live/?pid=3163e2c9e034770c0daaa98c7613b573&host=&pbs=1",
+        "body": {
+          "id": "id",
+          "imp": [
+            {
+              "id": "1",
+              "secure": 1,
+              "bidfloor": 0.01,
+              "bidfloorcur": "USD",
+              "banner": {
+                "w": 300,
+                "h": 250,
+                "pos": 0
+              },
+              "ext": {
+                "bidder": {
+                  "pid": "3163e2c9e034770c0daaa98c7613b573"
+                }
+              }
+            }
+          ],
+          "device": {
+            "ua": "UA",
+            "ip": "123.3.4.123"
+          },
+          "regs": {
+            "ext": {
+              "gdpr": 0
+            }
+          },
+          "user": {
+            "id": "userid"
+          },
+          "site": {
+            "id": "id",
+            "domain": "test,com",
+            "cat": ["IAB12"],
+            "publisher": {
+              "id": "pubid"
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "id",
+                  "impid": "1",
+                  "price": 1.2,
+                  "nurl": "http://test.com/nurl",
+                  "burl": "http://test.com/burl",
+                  "adm": "<span>Test2</span>",
+                  "adomain": ["test.com"],
+                  "cat": ["IAB1"],
+                  "cid": "cid",
+                  "crid": "crid1",
+                  "w": 300,
+                  "h": 250,
+                  "mtype": 1,
+                  "ext": {
+                    "prebid": {
+                      "type": "banner"
+                    }
+                  }
+                }
+              ],
+              "seat": "seat"
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    },
+    {
+      "expectedRequest": {
+        "uri": "http://rtb.boldwin.live/?pid=3163e2c9e034770c0daaa98c7613b573&host=boldwinx-stage&pbs=1",
+        "body": {
+          "id": "id",
+          "imp": [
+            {
+              "id": "2",
+              "secure": 1,
+              "bidfloor": 0.2,
+              "bidfloorcur": "USD",
+              "banner": {
+                "w": 300,
+                "h": 250,
+                "pos": 4
+              },
+              "ext": {
+                "bidder": {
+                  "env": "boldwinx-stage",
+                  "pid": "3163e2c9e034770c0daaa98c7613b573"
+                }
+              }
+            }
+          ],
+          "device": {
+            "ua": "UA",
+            "ip": "123.3.4.123"
+          },
+          "regs": {
+            "ext": {
+              "gdpr": 0
+            }
+          },
+          "user": {
+            "id": "userid"
+          },
+          "site": {
+            "id": "id",
+            "domain": "test,com",
+            "cat": ["IAB12"],
+            "publisher": {
+              "id": "pubid"
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "id",
+                  "impid": "2",
+                  "price": 2.4,
+                  "nurl": "http://test.com/nurl",
+                  "burl": "http://test.com/burl",
+                  "adm": "<span>Test3</span>",
+                  "adomain": ["test.com"],
+                  "cat": ["IAB1"],
+                  "cid": "cid",
+                  "crid": "crid",
+                  "w": 300,
+                  "h": 250,
+                  "mtype": 1,
+                  "ext": {
+                    "prebid": {
+                      "type": "banner"
+                    }
+                  }
+                }
+              ],
+              "seat": "seat"
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "id",
+            "impid": "1",
+            "price": 1.2,
+            "nurl": "http://test.com/nurl",
+            "burl": "http://test.com/burl",
+            "adm": "<span>Test2</span>",
+            "adomain": ["test.com"],
+            "cat": ["IAB1"],
+            "cid": "cid",
+            "crid": "crid1",
+            "w": 300,
+            "h": 250,
+            "mtype": 1,
+            "ext": {
+              "prebid": {
+                "type": "banner"
+              }
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    },
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "id",
+            "impid": "2",
+            "price": 2.4,
+            "nurl": "http://test.com/nurl",
+            "burl": "http://test.com/burl",
+            "adm": "<span>Test3</span>",
+            "adomain": ["test.com"],
+            "cat": ["IAB1"],
+            "cid": "cid",
+            "crid": "crid",
+            "w": 300,
+            "h": 250,
+            "mtype": 1,
+            "ext": {
+              "prebid": {
+                "type": "banner"
+              }
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/bwx/bwxtest/exemplary/native.json b/adapters/bwx/bwxtest/exemplary/native.json
new file mode 100644
index 00000000..0bba9482
--- /dev/null
+++ b/adapters/bwx/bwxtest/exemplary/native.json
@@ -0,0 +1,164 @@
+{
+  "mockBidRequest": {
+    "id": "id",
+    "imp": [
+      {
+        "id": "id",
+        "secure": 1,
+        "bidfloor": 0.01,
+        "bidfloorcur": "USD",
+        "native": {
+          "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}",
+          "ver": "1.1"
+        },
+        "ext": {
+          "bidder": {
+            "env": "boldwinx-stage",
+            "pid": "3163e2c9e034770c0daaa98c7613b573"
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "UA",
+      "ip": "123.3.4.123"
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 0
+      }
+    },
+    "user": {
+      "id": "userid"
+    },
+    "site": {
+      "id": "id",
+      "domain": "test,com",
+      "cat": [
+        "IAB12"
+      ],
+      "publisher": {
+        "id": "pubid"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://rtb.boldwin.live/?pid=3163e2c9e034770c0daaa98c7613b573&host=boldwinx-stage&pbs=1",
+        "body": {
+          "id": "id",
+          "imp": [
+            {
+              "id": "id",
+              "secure": 1,
+              "bidfloor": 0.01,
+              "bidfloorcur": "USD",
+              "native": {
+                "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}",
+                "ver": "1.1"
+              },
+              "ext": {
+                "bidder": {
+                  "env": "boldwinx-stage",
+                  "pid": "3163e2c9e034770c0daaa98c7613b573"
+                }
+              }
+            }
+          ],
+          "device": {
+            "ua": "UA",
+            "ip": "123.3.4.123"
+          },
+          "regs": {
+            "ext": {
+              "gdpr": 0
+            }
+          },
+          "user": {
+            "id": "userid"
+          },
+          "site": {
+            "id": "id",
+            "domain": "test,com",
+            "cat": [
+              "IAB12"
+            ],
+            "publisher": {
+              "id": "pubid"
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "id",
+          "bidid": "id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "id",
+                  "impid": "id",
+                  "price": 0.1,
+                  "nurl": "http://test.com/nurl",
+                  "burl": "http://test.com/burl",
+                  "adm": "{}",
+                  "adomain": [
+                    "test.com"
+                  ],
+                  "cat": [
+                    "IAB1"
+                  ],
+                  "cid": "cid",
+                  "crid": "crid",
+                  "mtype": 4,
+                  "ext": {
+                    "prebid": {
+                      "type": "native"
+                    }
+                  }
+                }
+              ],
+              "seat": "seat"
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "id",
+            "impid": "id",
+            "price": 0.1,
+            "nurl": "http://test.com/nurl",
+            "burl": "http://test.com/burl",
+            "adm": "{}",
+            "adomain": [
+              "test.com"
+            ],
+            "cat": [
+              "IAB1"
+            ],
+            "cid": "cid",
+            "crid": "crid",
+            "mtype": 4,
+            "ext": {
+              "prebid": {
+                "type": "native"
+              }
+            }
+          },
+          "type": "native"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/bwx/bwxtest/exemplary/video.json b/adapters/bwx/bwxtest/exemplary/video.json
new file mode 100644
index 00000000..ffe74ee7
--- /dev/null
+++ b/adapters/bwx/bwxtest/exemplary/video.json
@@ -0,0 +1,204 @@
+{
+  "mockBidRequest": {
+    "id": "id",
+    "imp": [
+      {
+        "id": "id",
+        "secure": 1,
+        "bidfloor": 0.01,
+        "bidfloorcur": "USD",
+        "video": {
+          "mimes": [
+            "video/mp4",
+            "video/ogg",
+            "video/webm"
+          ],
+          "minduration": 3,
+          "maxduration": 3000,
+          "protocols": [
+            2,
+            3,
+            5,
+            6,
+            7,
+            8
+          ],
+          "w": 480,
+          "h": 320,
+          "linearity": 1,
+          "playbackmethod": [
+            2
+          ],
+          "pos": 0
+        },
+        "ext": {
+          "bidder": {
+            "env": "boldwinx-stage",
+            "pid": "3163e2c9e034770c0daaa98c7613b573"
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "UA",
+      "ip": "123.3.4.123"
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 0
+      }
+    },
+    "user": {
+      "id": "userid"
+    },
+    "site": {
+      "id": "id",
+      "domain": "test,com",
+      "cat": [
+        "IAB12"
+      ],
+      "publisher": {
+        "id": "pubid"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://rtb.boldwin.live/?pid=3163e2c9e034770c0daaa98c7613b573&host=boldwinx-stage&pbs=1",
+        "body": {
+          "id": "id",
+          "imp": [
+            {
+              "id": "id",
+              "secure": 1,
+              "bidfloor": 0.01,
+              "bidfloorcur": "USD",
+              "video": {
+                "mimes": [
+                  "video/mp4",
+                  "video/ogg",
+                  "video/webm"
+                ],
+                "minduration": 3,
+                "maxduration": 3000,
+                "protocols": [
+                  2,
+                  3,
+                  5,
+                  6,
+                  7,
+                  8
+                ],
+                "w": 480,
+                "h": 320,
+                "linearity": 1,
+                "playbackmethod": [
+                  2
+                ],
+                "pos": 0
+              },
+              "ext": {
+                "bidder": {
+                  "env": "boldwinx-stage",
+                  "pid": "3163e2c9e034770c0daaa98c7613b573"
+                }
+              }
+            }
+          ],
+          "device": {
+            "ua": "UA",
+            "ip": "123.3.4.123"
+          },
+          "regs": {
+            "ext": {
+              "gdpr": 0
+            }
+          },
+          "user": {
+            "id": "userid"
+          },
+          "site": {
+            "id": "id",
+            "domain": "test,com",
+            "cat": [
+              "IAB12"
+            ],
+            "publisher": {
+              "id": "pubid"
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "id",
+          "bidid": "id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "id",
+                  "impid": "id",
+                  "price": 0.1,
+                  "nurl": "http://test.com/nurl",
+                  "burl": "http://test.com/burl",
+                  "adm": "<VAST>",
+                  "adomain": [
+                    "test.com"
+                  ],
+                  "cat": [
+                    "IAB1"
+                  ],
+                  "cid": "cid",
+                  "crid": "crid",
+                  "mtype": 2,
+                  "ext": {
+                    "prebid": {
+                      "type": "video"
+                    }
+                  }
+                }
+              ],
+              "seat": "seat"
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "id",
+            "impid": "id",
+            "price": 0.1,
+            "nurl": "http://test.com/nurl",
+            "burl": "http://test.com/burl",
+            "adm": "<VAST>",
+            "adomain": [
+              "test.com"
+            ],
+            "cat": [
+              "IAB1"
+            ],
+            "cid": "cid",
+            "crid": "crid",
+            "mtype": 2,
+            "ext": {
+              "prebid": {
+                "type": "video"
+              }
+            }
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/bwx/bwxtest/supplemental/bad-response.json b/adapters/bwx/bwxtest/supplemental/bad-response.json
new file mode 100644
index 00000000..8a32dab1
--- /dev/null
+++ b/adapters/bwx/bwxtest/supplemental/bad-response.json
@@ -0,0 +1,106 @@
+{
+  "mockBidRequest": {
+    "id": "id",
+    "imp": [
+      {
+        "id": "id",
+        "secure": 1,
+        "bidfloor": 0.01,
+        "bidfloorcur": "USD",
+        "banner": {
+          "w": 300,
+          "h": 250
+        },
+        "ext": {
+          "bidder": {
+            "env": "boldwinx-stage",
+            "pid": "3163e2c9e034770c0daaa98c7613b573"
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "UA",
+      "ip": "123.3.4.123"
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 0
+      }
+    },
+    "user": {
+      "id": "userid"
+    },
+    "site": {
+      "id": "id",
+      "domain": "test,com",
+      "cat": [
+        "IAB12"
+      ],
+      "publisher": {
+        "id": "pubid"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://rtb.boldwin.live/?pid=3163e2c9e034770c0daaa98c7613b573&host=boldwinx-stage&pbs=1",
+        "body": {
+          "id": "id",
+          "imp": [
+            {
+              "id": "id",
+              "secure": 1,
+              "bidfloor": 0.01,
+              "bidfloorcur": "USD",
+              "banner": {
+                "w": 300,
+                "h": 250
+              },
+              "ext": {
+                "bidder": {
+                  "env": "boldwinx-stage",
+                  "pid": "3163e2c9e034770c0daaa98c7613b573"
+                }
+              }
+            }
+          ],
+          "device": {
+            "ua": "UA",
+            "ip": "123.3.4.123"
+          },
+          "regs": {
+            "ext": {
+              "gdpr": 0
+            }
+          },
+          "user": {
+            "id": "userid"
+          },
+          "site": {
+            "id": "id",
+            "domain": "test,com",
+            "cat": [
+              "IAB12"
+            ],
+            "publisher": {
+              "id": "pubid"
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": ""
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "json: cannot unmarshal string into Go value of type openrtb2.BidResponse",
+      "comparison": "literal"
+    }
+  ],
+  "expectedBidResponses": []
+}
diff --git a/adapters/bwx/bwxtest/supplemental/empty-mediatype.json b/adapters/bwx/bwxtest/supplemental/empty-mediatype.json
new file mode 100644
index 00000000..ad319e84
--- /dev/null
+++ b/adapters/bwx/bwxtest/supplemental/empty-mediatype.json
@@ -0,0 +1,190 @@
+{
+  "mockBidRequest": {
+    "id": "id",
+    "imp": [
+      {
+        "id": "id",
+        "secure": 1,
+        "bidfloor": 0.01,
+        "bidfloorcur": "USD",
+        "banner": {
+          "w": 300,
+          "h": 250
+        },
+        "ext": {
+          "bidder": {
+            "env": "boldwinx-stage",
+            "pid": "3163e2c9e034770c0daaa98c7613b573"
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "UA",
+      "ip": "123.3.4.123"
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 0
+      }
+    },
+    "user": {
+      "id": "userid"
+    },
+    "site": {
+      "id": "id",
+      "domain": "test,com",
+      "cat": [
+        "IAB12"
+      ],
+      "publisher": {
+        "id": "pubid"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://rtb.boldwin.live/?pid=3163e2c9e034770c0daaa98c7613b573&host=boldwinx-stage&pbs=1",
+        "body": {
+          "id": "id",
+          "imp": [
+            {
+              "id": "id",
+              "secure": 1,
+              "bidfloor": 0.01,
+              "bidfloorcur": "USD",
+              "banner": {
+                "w": 300,
+                "h": 250
+              },
+              "ext": {
+                "bidder": {
+                  "env": "boldwinx-stage",
+                  "pid": "3163e2c9e034770c0daaa98c7613b573"
+                }
+              }
+            }
+          ],
+          "device": {
+            "ua": "UA",
+            "ip": "123.3.4.123"
+          },
+          "regs": {
+            "ext": {
+              "gdpr": 0
+            }
+          },
+          "user": {
+            "id": "userid"
+          },
+          "site": {
+            "id": "id",
+            "domain": "test,com",
+            "cat": [
+              "IAB12"
+            ],
+            "publisher": {
+              "id": "pubid"
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "id",
+          "bidid": "id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "id",
+                  "impid": "1",
+                  "price": 0.1,
+                  "nurl": "http://test.com/nurl",
+                  "burl": "http://test.com/burl",
+                  "adm": "<span>Test1</span>",
+                  "adomain": [
+                    "test.com"
+                  ],
+                  "cat": [
+                    "IAB1"
+                  ],
+                  "cid": "cid",
+                  "crid": "crid",
+                  "mtype": 1,
+                  "w": 300,
+                  "h": 250,
+                  "ext": {
+                    "prebid": {
+                      "type": "banner"
+                    }
+                  }
+                }, {
+                  "id": "id",
+                  "impid": "2",
+                  "price": 1.2,
+                  "nurl": "http://test.com/nurl",
+                  "burl": "http://test.com/burl",
+                  "adm": "<span>Test2</span>",
+                  "adomain": [
+                    "test.com"
+                  ],
+                  "cat": [
+                    "IAB1"
+                  ],
+                  "cid": "cid",
+                  "crid": "crid",
+                  "w": 300,
+                  "h": 250,
+                  "ext": {
+                    "some": "value"
+                  }
+                }
+              ],
+              "seat": "seat"
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "failed to parse bid mtype (0) for impression id \"2\"",
+      "comparison": "literal"
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency":"USD",
+      "bids":[
+        {
+          "bid": {
+            "id": "id",
+            "impid": "1",
+            "price": 0.1,
+            "nurl": "http://test.com/nurl",
+            "burl": "http://test.com/burl",
+            "adm": "<span>Test1</span>",
+            "adomain": ["test.com"],
+            "cat": ["IAB1"],
+            "cid": "cid",
+            "crid": "crid",
+            "w": 300,
+            "h": 250,
+            "mtype": 1,
+            "ext": {
+              "prebid": {
+                "type": "banner"
+              }
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/bwx/bwxtest/supplemental/empty-seatbid-0-bid.json b/adapters/bwx/bwxtest/supplemental/empty-seatbid-0-bid.json
new file mode 100644
index 00000000..ce9d7875
--- /dev/null
+++ b/adapters/bwx/bwxtest/supplemental/empty-seatbid-0-bid.json
@@ -0,0 +1,111 @@
+{
+  "mockBidRequest": {
+    "id": "id",
+    "imp": [
+      {
+        "id": "id",
+        "secure": 1,
+        "bidfloor": 0.01,
+        "bidfloorcur": "USD",
+        "banner": {
+          "w": 300,
+          "h": 250
+        },
+        "ext": {
+          "bidder": {
+            "env": "boldwinx-stage",
+            "pid": "3163e2c9e034770c0daaa98c7613b573"
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "UA",
+      "ip": "123.3.4.123"
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 0
+      }
+    },
+    "user": {
+      "id": "userid"
+    },
+    "site": {
+      "id": "id",
+      "domain": "test,com",
+      "cat": [
+        "IAB12"
+      ],
+      "publisher": {
+        "id": "pubid"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://rtb.boldwin.live/?pid=3163e2c9e034770c0daaa98c7613b573&host=boldwinx-stage&pbs=1",
+        "body": {
+          "id": "id",
+          "imp": [
+            {
+              "id": "id",
+              "secure": 1,
+              "bidfloor": 0.01,
+              "bidfloorcur": "USD",
+              "banner": {
+                "w": 300,
+                "h": 250
+              },
+              "ext": {
+                "bidder": {
+                  "env": "boldwinx-stage",
+                  "pid": "3163e2c9e034770c0daaa98c7613b573"
+                }
+              }
+            }
+          ],
+          "device": {
+            "ua": "UA",
+            "ip": "123.3.4.123"
+          },
+          "regs": {
+            "ext": {
+              "gdpr": 0
+            }
+          },
+          "user": {
+            "id": "userid"
+          },
+          "site": {
+            "id": "id",
+            "domain": "test,com",
+            "cat": [
+              "IAB12"
+            ],
+            "publisher": {
+              "id": "pubid"
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "id",
+          "bidid": "id",
+          "seatbid": [
+            {
+              "bid": [],
+              "seat": "seat"
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [],
+  "expectedBidResponses": [{"currency":"USD","bids":[]}]
+}
diff --git a/adapters/bwx/bwxtest/supplemental/empty-seatbid.json b/adapters/bwx/bwxtest/supplemental/empty-seatbid.json
new file mode 100644
index 00000000..2bb3290f
--- /dev/null
+++ b/adapters/bwx/bwxtest/supplemental/empty-seatbid.json
@@ -0,0 +1,111 @@
+{
+  "mockBidRequest": {
+    "id": "id",
+    "imp": [
+      {
+        "id": "id",
+        "secure": 1,
+        "bidfloor": 0.01,
+        "bidfloorcur": "USD",
+        "banner": {
+          "w": 300,
+          "h": 250
+        },
+        "ext": {
+          "bidder": {
+            "env": "boldwinx-stage",
+            "pid": "3163e2c9e034770c0daaa98c7613b573"
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "UA",
+      "ip": "123.3.4.123"
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 0
+      }
+    },
+    "user": {
+      "id": "userid"
+    },
+    "site": {
+      "id": "id",
+      "domain": "test,com",
+      "cat": [
+        "IAB12"
+      ],
+      "publisher": {
+        "id": "pubid"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://rtb.boldwin.live/?pid=3163e2c9e034770c0daaa98c7613b573&host=boldwinx-stage&pbs=1",
+        "body": {
+          "id": "id",
+          "imp": [
+            {
+              "id": "id",
+              "secure": 1,
+              "bidfloor": 0.01,
+              "bidfloorcur": "USD",
+              "banner": {
+                "w": 300,
+                "h": 250
+              },
+              "ext": {
+                "bidder": {
+                  "env": "boldwinx-stage",
+                  "pid": "3163e2c9e034770c0daaa98c7613b573"
+                }
+              }
+            }
+          ],
+          "device": {
+            "ua": "UA",
+            "ip": "123.3.4.123"
+          },
+          "regs": {
+            "ext": {
+              "gdpr": 0
+            }
+          },
+          "user": {
+            "id": "userid"
+          },
+          "site": {
+            "id": "id",
+            "domain": "test,com",
+            "cat": [
+              "IAB12"
+            ],
+            "publisher": {
+              "id": "pubid"
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "a1580f2f-be6d-11eb-a150-d094662c1c35",
+          "bidid": "359da97d0384d8a14767029c18fd840d",
+          "seatbid": [],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Array SeatBid cannot be empty",
+      "comparison": "literal"
+    }
+  ],
+  "expectedBidResponses": []
+}
diff --git a/adapters/bwx/bwxtest/supplemental/invalid-ext-bidder-object.json b/adapters/bwx/bwxtest/supplemental/invalid-ext-bidder-object.json
new file mode 100644
index 00000000..ed081b79
--- /dev/null
+++ b/adapters/bwx/bwxtest/supplemental/invalid-ext-bidder-object.json
@@ -0,0 +1,49 @@
+{
+  "mockBidRequest": {
+    "id": "id",
+    "imp": [
+      {
+        "id": "id",
+        "secure": 1,
+        "bidfloor": 0.01,
+        "bidfloorcur": "USD",
+        "banner": {
+          "w": 300,
+          "h": 250
+        },
+        "ext": {
+          "bidder": []
+        }
+      }
+    ],
+    "device": {
+      "ua": "UA",
+      "ip": "123.3.4.123"
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 0
+      }
+    },
+    "user": {
+      "id": "userid"
+    },
+    "site": {
+      "id": "id",
+      "domain": "test,com",
+      "cat": [
+        "IAB12"
+      ],
+      "publisher": {
+        "id": "pubid"
+      }
+    }
+  },
+  "httpCalls": [],
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "Failed to deserialize BoldwinX extension: json: cannot unmarshal array into Go value of type openrtb_ext.ExtBWX",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/bwx/bwxtest/supplemental/invalid-ext-object.json b/adapters/bwx/bwxtest/supplemental/invalid-ext-object.json
new file mode 100644
index 00000000..aa215eb3
--- /dev/null
+++ b/adapters/bwx/bwxtest/supplemental/invalid-ext-object.json
@@ -0,0 +1,47 @@
+{
+  "mockBidRequest": {
+    "id": "id",
+    "imp": [
+      {
+        "id": "id",
+        "secure": 1,
+        "bidfloor": 0.01,
+        "bidfloorcur": "USD",
+        "banner": {
+          "w": 300,
+          "h": 250
+        },
+        "ext": ""
+      }
+    ],
+    "device": {
+      "ua": "UA",
+      "ip": "123.3.4.123"
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 0
+      }
+    },
+    "user": {
+      "id": "userid"
+    },
+    "site": {
+      "id": "id",
+      "domain": "test,com",
+      "cat": [
+        "IAB12"
+      ],
+      "publisher": {
+        "id": "pubid"
+      }
+    }
+  },
+  "httpCalls": [],
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "Failed to deserialize bidder impression extension: json: cannot unmarshal string into Go value of type adapters.ExtImpBidder",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/bwx/bwxtest/supplemental/invalid-mediatype.json b/adapters/bwx/bwxtest/supplemental/invalid-mediatype.json
new file mode 100644
index 00000000..aff08f53
--- /dev/null
+++ b/adapters/bwx/bwxtest/supplemental/invalid-mediatype.json
@@ -0,0 +1,187 @@
+{
+  "mockBidRequest": {
+    "id": "id",
+    "imp": [
+      {
+        "id": "id",
+        "secure": 1,
+        "bidfloor": 0.01,
+        "bidfloorcur": "USD",
+        "banner": {
+          "w": 300,
+          "h": 250
+        },
+        "ext": {
+          "bidder": {
+            "env": "boldwinx-stage",
+            "pid": "3163e2c9e034770c0daaa98c7613b573"
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "UA",
+      "ip": "123.3.4.123"
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 0
+      }
+    },
+    "user": {
+      "id": "userid"
+    },
+    "site": {
+      "id": "id",
+      "domain": "test,com",
+      "cat": [
+        "IAB12"
+      ],
+      "publisher": {
+        "id": "pubid"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://rtb.boldwin.live/?pid=3163e2c9e034770c0daaa98c7613b573&host=boldwinx-stage&pbs=1",
+        "body": {
+          "id": "id",
+          "imp": [
+            {
+              "id": "id",
+              "secure": 1,
+              "bidfloor": 0.01,
+              "bidfloorcur": "USD",
+              "banner": {
+                "w": 300,
+                "h": 250
+              },
+              "ext": {
+                "bidder": {
+                  "env": "boldwinx-stage",
+                  "pid": "3163e2c9e034770c0daaa98c7613b573"
+                }
+              }
+            }
+          ],
+          "device": {
+            "ua": "UA",
+            "ip": "123.3.4.123"
+          },
+          "regs": {
+            "ext": {
+              "gdpr": 0
+            }
+          },
+          "user": {
+            "id": "userid"
+          },
+          "site": {
+            "id": "id",
+            "domain": "test,com",
+            "cat": [
+              "IAB12"
+            ],
+            "publisher": {
+              "id": "pubid"
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "id",
+          "bidid": "id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "id",
+                  "impid": "1",
+                  "price": 0.1,
+                  "nurl": "http://test.com/nurl",
+                  "burl": "http://test.com/burl",
+                  "adm": "<span>Test1</span>",
+                  "adomain": [
+                    "test.com"
+                  ],
+                  "cat": [
+                    "IAB1"
+                  ],
+                  "cid": "cid",
+                  "crid": "crid",
+                  "w": 300,
+                  "h": 250,
+                  "mtype": 1,
+                  "ext": {
+                    "prebid": {
+                      "type": "banner"
+                    }
+                  }
+                }, {
+                  "id": "id",
+                  "impid": "2",
+                  "price": 1.2,
+                  "nurl": "http://test.com/nurl",
+                  "burl": "http://test.com/burl",
+                  "adm": "<span>Test2</span>",
+                  "adomain": [
+                    "test.com"
+                  ],
+                  "cat": [
+                    "IAB1"
+                  ],
+                  "cid": "cid",
+                  "crid": "crid",
+                  "w": 300,
+                  "h": 250
+                }
+              ],
+              "seat": "seat"
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "failed to parse bid mtype (0) for impression id \"2\"",
+      "comparison": "literal"
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency":"USD",
+      "bids":[
+        {
+          "bid": {
+            "id": "id",
+            "impid": "1",
+            "price": 0.1,
+            "nurl": "http://test.com/nurl",
+            "burl": "http://test.com/burl",
+            "adm": "<span>Test1</span>",
+            "adomain": ["test.com"],
+            "cat": ["IAB1"],
+            "cid": "cid",
+            "crid": "crid",
+            "w": 300,
+            "h": 250,
+            "mtype": 1,
+            "ext": {
+              "prebid": {
+                "type": "banner"
+              }
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/bwx/bwxtest/supplemental/status-204.json b/adapters/bwx/bwxtest/supplemental/status-204.json
new file mode 100644
index 00000000..e89b6e19
--- /dev/null
+++ b/adapters/bwx/bwxtest/supplemental/status-204.json
@@ -0,0 +1,100 @@
+{
+  "mockBidRequest": {
+    "id": "id",
+    "imp": [
+      {
+        "id": "id",
+        "secure": 1,
+        "bidfloor": 0.01,
+        "bidfloorcur": "USD",
+        "banner": {
+          "w": 300,
+          "h": 250
+        },
+        "ext": {
+          "bidder": {
+            "env": "boldwinx-stage",
+            "pid": "3163e2c9e034770c0daaa98c7613b573"
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "UA",
+      "ip": "123.3.4.123"
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 0
+      }
+    },
+    "user": {
+      "id": "userid"
+    },
+    "site": {
+      "id": "id",
+      "domain": "test,com",
+      "cat": [
+        "IAB12"
+      ],
+      "publisher": {
+        "id": "pubid"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://rtb.boldwin.live/?pid=3163e2c9e034770c0daaa98c7613b573&host=boldwinx-stage&pbs=1",
+        "body": {
+          "id": "id",
+          "imp": [
+            {
+              "id": "id",
+              "secure": 1,
+              "bidfloor": 0.01,
+              "bidfloorcur": "USD",
+              "banner": {
+                "w": 300,
+                "h": 250
+              },
+              "ext": {
+                "bidder": {
+                  "env": "boldwinx-stage",
+                  "pid": "3163e2c9e034770c0daaa98c7613b573"
+                }
+              }
+            }
+          ],
+          "device": {
+            "ua": "UA",
+            "ip": "123.3.4.123"
+          },
+          "regs": {
+            "ext": {
+              "gdpr": 0
+            }
+          },
+          "user": {
+            "id": "userid"
+          },
+          "site": {
+            "id": "id",
+            "domain": "test,com",
+            "cat": [
+              "IAB12"
+            ],
+            "publisher": {
+              "id": "pubid"
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 204,
+        "body": {}
+      }
+    }
+  ],
+  "expectedBidResponses": []
+}
diff --git a/adapters/bwx/bwxtest/supplemental/status-400.json b/adapters/bwx/bwxtest/supplemental/status-400.json
new file mode 100644
index 00000000..20942040
--- /dev/null
+++ b/adapters/bwx/bwxtest/supplemental/status-400.json
@@ -0,0 +1,106 @@
+{
+  "mockBidRequest": {
+    "id": "id",
+    "imp": [
+      {
+        "id": "id",
+        "secure": 1,
+        "bidfloor": 0.01,
+        "bidfloorcur": "USD",
+        "banner": {
+          "w": 300,
+          "h": 250
+        },
+        "ext": {
+          "bidder": {
+            "env": "boldwinx-stage",
+            "pid": "3163e2c9e034770c0daaa98c7613b573"
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "UA",
+      "ip": "123.3.4.123"
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 0
+      }
+    },
+    "user": {
+      "id": "userid"
+    },
+    "site": {
+      "id": "id",
+      "domain": "test,com",
+      "cat": [
+        "IAB12"
+      ],
+      "publisher": {
+        "id": "pubid"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://rtb.boldwin.live/?pid=3163e2c9e034770c0daaa98c7613b573&host=boldwinx-stage&pbs=1",
+        "body": {
+          "id": "id",
+          "imp": [
+            {
+              "id": "id",
+              "secure": 1,
+              "bidfloor": 0.01,
+              "bidfloorcur": "USD",
+              "banner": {
+                "w": 300,
+                "h": 250
+              },
+              "ext": {
+                "bidder": {
+                  "env": "boldwinx-stage",
+                  "pid": "3163e2c9e034770c0daaa98c7613b573"
+                }
+              }
+            }
+          ],
+          "device": {
+            "ua": "UA",
+            "ip": "123.3.4.123"
+          },
+          "regs": {
+            "ext": {
+              "gdpr": 0
+            }
+          },
+          "user": {
+            "id": "userid"
+          },
+          "site": {
+            "id": "id",
+            "domain": "test,com",
+            "cat": [
+              "IAB12"
+            ],
+            "publisher": {
+              "id": "pubid"
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 400,
+        "body": "The Key has a different ad format"
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Unexpected status code: 400. Run with request.debug = 1 for more info",
+      "comparison": "literal"
+    }
+  ],
+  "expectedBidResponses": []
+}
diff --git a/adapters/bwx/bwxtest/supplemental/status-503.json b/adapters/bwx/bwxtest/supplemental/status-503.json
new file mode 100644
index 00000000..3d5a6170
--- /dev/null
+++ b/adapters/bwx/bwxtest/supplemental/status-503.json
@@ -0,0 +1,105 @@
+{
+  "mockBidRequest": {
+    "id": "id",
+    "imp": [
+      {
+        "id": "id",
+        "secure": 1,
+        "bidfloor": 0.01,
+        "bidfloorcur": "USD",
+        "banner": {
+          "w": 300,
+          "h": 250
+        },
+        "ext": {
+          "bidder": {
+            "env": "boldwinx-stage",
+            "pid": "3163e2c9e034770c0daaa98c7613b573"
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "UA",
+      "ip": "123.3.4.123"
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 0
+      }
+    },
+    "user": {
+      "id": "userid"
+    },
+    "site": {
+      "id": "id",
+      "domain": "test,com",
+      "cat": [
+        "IAB12"
+      ],
+      "publisher": {
+        "id": "pubid"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://rtb.boldwin.live/?pid=3163e2c9e034770c0daaa98c7613b573&host=boldwinx-stage&pbs=1",
+        "body": {
+          "id": "id",
+          "imp": [
+            {
+              "id": "id",
+              "secure": 1,
+              "bidfloor": 0.01,
+              "bidfloorcur": "USD",
+              "banner": {
+                "w": 300,
+                "h": 250
+              },
+              "ext": {
+                "bidder": {
+                  "env": "boldwinx-stage",
+                  "pid": "3163e2c9e034770c0daaa98c7613b573"
+                }
+              }
+            }
+          ],
+          "device": {
+            "ua": "UA",
+            "ip": "123.3.4.123"
+          },
+          "regs": {
+            "ext": {
+              "gdpr": 0
+            }
+          },
+          "user": {
+            "id": "userid"
+          },
+          "site": {
+            "id": "id",
+            "domain": "test,com",
+            "cat": [
+              "IAB12"
+            ],
+            "publisher": {
+              "id": "pubid"
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 503
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Unexpected status code: 503. Run with request.debug = 1 for more info",
+      "comparison": "literal"
+    }
+  ],
+  "expectedBidResponses": []
+}
diff --git a/adapters/bwx/bwxtest/supplemental/unexpected-status.json b/adapters/bwx/bwxtest/supplemental/unexpected-status.json
new file mode 100644
index 00000000..f3d7ecc7
--- /dev/null
+++ b/adapters/bwx/bwxtest/supplemental/unexpected-status.json
@@ -0,0 +1,106 @@
+{
+  "mockBidRequest": {
+    "id": "id",
+    "imp": [
+      {
+        "id": "id",
+        "secure": 1,
+        "bidfloor": 0.01,
+        "bidfloorcur": "USD",
+        "banner": {
+          "w": 300,
+          "h": 250
+        },
+        "ext": {
+          "bidder": {
+            "env": "boldwinx-stage",
+            "pid": "3163e2c9e034770c0daaa98c7613b573"
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "UA",
+      "ip": "123.3.4.123"
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 0
+      }
+    },
+    "user": {
+      "id": "userid"
+    },
+    "site": {
+      "id": "id",
+      "domain": "test,com",
+      "cat": [
+        "IAB12"
+      ],
+      "publisher": {
+        "id": "pubid"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://rtb.boldwin.live/?pid=3163e2c9e034770c0daaa98c7613b573&host=boldwinx-stage&pbs=1",
+        "body": {
+          "id": "id",
+          "imp": [
+            {
+              "id": "id",
+              "secure": 1,
+              "bidfloor": 0.01,
+              "bidfloorcur": "USD",
+              "banner": {
+                "w": 300,
+                "h": 250
+              },
+              "ext": {
+                "bidder": {
+                  "env": "boldwinx-stage",
+                  "pid": "3163e2c9e034770c0daaa98c7613b573"
+                }
+              }
+            }
+          ],
+          "device": {
+            "ua": "UA",
+            "ip": "123.3.4.123"
+          },
+          "regs": {
+            "ext": {
+              "gdpr": 0
+            }
+          },
+          "user": {
+            "id": "userid"
+          },
+          "site": {
+            "id": "id",
+            "domain": "test,com",
+            "cat": [
+              "IAB12"
+            ],
+            "publisher": {
+              "id": "pubid"
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 403,
+        "body": "Access is denied"
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Unexpected status code: 403. Run with request.debug = 1 for more info",
+      "comparison": "literal"
+    }
+  ],
+  "expectedBidResponses": []
+}
diff --git a/adapters/bwx/params_test.go b/adapters/bwx/params_test.go
new file mode 100644
index 00000000..f51d8630
--- /dev/null
+++ b/adapters/bwx/params_test.go
@@ -0,0 +1,54 @@
+package bwx
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/prebid/prebid-server/v2/openrtb_ext"
+)
+
+var validParams = []string{
+	`{"env":"boldwinx-stage", "pid":"123456"}`,
+	`{"pid":"123456"}`,
+}
+
+func TestValidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json-schemas. %v", err)
+	}
+
+	for _, validParam := range validParams {
+		if err := validator.Validate(openrtb_ext.BidderBWX, json.RawMessage(validParam)); err != nil {
+			t.Errorf("Schema rejected boldwinx params: %s", validParam)
+		}
+	}
+}
+
+var invalidParams = []string{
+	``,
+	`null`,
+	`true`,
+	`5`,
+	`[]`,
+	`{}`,
+	`{"some": "param"}`,
+	`{"env":"boldwinx-stage"}`,
+	`{"othervalue":"Lorem ipsum"}`,
+	`{"env":"boldwinx-stage", pid:""}`,
+	`{pid:101010}`,
+	`{pid:"valid-pid", env: 0}`,
+}
+
+func TestInvalidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json-schemas. %v", err)
+	}
+
+	for _, invalidParam := range invalidParams {
+		if err := validator.Validate(openrtb_ext.BidderBWX, json.RawMessage(invalidParam)); err == nil {
+			t.Errorf("Schema allowed unexpected params: %s", invalidParam)
+		}
+	}
+}
diff --git a/adapters/improvedigital/improvedigitaltest/supplemental/addtl-consent-multi-tilda.json b/adapters/improvedigital/improvedigitaltest/supplemental/addtl-consent-multi-tilda.json
new file mode 100644
index 00000000..24c4b813
--- /dev/null
+++ b/adapters/improvedigital/improvedigitaltest/supplemental/addtl-consent-multi-tilda.json
@@ -0,0 +1,94 @@
+{
+  "mockBidRequest": {
+    "id": "addtl-consent-request-id",
+    "site": {
+      "page": "https://good.site/url"
+    },
+    "imp": [{
+      "id": "test-imp-id",
+      "banner": {
+        "format": [{
+          "w": 300,
+          "h": 250
+        }]
+      },
+      "ext": {
+        "bidder": {
+          "placementId": 13245
+        }
+      }
+    }],
+    "user": {
+      "ext":{"consent":"ABC","ConsentedProvidersSettings":{"consented_providers":"1~10.20.90~2"}}
+    }
+  },
+
+  "httpCalls": [{
+    "expectedRequest": {
+      "uri": "http://localhost/pbs",
+      "body": {
+        "id": "addtl-consent-request-id",
+        "site": {
+          "page": "https://good.site/url"
+        },
+        "imp": [{
+          "id": "test-imp-id",
+          "banner": {
+            "format": [{
+              "w": 300,
+              "h": 250
+            }]
+          },
+          "ext": {
+            "bidder": {
+              "placementId": 13245
+            }
+          }
+        }],
+        "user": {
+          "ext": {"consent": "ABC","ConsentedProvidersSettings":{"consented_providers":"1~10.20.90~2"},"consented_providers_settings": {"consented_providers": [10,20,90]}
+          }
+        }
+      }
+    },
+    "mockResponse": {
+      "status": 200,
+      "body": {
+        "id": "addtl-consent-request-id",
+        "seatbid": [{
+          "seat": "improvedigital",
+          "bid": [{
+            "id": "randomid",
+            "impid": "test-imp-id",
+            "price": 0.500000,
+            "adid": "12345678",
+            "adm": "some-test-ad",
+            "cid": "987",
+            "crid": "12345678",
+            "h": 250,
+            "w": 300
+          }]
+        }],
+        "cur": "USD"
+      }
+    }
+  }],
+
+  "expectedBidResponses": [{
+    "currency": "USD",
+    "bids": [{
+      "bid": {
+        "id": "randomid",
+        "impid": "test-imp-id",
+        "price": 0.5,
+        "adm": "some-test-ad",
+        "adid": "12345678",
+        "cid": "987",
+        "crid": "12345678",
+        "w": 300,
+        "h": 250
+      },
+      "type": "banner"
+    }]
+  }]
+}
diff --git a/adapters/yieldlab/yieldlab_test.go b/adapters/yieldlab/yieldlab_test.go
index 7704c886..49dcc31e 100644
--- a/adapters/yieldlab/yieldlab_test.go
+++ b/adapters/yieldlab/yieldlab_test.go
@@ -257,6 +257,98 @@ func Test_makeSupplyChain(t *testing.T) {
 	}
 }
 
+func Test_makeDSATransparencyUrlParam(t *testing.T) {
+	tests := []struct {
+		name           string
+		transparencies []dsaTransparency
+		expected       string
+	}{
+		{
+			name:           "No transparency objects",
+			transparencies: []dsaTransparency{},
+			expected:       "",
+		},
+		{
+			name:           "Nil transparency",
+			transparencies: nil,
+			expected:       "",
+		},
+		{
+			name: "Params without a domain",
+			transparencies: []dsaTransparency{
+				{
+					Params: []int{1, 2},
+				},
+			},
+			expected: "",
+		},
+		{
+			name: "Params without a params",
+			transparencies: []dsaTransparency{
+				{
+					Domain: "domain.com",
+				},
+			},
+			expected: "domain.com",
+		},
+		{
+			name: "One object; No Params",
+			transparencies: []dsaTransparency{
+				{
+					Domain: "domain.com",
+					Params: []int{},
+				},
+			},
+			expected: "domain.com",
+		},
+		{
+			name: "One object; One Param",
+			transparencies: []dsaTransparency{
+				{
+					Domain: "domain.com",
+					Params: []int{1},
+				},
+			},
+			expected: "domain.com~1",
+		},
+		{
+			name: "Three domain objects",
+			transparencies: []dsaTransparency{
+				{
+					Domain: "domain1.com",
+					Params: []int{1, 2},
+				},
+				{
+					Domain: "domain2.com",
+					Params: []int{3, 4},
+				},
+				{
+					Domain: "domain3.com",
+					Params: []int{5, 6},
+				},
+			},
+			expected: "domain1.com~1_2~~domain2.com~3_4~~domain3.com~5_6",
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			actual := makeDSATransparencyURLParam(test.transparencies)
+			assert.Equal(t, test.expected, actual)
+		})
+	}
+}
+
+func Test_getDSA_invalidRequestExt(t *testing.T) {
+	req := &openrtb2.BidRequest{
+		Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"DSA":"wrongValueType"}`)},
+	}
+
+	dsa, err := getDSA(req)
+
+	assert.NotNil(t, err)
+	assert.Nil(t, dsa)
+}
+
 func TestYieldlabAdapter_makeEndpointURL_invalidEndpoint(t *testing.T) {
 	bidder, buildErr := Builder(openrtb_ext.BidderYieldlab, config.Adapter{
 		Endpoint: "test$:/something§"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})
diff --git a/adapters/yieldlab/yieldlabtest/exemplary/dsa.json b/adapters/yieldlab/yieldlabtest/exemplary/dsa.json
new file mode 100644
index 00000000..0c815835
--- /dev/null
+++ b/adapters/yieldlab/yieldlabtest/exemplary/dsa.json
@@ -0,0 +1,169 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-with-DSA",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 728,
+              "h": 90
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "adslotId": "12345",
+            "supplyId": "123456789",
+            "targeting": {
+              "key1": "value1",
+              "key2": "value2"
+            },
+            "extId": "abc"
+          }
+        }
+      }
+    ],
+    "device": {
+      "ifa": "hello-ads",
+      "devicetype": 4,
+      "connectiontype": 6,
+      "geo": {
+        "lat": 51.499488,
+        "lon": -0.128953
+      },
+      "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36",
+      "ip": "169.254.13.37",
+      "h": 1098,
+      "w": 814
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 1,
+        "dsa": {
+          "dsarequired": 3,
+          "pubrender": 0,
+          "datatopub": 2,
+          "transparency": [
+            {
+              "domain": "platform1domain.com",
+              "dsaparams": [
+                1
+              ]
+            },
+            {
+              "domain": "SSP2domain.com",
+              "dsaparams": [
+                1,
+                2
+              ]
+            }
+          ]
+        }
+      }
+    },
+    "site": {
+      "id": "fake-site-id",
+      "publisher": {
+        "id": "1"
+      },
+      "page": "http://localhost:9090/gdpr.html"
+    },
+    "user": {
+      "buyeruid": "34a53e82-0dc3-4815-8b7e-b725ede0361c",
+      "ext": {
+        "consent": "BOlOrv1OlOr2EAAABADECg-AAAApp7v______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-3zd4u_1vf99yfm1-7etr3tp_87ues2_Xur__79__3z3_9phP78k89r7337Ew-v02"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "headers": {
+          "Accept": [
+            "application/json"
+          ],
+          "Cookie": [
+            "id=34a53e82-0dc3-4815-8b7e-b725ede0361c"
+          ],
+          "Referer": [
+            "http://localhost:9090/gdpr.html"
+          ],
+          "User-Agent": [
+            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36"
+          ],
+          "X-Forwarded-For": [
+            "169.254.13.37"
+          ]
+        },
+        "uri": "https://ad.yieldlab.net/testing/12345?consent=BOlOrv1OlOr2EAAABADECg-AAAApp7v______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-3zd4u_1vf99yfm1-7etr3tp_87ues2_Xur__79__3z3_9phP78k89r7337Ew-v02&content=json&dsadatatopub=2&dsapubrender=0&dsarequired=3&dsatransparency=platform1domain.com~1~~SSP2domain.com~1_2&gdpr=1&ids=ylid%3A34a53e82-0dc3-4815-8b7e-b725ede0361c&lat=51.499488&lon=-0.128953&pvid=true&sizes=12345%3A728x90&t=key1%3Dvalue1%26key2%3Dvalue2&ts=testing&yl_rtb_connectiontype=6&yl_rtb_devicetype=4&yl_rtb_ifa=hello-ads"
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": [
+          {
+            "id": 12345,
+            "price": 201,
+            "advertiser": "yieldlab",
+            "adsize": "728x90",
+            "pid": 1234,
+            "did": 5678,
+            "pvid": "40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5",
+            "dsa": {
+              "required": 3,
+              "behalf": "on behalf of yieldlab",
+              "paid": "by yieldlab",
+              "transparency": [
+                {
+                  "domain": "yieldlab.de",
+                  "dsaparams": [
+                    1,
+                    2
+                  ]
+                }
+              ],
+              "adrender": 0
+            }
+          }
+        ]
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "EUR",
+      "bids": [
+        {
+          "bid": {
+            "adm": "<script src=\"https://ad.yieldlab.net/d/12345/123456789/728x90?consent=BOlOrv1OlOr2EAAABADECg-AAAApp7v______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-3zd4u_1vf99yfm1-7etr3tp_87ues2_Xur__79__3z3_9phP78k89r7337Ew-v02&gdpr=1&id=abc&ids=ylid%3A34a53e82-0dc3-4815-8b7e-b725ede0361c&pvid=40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5&ts=testing\"></script>",
+            "crid": "12345123433",
+            "dealid": "1234",
+            "ext": {
+              "dsa": {
+                "adrender": 0,
+                "transparency": [
+                  {
+                    "domain": "yieldlab.de",
+                    "dsaparams": [
+                      1,
+                      2
+                    ]
+                  }
+                ],
+                "behalf": "on behalf of yieldlab",
+                "paid": "by yieldlab"
+              }
+            },
+            "id": "12345",
+            "impid": "test-imp-id",
+            "price": 2.01,
+            "w": 728,
+            "h": 90
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/yieldlab/yieldlabtest/supplemental/dsa_empty.json b/adapters/yieldlab/yieldlabtest/supplemental/dsa_empty.json
new file mode 100644
index 00000000..594e5bb4
--- /dev/null
+++ b/adapters/yieldlab/yieldlabtest/supplemental/dsa_empty.json
@@ -0,0 +1,127 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-with-empty-DSA",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 728,
+              "h": 90
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "adslotId": "12345",
+            "supplyId": "123456789",
+            "targeting": {
+              "key1": "value1",
+              "key2": "value2"
+            },
+            "extId": "abc"
+          }
+        }
+      }
+    ],
+    "device": {
+      "ifa": "hello-ads",
+      "devicetype": 4,
+      "connectiontype": 6,
+      "geo": {
+        "lat": 51.499488,
+        "lon": -0.128953
+      },
+      "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36",
+      "ip": "169.254.13.37",
+      "h": 1098,
+      "w": 814
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 1,
+        "dsa": {
+        }
+      }
+    },
+    "site": {
+      "id": "fake-site-id",
+      "publisher": {
+        "id": "1"
+      },
+      "page": "http://localhost:9090/gdpr.html"
+    },
+    "user": {
+      "buyeruid": "34a53e82-0dc3-4815-8b7e-b725ede0361c",
+      "ext": {
+        "consent": "BOlOrv1OlOr2EAAABADECg-AAAApp7v______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-3zd4u_1vf99yfm1-7etr3tp_87ues2_Xur__79__3z3_9phP78k89r7337Ew-v02"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "headers": {
+          "Accept": [
+            "application/json"
+          ],
+          "Cookie": [
+            "id=34a53e82-0dc3-4815-8b7e-b725ede0361c"
+          ],
+          "Referer": [
+            "http://localhost:9090/gdpr.html"
+          ],
+          "User-Agent": [
+            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36"
+          ],
+          "X-Forwarded-For": [
+            "169.254.13.37"
+          ]
+        },
+        "uri": "https://ad.yieldlab.net/testing/12345?consent=BOlOrv1OlOr2EAAABADECg-AAAApp7v______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-3zd4u_1vf99yfm1-7etr3tp_87ues2_Xur__79__3z3_9phP78k89r7337Ew-v02&content=json&gdpr=1&ids=ylid%3A34a53e82-0dc3-4815-8b7e-b725ede0361c&lat=51.499488&lon=-0.128953&pvid=true&sizes=12345%3A728x90&t=key1%3Dvalue1%26key2%3Dvalue2&ts=testing&yl_rtb_connectiontype=6&yl_rtb_devicetype=4&yl_rtb_ifa=hello-ads"
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": [
+          {
+            "id": 12345,
+            "price": 201,
+            "advertiser": "yieldlab",
+            "adsize": "728x90",
+            "pid": 1234,
+            "did": 5678,
+            "pvid": "40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5",
+            "dsa": {
+            }
+          }
+        ]
+
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "EUR",
+      "bids": [
+        {
+          "bid": {
+            "adm": "<script src=\"https://ad.yieldlab.net/d/12345/123456789/728x90?consent=BOlOrv1OlOr2EAAABADECg-AAAApp7v______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-3zd4u_1vf99yfm1-7etr3tp_87ues2_Xur__79__3z3_9phP78k89r7337Ew-v02&gdpr=1&id=abc&ids=ylid%3A34a53e82-0dc3-4815-8b7e-b725ede0361c&pvid=40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5&ts=testing\"></script>",
+            "crid": "12345123433",
+            "dealid": "1234",
+            "ext": {
+              "dsa": {
+              }
+            },
+            "id": "12345",
+            "impid": "test-imp-id",
+            "price": 2.01,
+            "w": 728,
+            "h": 90
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/yieldlab/yieldlabtest/supplemental/dsa_empty_transparency.json b/adapters/yieldlab/yieldlabtest/supplemental/dsa_empty_transparency.json
new file mode 100644
index 00000000..c966a716
--- /dev/null
+++ b/adapters/yieldlab/yieldlabtest/supplemental/dsa_empty_transparency.json
@@ -0,0 +1,139 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-with-empty-DSA-transparency",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 728,
+              "h": 90
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "adslotId": "12345",
+            "supplyId": "123456789",
+            "targeting": {
+              "key1": "value1",
+              "key2": "value2"
+            },
+            "extId": "abc"
+          }
+        }
+      }
+    ],
+    "device": {
+      "ifa": "hello-ads",
+      "devicetype": 4,
+      "connectiontype": 6,
+      "geo": {
+        "lat": 51.499488,
+        "lon": -0.128953
+      },
+      "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36",
+      "ip": "169.254.13.37",
+      "h": 1098,
+      "w": 814
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 1,
+        "dsa": {
+          "transparency": [
+            {
+            }
+          ]
+        }
+      }
+    },
+    "site": {
+      "id": "fake-site-id",
+      "publisher": {
+        "id": "1"
+      },
+      "page": "http://localhost:9090/gdpr.html"
+    },
+    "user": {
+      "buyeruid": "34a53e82-0dc3-4815-8b7e-b725ede0361c",
+      "ext": {
+        "consent": "BOlOrv1OlOr2EAAABADECg-AAAApp7v______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-3zd4u_1vf99yfm1-7etr3tp_87ues2_Xur__79__3z3_9phP78k89r7337Ew-v02"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "headers": {
+          "Accept": [
+            "application/json"
+          ],
+          "Cookie": [
+            "id=34a53e82-0dc3-4815-8b7e-b725ede0361c"
+          ],
+          "Referer": [
+            "http://localhost:9090/gdpr.html"
+          ],
+          "User-Agent": [
+            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36"
+          ],
+          "X-Forwarded-For": [
+            "169.254.13.37"
+          ]
+        },
+        "uri": "https://ad.yieldlab.net/testing/12345?consent=BOlOrv1OlOr2EAAABADECg-AAAApp7v______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-3zd4u_1vf99yfm1-7etr3tp_87ues2_Xur__79__3z3_9phP78k89r7337Ew-v02&content=json&gdpr=1&ids=ylid%3A34a53e82-0dc3-4815-8b7e-b725ede0361c&lat=51.499488&lon=-0.128953&pvid=true&sizes=12345%3A728x90&t=key1%3Dvalue1%26key2%3Dvalue2&ts=testing&yl_rtb_connectiontype=6&yl_rtb_devicetype=4&yl_rtb_ifa=hello-ads"
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": [
+          {
+            "id": 12345,
+            "price": 201,
+            "advertiser": "yieldlab",
+            "adsize": "728x90",
+            "pid": 1234,
+            "did": 5678,
+            "pvid": "40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5",
+            "dsa": {
+              "transparency": [
+                {
+                }
+              ]
+            }
+          }
+        ]
+
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "EUR",
+      "bids": [
+        {
+          "bid": {
+            "adm": "<script src=\"https://ad.yieldlab.net/d/12345/123456789/728x90?consent=BOlOrv1OlOr2EAAABADECg-AAAApp7v______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-3zd4u_1vf99yfm1-7etr3tp_87ues2_Xur__79__3z3_9phP78k89r7337Ew-v02&gdpr=1&id=abc&ids=ylid%3A34a53e82-0dc3-4815-8b7e-b725ede0361c&pvid=40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5&ts=testing\"></script>",
+            "crid": "12345123433",
+            "dealid": "1234",
+            "ext": {
+              "dsa": {
+                "transparency": [
+                  {
+                  }
+                ]
+              }
+            },
+            "id": "12345",
+            "impid": "test-imp-id",
+            "price": 2.01,
+            "w": 728,
+            "h": 90
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/yieldlab/yieldlabtest/supplemental/invalid_reg_ext.json b/adapters/yieldlab/yieldlabtest/supplemental/invalid_reg_ext.json
new file mode 100644
index 00000000..1723b8fb
--- /dev/null
+++ b/adapters/yieldlab/yieldlabtest/supplemental/invalid_reg_ext.json
@@ -0,0 +1,67 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-with-wrong-DSA-type",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 728,
+              "h": 90
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "adslotId": "12345",
+            "supplyId": "123456789",
+            "targeting": {
+              "key1": "value1",
+              "key2": "value2"
+            },
+            "extId": "abc"
+          }
+        }
+      }
+    ],
+    "device": {
+      "ifa": "hello-ads",
+      "devicetype": 4,
+      "connectiontype": 6,
+      "geo": {
+        "lat": 51.499488,
+        "lon": -0.128953
+      },
+      "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36",
+      "ip": "169.254.13.37",
+      "h": 1098,
+      "w": 814
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 1,
+        "DSA": ""
+      }
+    },
+    "site": {
+      "id": "fake-site-id",
+      "publisher": {
+        "id": "1"
+      },
+      "page": "http://localhost:9090/gdpr.html"
+    },
+    "user": {
+      "buyeruid": "34a53e82-0dc3-4815-8b7e-b725ede0361c",
+      "ext": {
+        "consent": "BOlOrv1OlOr2EAAABADECg-AAAApp7v______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-3zd4u_1vf99yfm1-7etr3tp_87ues2_Xur__79__3z3_9phP78k89r7337Ew-v02"
+      }
+    }
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "failed to parse Regs.Ext object from Yieldlab response: json: cannot unmarshal string into Go struct field openRTBExtRegsWithDSA.dsa of type yieldlab.dsaRequest",
+      "comparison": "literal"
+    }
+  ]
+}
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/auction_test.go b/exchange/auction_test.go
index 40b1aba9..17224dcd 100644
--- a/exchange/auction_test.go
+++ b/exchange/auction_test.go
@@ -191,7 +191,7 @@ func loadCacheSpec(filename string) (*cacheSpec, error) {
 func runCacheSpec(t *testing.T, fileDisplayName string, specData *cacheSpec) {
 	var bid *entities.PbsOrtbBid
 	winningBidsByImp := make(map[string]*entities.PbsOrtbBid)
-	winningBidsByBidder := make(map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid)
+	allBidsByBidder := make(map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid)
 	roundedPrices := make(map[*entities.PbsOrtbBid]string)
 	bidCategory := make(map[string]string)
 
@@ -210,15 +210,15 @@ func runCacheSpec(t *testing.T, fileDisplayName string, specData *cacheSpec) {
 		}
 
 		// Map this bid if it's the highest we've seen from this bidder so far
-		if bidMap, ok := winningBidsByBidder[bid.Bid.ImpID]; ok {
+		if bidMap, ok := allBidsByBidder[bid.Bid.ImpID]; ok {
 			bidMap[pbsBid.Bidder] = append(bidMap[pbsBid.Bidder], bid)
 		} else {
-			winningBidsByBidder[bid.Bid.ImpID] = map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+			allBidsByBidder[bid.Bid.ImpID] = map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				pbsBid.Bidder: {bid},
 			}
 		}
 
-		for _, topBidsPerBidder := range winningBidsByBidder {
+		for _, topBidsPerBidder := range allBidsByBidder {
 			for _, topBids := range topBidsPerBidder {
 				sort.Slice(topBids, func(i, j int) bool {
 					return isNewWinningBid(topBids[i].Bid, topBids[j].Bid, true)
@@ -263,9 +263,9 @@ func runCacheSpec(t *testing.T, fileDisplayName string, specData *cacheSpec) {
 	}
 
 	testAuction := &auction{
-		winningBids:         winningBidsByImp,
-		winningBidsByBidder: winningBidsByBidder,
-		roundedPrices:       roundedPrices,
+		winningBids:     winningBidsByImp,
+		allBidsByBidder: allBidsByBidder,
+		roundedPrices:   roundedPrices,
 	}
 	evTracking := &eventTracking{
 		accountID:          "TEST_ACC_ID",
@@ -405,7 +405,7 @@ func TestNewAuction(t *testing.T) {
 				winningBids: map[string]*entities.PbsOrtbBid{
 					"imp1": &bid1p230,
 				},
-				winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+				allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 					"imp1": {
 						"appnexus": []*entities.PbsOrtbBid{&bid1p123},
 						"rubicon":  []*entities.PbsOrtbBid{&bid1p230},
@@ -433,7 +433,7 @@ func TestNewAuction(t *testing.T) {
 					"imp1": &bid1p230,
 					"imp2": &bid2p144,
 				},
-				winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+				allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 					"imp1": {
 						"appnexus": []*entities.PbsOrtbBid{&bid1p230},
 						"rubicon":  []*entities.PbsOrtbBid{&bid1p077},
@@ -462,7 +462,7 @@ func TestNewAuction(t *testing.T) {
 				winningBids: map[string]*entities.PbsOrtbBid{
 					"imp1": &bid1p123,
 				},
-				winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+				allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 					"imp1": {
 						"appnexus": []*entities.PbsOrtbBid{&bid1p123},
 						"rubicon":  []*entities.PbsOrtbBid{&bid1p088d},
@@ -486,7 +486,7 @@ func TestNewAuction(t *testing.T) {
 				winningBids: map[string]*entities.PbsOrtbBid{
 					"imp1": &bid1p088d,
 				},
-				winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+				allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 					"imp1": {
 						"appnexus": []*entities.PbsOrtbBid{&bid1p123},
 						"rubicon":  []*entities.PbsOrtbBid{&bid1p088d},
@@ -510,7 +510,7 @@ func TestNewAuction(t *testing.T) {
 				winningBids: map[string]*entities.PbsOrtbBid{
 					"imp1": &bid1p166d,
 				},
-				winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+				allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 					"imp1": {
 						"appnexus": []*entities.PbsOrtbBid{&bid1p166d},
 						"rubicon":  []*entities.PbsOrtbBid{&bid1p088d},
@@ -537,7 +537,7 @@ func TestNewAuction(t *testing.T) {
 				winningBids: map[string]*entities.PbsOrtbBid{
 					"imp1": &bid1p166d,
 				},
-				winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+				allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 					"imp1": {
 						"appnexus": []*entities.PbsOrtbBid{&bid1p166d},
 						"rubicon":  []*entities.PbsOrtbBid{&bid1p088d},
@@ -563,7 +563,7 @@ func TestNewAuction(t *testing.T) {
 					"imp1": &bid1p166d,
 					"imp2": &bid2p166,
 				},
-				winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+				allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 					"imp1": {
 						"appnexus": []*entities.PbsOrtbBid{&bid1p166d, &bid1p077},
 						"pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123},
@@ -645,11 +645,11 @@ func TestValidateAndUpdateMultiBid(t *testing.T) {
 	}
 
 	type fields struct {
-		winningBids         map[string]*entities.PbsOrtbBid
-		winningBidsByBidder map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid
-		roundedPrices       map[*entities.PbsOrtbBid]string
-		cacheIds            map[*openrtb2.Bid]string
-		vastCacheIds        map[*openrtb2.Bid]string
+		winningBids     map[string]*entities.PbsOrtbBid
+		allBidsByBidder map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid
+		roundedPrices   map[*entities.PbsOrtbBid]string
+		cacheIds        map[*openrtb2.Bid]string
+		vastCacheIds    map[*openrtb2.Bid]string
 	}
 	type args struct {
 		adapterBids            map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid
@@ -657,8 +657,8 @@ func TestValidateAndUpdateMultiBid(t *testing.T) {
 		accountDefaultBidLimit int
 	}
 	type want struct {
-		winningBidsByBidder map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid
-		adapterBids         map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid
+		allBidsByBidder map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid
+		adapterBids     map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid
 	}
 	tests := []struct {
 		description string
@@ -673,7 +673,7 @@ func TestValidateAndUpdateMultiBid(t *testing.T) {
 					"imp1": &bid1p166d,
 					"imp2": &bid2p166,
 				},
-				winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+				allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 					"imp1": {
 						"appnexus": []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077},
 						"pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123},
@@ -697,7 +697,7 @@ func TestValidateAndUpdateMultiBid(t *testing.T) {
 				preferDeals:            true,
 			},
 			want: want{
-				winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+				allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 					"imp1": {
 						"appnexus": []*entities.PbsOrtbBid{&bid1p166d, &bid1p077, &bid1p001},
 						"pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123},
@@ -724,7 +724,7 @@ func TestValidateAndUpdateMultiBid(t *testing.T) {
 					"imp1": &bid1p166d,
 					"imp2": &bid2p166,
 				},
-				winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+				allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 					"imp1": {
 						"appnexus": []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077},
 						"pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123},
@@ -748,7 +748,7 @@ func TestValidateAndUpdateMultiBid(t *testing.T) {
 				preferDeals:            true,
 			},
 			want: want{
-				winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+				allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 					"imp1": {
 						"appnexus": []*entities.PbsOrtbBid{&bid1p166d, &bid1p077, &bid1p001},
 						"pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123},
@@ -775,7 +775,7 @@ func TestValidateAndUpdateMultiBid(t *testing.T) {
 					"imp1": &bid1p166d,
 					"imp2": &bid2p166,
 				},
-				winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+				allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 					"imp1": {
 						"appnexus": []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077},
 						"pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123},
@@ -799,7 +799,7 @@ func TestValidateAndUpdateMultiBid(t *testing.T) {
 				preferDeals:            true,
 			},
 			want: want{
-				winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+				allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 					"imp1": {
 						"appnexus": []*entities.PbsOrtbBid{&bid1p166d, &bid1p077},
 						"pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123},
@@ -823,14 +823,14 @@ func TestValidateAndUpdateMultiBid(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.description, func(t *testing.T) {
 			a := &auction{
-				winningBids:         tt.fields.winningBids,
-				winningBidsByBidder: tt.fields.winningBidsByBidder,
-				roundedPrices:       tt.fields.roundedPrices,
-				cacheIds:            tt.fields.cacheIds,
-				vastCacheIds:        tt.fields.vastCacheIds,
+				winningBids:     tt.fields.winningBids,
+				allBidsByBidder: tt.fields.allBidsByBidder,
+				roundedPrices:   tt.fields.roundedPrices,
+				cacheIds:        tt.fields.cacheIds,
+				vastCacheIds:    tt.fields.vastCacheIds,
 			}
 			a.validateAndUpdateMultiBid(tt.args.adapterBids, tt.args.preferDeals, tt.args.accountDefaultBidLimit)
-			assert.Equal(t, tt.want.winningBidsByBidder, tt.fields.winningBidsByBidder, tt.description)
+			assert.Equal(t, tt.want.allBidsByBidder, tt.fields.allBidsByBidder, tt.description)
 			assert.Equal(t, tt.want.adapterBids, tt.args.adapterBids, tt.description)
 		})
 	}
diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go
index a1662ab7..9f092b7d 100644
--- a/exchange/bidder_test.go
+++ b/exchange/bidder_test.go
@@ -3377,11 +3377,11 @@ func TestGetRequestBody(t *testing.T) {
 			if test.endpointCompression == "GZIP" {
 				assert.Equal(t, "gzip", req.Headers.Get("Content-Encoding"))
 
-				decompressedReqBody, err := decompressGzip(requestBody)
+				decompressedReqBody, err := decompressGzip(requestBody.Bytes())
 				assert.NoError(t, err)
 				assert.Equal(t, test.givenReqBody, decompressedReqBody)
 			} else {
-				assert.Equal(t, test.givenReqBody, requestBody)
+				assert.Equal(t, test.givenReqBody, requestBody.Bytes())
 			}
 		})
 	}
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index ee2e0f6e..44baaf8d 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
@@ -3542,7 +3550,7 @@ func TestApplyDealSupport(t *testing.T) {
 		}
 
 		auc := &auction{
-			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+			allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				"imp_id1": {
 					test.in.bidderName: {&bid},
 				},
@@ -3551,8 +3559,8 @@ func TestApplyDealSupport(t *testing.T) {
 
 		dealErrs := applyDealSupport(bidRequest, auc, bidCategory, nil)
 
-		assert.Equal(t, test.expected.hbPbCatDur, bidCategory[auc.winningBidsByBidder["imp_id1"][test.in.bidderName][0].Bid.ID], test.description)
-		assert.Equal(t, test.expected.dealTierSatisfied, auc.winningBidsByBidder["imp_id1"][test.in.bidderName][0].DealTierSatisfied, "expected.dealTierSatisfied=%v when %v", test.expected.dealTierSatisfied, test.description)
+		assert.Equal(t, test.expected.hbPbCatDur, bidCategory[auc.allBidsByBidder["imp_id1"][test.in.bidderName][0].Bid.ID], test.description)
+		assert.Equal(t, test.expected.dealTierSatisfied, auc.allBidsByBidder["imp_id1"][test.in.bidderName][0].DealTierSatisfied, "expected.dealTierSatisfied=%v when %v", test.expected.dealTierSatisfied, test.description)
 		if len(test.expected.dealErr) > 0 {
 			assert.Containsf(t, dealErrs, errors.New(test.expected.dealErr), "Expected error message not found in deal errors")
 		}
@@ -3593,7 +3601,7 @@ func TestApplyDealSupportMultiBid(t *testing.T) {
 					},
 				},
 				auc: &auction{
-					winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+					allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 						"imp_id1": {
 							openrtb_ext.BidderName("appnexus"): {
 								&entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""},
@@ -3639,7 +3647,7 @@ func TestApplyDealSupportMultiBid(t *testing.T) {
 					},
 				},
 				auc: &auction{
-					winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+					allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 						"imp_id1": {
 							openrtb_ext.BidderName("appnexus"): {
 								&entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""},
@@ -3690,7 +3698,7 @@ func TestApplyDealSupportMultiBid(t *testing.T) {
 					},
 				},
 				auc: &auction{
-					winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+					allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 						"imp_id1": {
 							openrtb_ext.BidderName("appnexus"): {
 								&entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""},
@@ -3729,7 +3737,7 @@ func TestApplyDealSupportMultiBid(t *testing.T) {
 			errs := applyDealSupport(tt.args.bidRequest, tt.args.auc, tt.args.bidCategory, tt.args.multiBid)
 			assert.Equal(t, tt.want.errs, errs)
 
-			for impID, topBidsPerImp := range tt.args.auc.winningBidsByBidder {
+			for impID, topBidsPerImp := range tt.args.auc.allBidsByBidder {
 				for bidder, topBidsPerBidder := range topBidsPerImp {
 					for i, topBid := range topBidsPerBidder {
 						assert.Equal(t, tt.want.expectedHbPbCatDur[impID][bidder.String()][i], tt.args.bidCategory[topBid.Bid.ID], tt.name)
@@ -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: 305,
+							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/exchange/exchangetest/targeting-always-include-deals.json b/exchange/exchangetest/targeting-always-include-deals.json
new file mode 100644
index 00000000..1ffb8088
--- /dev/null
+++ b/exchange/exchangetest/targeting-always-include-deals.json
@@ -0,0 +1,296 @@
+{
+  "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"
+                }
+              }
+            }
+          }
+        },
+        {
+          "id": "imp-id-2",
+          "video": {
+            "mimes": [
+              "video/mp4"
+            ]
+          },
+          "ext": {
+            "prebid": {
+              "bidder": {
+                "appnexus": {
+                  "placementId": 2
+                },
+                "audienceNetwork": {
+                  "placementId": "some-other-placement"
+                }
+              }
+            }
+          }
+        }
+      ],
+      "ext": {
+        "prebid": {
+          "targeting": {
+            "pricegranularity": {
+              "precision": 2,
+              "ranges": [
+                {
+                  "min": 0,
+                  "max": 20,
+                  "increment": 0.1
+                }
+              ]
+            },
+            "includewinners": true,
+            "includebidderkeys": false,
+            "alwaysincludedeals": true
+          }
+        }
+      }
+    }
+  },
+  "outgoingRequests": {
+    "appnexus": {
+      "mockResponse": {
+        "pbsSeatBids": [
+          {
+            "pbsBids": [
+              {
+                "ortbBid": {
+                  "id": "winning-bid",
+                  "impid": "my-imp-id",
+                  "price": 0.71,
+                  "w": 200,
+                  "h": 250,
+                  "crid": "creative-1",
+                  "dealid": "deal-1"
+                },
+                "bidType": "video"
+              },
+              {
+                "ortbBid": {
+                  "id": "losing-bid",
+                  "impid": "my-imp-id",
+                  "price": 0.21,
+                  "w": 200,
+                  "h": 250,
+                  "crid": "creative-2",
+                  "dealid": "deal-2"
+                },
+                "bidType": "video"
+              },
+              {
+                "ortbBid": {
+                  "id": "other-bid",
+                  "impid": "imp-id-2",
+                  "price": 0.61,
+                  "w": 300,
+                  "h": 500,
+                  "crid": "creative-3",
+                  "dealid": "deal-3"
+                },
+                "bidType": "video"
+              }
+            ],
+            "seat": "appnexus"
+          }
+        ]
+      }
+    },
+    "audienceNetwork": {
+      "mockResponse": {
+        "pbsSeatBids": [
+          {
+            "pbsBids": [
+              {
+                "ortbBid": {
+                  "id": "contending-bid",
+                  "impid": "my-imp-id",
+                  "price": 0.51,
+                  "w": 200,
+                  "h": 250,
+                  "crid": "creative-4",
+                  "dealid": "deal-4"
+                },
+                "bidType": "video"
+              },
+              {
+                "ortbBid": {
+                  "id": "losing-bid-aN",
+                  "impid": "imp-id-2",
+                  "price": 0.40,
+                  "w": 200,
+                  "h": 250,
+                  "crid": "creative-5"
+                },
+                "bidType": "video"
+              }
+            ],
+            "seat": "audienceNetwork"
+          }
+        ]
+      }
+    }
+  },
+  "response": {
+    "bids": {
+      "id": "some-request-id",
+      "seatbid": [
+        {
+          "seat": "audienceNetwork",
+          "bid": [
+            {
+              "id": "contending-bid",
+              "impid": "my-imp-id",
+              "price": 0.51,
+              "w": 200,
+              "h": 250,
+              "crid": "creative-4",
+              "dealid": "deal-4",
+              "ext": {
+                "origbidcpm": 0.51,
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "audienceNetwork"
+                  },
+                  "type": "video",
+                  "targeting": {
+                    "hb_bidder_audienceNe": "audienceNetwork",
+                    "hb_cache_host_audien": "www.pbcserver.com",
+                    "hb_cache_path_audien": "/pbcache/endpoint",
+                    "hb_deal_audienceNetw": "deal-4",
+                    "hb_pb_audienceNetwor": "0.50",
+                    "hb_size_audienceNetw": "200x250"
+                  }
+                }
+              }
+            },
+            {
+              "id": "losing-bid-aN",
+              "impid": "imp-id-2",
+              "price": 0.40,
+              "w": 200,
+              "h": 250,
+              "crid": "creative-5",
+              "ext": {
+                "origbidcpm": 0.40,
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "audienceNetwork"
+                  },
+                  "type": "video"
+                }
+              }
+            }
+          ]
+        },
+        {
+          "seat": "appnexus",
+          "bid": [
+            {
+              "id": "winning-bid",
+              "impid": "my-imp-id",
+              "price": 0.71,
+              "w": 200,
+              "h": 250,
+              "crid": "creative-1",
+              "dealid": "deal-1",
+              "ext": {
+                "origbidcpm": 0.71,
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "appnexus"
+                  },
+                  "type": "video",
+                  "targeting": {
+                    "hb_bidder": "appnexus",
+                    "hb_bidder_appnexus": "appnexus",
+                    "hb_cache_host": "www.pbcserver.com",
+                    "hb_cache_host_appnex": "www.pbcserver.com",
+                    "hb_cache_path": "/pbcache/endpoint",
+                    "hb_cache_path_appnex": "/pbcache/endpoint",
+                    "hb_pb": "0.70",
+                    "hb_pb_appnexus": "0.70",
+                    "hb_deal":"deal-1",
+                    "hb_deal_appnexus":"deal-1",
+                    "hb_size": "200x250",
+                    "hb_size_appnexus": "200x250"
+                  }
+                }
+              }
+            },
+            {
+              "id": "losing-bid",
+              "impid": "my-imp-id",
+              "price": 0.21,
+              "w": 200,
+              "h": 250,
+              "crid": "creative-2",
+              "dealid": "deal-2",
+              "ext": {
+                "origbidcpm": 0.21,
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "appnexus"
+                  },
+                  "type": "video"
+                }
+              }
+            },
+            {
+              "id": "other-bid",
+              "impid": "imp-id-2",
+              "price": 0.61,
+              "w": 300,
+              "h": 500,
+              "crid": "creative-3",
+              "dealid": "deal-3",
+              "ext": {
+                "origbidcpm": 0.61,
+                "prebid": {
+                  "meta": {
+                    "adaptercode": "appnexus"
+                  },
+                  "type": "video",
+                  "targeting": {
+                    "hb_bidder": "appnexus",
+                    "hb_bidder_appnexus": "appnexus",
+                    "hb_cache_host": "www.pbcserver.com",
+                    "hb_cache_host_appnex": "www.pbcserver.com",
+                    "hb_cache_path": "/pbcache/endpoint",
+                    "hb_cache_path_appnex": "/pbcache/endpoint",
+                    "hb_deal":"deal-3",
+                    "hb_deal_appnexus":"deal-3",
+                    "hb_pb": "0.60",
+                    "hb_pb_appnexus": "0.60",
+                    "hb_size": "300x500",
+                    "hb_size_appnexus": "300x500"
+                  }
+                }
+              }
+            }
+          ]
+        }
+      ]
+    }
+  }
+}
diff --git a/exchange/targeting_test.go b/exchange/targeting_test.go
index c124178e..e6cc429e 100644
--- a/exchange/targeting_test.go
+++ b/exchange/targeting_test.go
@@ -319,6 +319,11 @@ var bid2p166 *openrtb2.Bid = &openrtb2.Bid{
 	Price: 1.66,
 }
 
+var bid175 *openrtb2.Bid = &openrtb2.Bid{
+	Price:  1.75,
+	DealID: "mydeal2",
+}
+
 var (
 	truncateTargetAttrValue10       int = 10
 	truncateTargetAttrValue5        int = 5
@@ -339,7 +344,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 			includeWinners:   true,
 		},
 		Auction: auction{
-			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+			allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				"ImpId-1": {
 					openrtb_ext.BidderAppnexus: {{
 						Bid:     bid123,
@@ -374,7 +379,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 			includeBidderKeys: true,
 		},
 		Auction: auction{
-			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+			allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				"ImpId-1": {
 					openrtb_ext.BidderAppnexus: {{
 						Bid:     bid123,
@@ -409,6 +414,54 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 		},
 		TruncateTargetAttr: nil,
 	},
+	{
+		Description: "Targeting with alwaysIncludeDeals",
+		TargetData: targetData{
+			priceGranularity:   lookupPriceGranularity("med"),
+			alwaysIncludeDeals: true,
+		},
+		Auction: auction{
+			allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+				"ImpId-1": {
+					openrtb_ext.BidderAppnexus: {{
+						Bid:     bid111,
+						BidType: openrtb_ext.BidTypeBanner,
+					}},
+					openrtb_ext.BidderRubicon: {{
+						Bid:     bid175,
+						BidType: openrtb_ext.BidTypeBanner,
+					}},
+					openrtb_ext.BidderPubmatic: {{
+						Bid:     bid123,
+						BidType: openrtb_ext.BidTypeBanner,
+					}},
+				},
+			},
+		},
+		ExpectedPbsBids: map[string]map[openrtb_ext.BidderName][]ExpectedPbsBid{
+			"ImpId-1": {
+				openrtb_ext.BidderAppnexus: []ExpectedPbsBid{
+					{
+						BidTargets: map[string]string{
+							"hb_bidder_appnexus": "appnexus",
+							"hb_pb_appnexus":     "1.10",
+							"hb_deal_appnexus":   "mydeal",
+						},
+					},
+				},
+				openrtb_ext.BidderRubicon: []ExpectedPbsBid{
+					{
+						BidTargets: map[string]string{
+							"hb_bidder_rubicon": "rubicon",
+							"hb_pb_rubicon":     "1.70",
+							"hb_deal_rubicon":   "mydeal2",
+						},
+					},
+				},
+			},
+		},
+		TruncateTargetAttr: nil,
+	},
 	{
 		Description: "Full basic targeting with hd_format",
 		TargetData: targetData{
@@ -418,7 +471,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 			includeFormat:     true,
 		},
 		Auction: auction{
-			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+			allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				"ImpId-1": {
 					openrtb_ext.BidderAppnexus: {{
 						Bid:     bid123,
@@ -467,7 +520,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 			cachePath:         "cache",
 		},
 		Auction: auction{
-			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+			allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				"ImpId-1": {
 					openrtb_ext.BidderAppnexus: {{
 						Bid:     bid123,
@@ -520,7 +573,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 			includeBidderKeys: true,
 		},
 		Auction: auction{
-			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+			allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				"ImpId-1": {
 					openrtb_ext.BidderAppnexus: {{
 						Bid:     bid123,
@@ -550,7 +603,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 			includeBidderKeys: true,
 		},
 		Auction: auction{
-			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+			allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				"ImpId-1": {
 					openrtb_ext.BidderAppnexus: {{
 						Bid:     bid123,
@@ -592,7 +645,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 			includeBidderKeys: true,
 		},
 		Auction: auction{
-			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+			allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				"ImpId-1": {
 					openrtb_ext.BidderAppnexus: {{
 						Bid:     bid123,
@@ -634,7 +687,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 			includeBidderKeys: true,
 		},
 		Auction: auction{
-			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+			allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				"ImpId-1": {
 					openrtb_ext.BidderAppnexus: {{
 						Bid:     bid123,
@@ -676,7 +729,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 			includeWinners:   true,
 		},
 		Auction: auction{
-			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+			allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				"ImpId-1": {
 					openrtb_ext.BidderAppnexus: {{
 						Bid:     bid123,
@@ -711,7 +764,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 			includeWinners:   true,
 		},
 		Auction: auction{
-			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+			allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				"ImpId-1": {
 					openrtb_ext.BidderAppnexus: {{
 						Bid:     bid123,
@@ -746,7 +799,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 			includeWinners:   true,
 		},
 		Auction: auction{
-			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+			allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				"ImpId-1": {
 					openrtb_ext.BidderAppnexus: {{
 						Bid:     bid123,
@@ -783,7 +836,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{
 			includeFormat:     true,
 		},
 		Auction: auction{
-			winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
+			allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 				"ImpId-1": {
 					openrtb_ext.BidderAppnexus: {
 						{
@@ -918,7 +971,7 @@ func TestSetTargeting(t *testing.T) {
 		auc.setRoundedPrices(test.TargetData)
 		winningBids := make(map[string]*entities.PbsOrtbBid)
 		// Set winning bids from the auction data
-		for imp, bidsByBidder := range auc.winningBidsByBidder {
+		for imp, bidsByBidder := range auc.allBidsByBidder {
 			for _, bids := range bidsByBidder {
 				for _, bid := range bids {
 					if winningBid, ok := winningBids[imp]; ok {
@@ -939,12 +992,12 @@ func TestSetTargeting(t *testing.T) {
 				for i, expected := range expectedTargets {
 					assert.Equal(t,
 						expected.BidTargets,
-						auc.winningBidsByBidder[imp][bidder][i].BidTargets,
+						auc.allBidsByBidder[imp][bidder][i].BidTargets,
 						"Test: %s\nTargeting failed for bidder %s on imp %s.",
 						test.Description,
 						string(bidder),
 						imp)
-					assert.Equal(t, expected.TargetBidderCode, auc.winningBidsByBidder[imp][bidder][i].TargetBidderCode)
+					assert.Equal(t, expected.TargetBidderCode, auc.allBidsByBidder[imp][bidder][i].TargetBidderCode)
 				}
 			}
 		}
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)
+}