Repository: prebid/prebid-server
Pull request #2710 (2023-04-20 20:27:27)

Richaudience: Shared memory fix
Adapters receive a shallow copy of a request In MakeBids. It means this request has shared pointer values: site, app and other pointer structures are shared between all adapters in request. In order to modify pointer structures you need to create own copy of it and set it back to request. 
The reason why previous unit tests didn't show it is because `imp.tagid` was empty and result `app/site.keywords` didn't change. 
Please review. @richaudience 


Patch:
diff --git a/adapters/axis/axis.go b/adapters/axis/axis.go
new file mode 100644
index 00000000..b9901c6c
--- /dev/null
+++ b/adapters/axis/axis.go
@@ -0,0 +1,162 @@
+package axis
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"text/template"
+
+	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/macros"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+type adapter struct {
+	endpoint *template.Template
+}
+
+type reqBodyExt struct {
+	AxisBidderExt openrtb_ext.ImpExtAxis `json:"bidder"`
+}
+
+func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
+	template, err := template.New("endpointTemplate").Parse(config.Endpoint)
+	if err != nil {
+		return nil, fmt.Errorf("unable to parse endpoint url template: %v", err)
+	}
+
+	bidder := &adapter{
+		endpoint: template,
+	}
+
+	return bidder, nil
+}
+
+func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	var adapterRequests []*adapters.RequestData
+
+	originalImpSlice := request.Imp
+
+	for i := range request.Imp {
+		currImp := originalImpSlice[i]
+		request.Imp = []openrtb2.Imp{currImp}
+
+		var bidderExt reqBodyExt
+		if err := json.Unmarshal(currImp.Ext, &bidderExt); err != nil {
+			continue
+		}
+
+		url, err := a.buildEndpointURL(&bidderExt)
+		if err != nil {
+			return nil, []error{err}
+		}
+
+		extJson, err := json.Marshal(bidderExt)
+		if err != nil {
+			return nil, []error{err}
+		}
+
+		request.Imp[0].Ext = extJson
+
+		adapterReq, err := buildRequest(request, url)
+		if err != nil {
+			return nil, []error{err}
+		}
+
+		if adapterReq != nil {
+			adapterRequests = append(adapterRequests, adapterReq)
+		}
+	}
+	request.Imp = originalImpSlice
+	return adapterRequests, nil
+}
+
+func (a *adapter) buildEndpointURL(bidderExt *reqBodyExt) (string, error) {
+	endpointParams := macros.EndpointTemplateParams{
+		AccountID: bidderExt.AxisBidderExt.Integration,
+		SourceId:  bidderExt.AxisBidderExt.Token,
+	}
+
+	return macros.ResolveMacros(a.endpoint, endpointParams)
+}
+
+func buildRequest(request *openrtb2.BidRequest, url string) (*adapters.RequestData, error) {
+	reqJSON, err := json.Marshal(request)
+	if err != nil {
+		return nil, err
+	}
+
+	headers := http.Header{}
+	headers.Add("Content-Type", "application/json;charset=utf-8")
+	headers.Add("Accept", "application/json")
+
+	return &adapters.RequestData{
+		Method:  "POST",
+		Uri:     url,
+		Body:    reqJSON,
+		Headers: headers,
+	}, nil
+}
+
+func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	if responseData.StatusCode == http.StatusNoContent {
+		return nil, nil
+	}
+
+	if responseData.StatusCode != http.StatusOK {
+		err := &errortypes.BadServerResponse{
+			Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info.", responseData.StatusCode),
+		}
+		return nil, []error{err}
+	}
+
+	var response openrtb2.BidResponse
+	if err := json.Unmarshal(responseData.Body, &response); err != nil {
+		return nil, []error{err}
+	}
+
+	bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
+	bidResponse.Currency = response.Cur
+
+	impsMappedByID := make(map[string]openrtb2.Imp, len(request.Imp))
+	for i, imp := range request.Imp {
+		impsMappedByID[request.Imp[i].ID] = imp
+	}
+
+	for _, seatBid := range response.SeatBid {
+		for i := range seatBid.Bid {
+			bidType, err := getMediaTypeForImp(seatBid.Bid[i].ImpID, impsMappedByID)
+			if err != nil {
+				return nil, []error{err}
+			}
+
+			b := &adapters.TypedBid{
+				Bid:     &seatBid.Bid[i],
+				BidType: bidType,
+			}
+			bidResponse.Bids = append(bidResponse.Bids, b)
+		}
+	}
+	return bidResponse, nil
+}
+
+func getMediaTypeForImp(impID string, impMap map[string]openrtb2.Imp) (openrtb_ext.BidType, error) {
+	if index, found := impMap[impID]; found {
+		if index.Banner != nil {
+			return openrtb_ext.BidTypeBanner, nil
+		}
+		if index.Video != nil {
+			return openrtb_ext.BidTypeVideo, nil
+		}
+		if index.Native != nil {
+			return openrtb_ext.BidTypeNative, nil
+		}
+	}
+
+	return "", &errortypes.BadInput{
+		Message: fmt.Sprintf("Failed to find impression \"%s\"", impID),
+	}
+}
diff --git a/adapters/bidder.go b/adapters/bidder.go
index bb7424f3..28fb555d 100644
--- a/adapters/bidder.go
+++ b/adapters/bidder.go
@@ -4,6 +4,7 @@ import (
 	"encoding/base64"
 	"encoding/json"
 	"net/http"
+	"time"
 
 	"github.com/prebid/openrtb/v19/openrtb2"
 	"github.com/prebid/prebid-server/config"
@@ -145,8 +146,15 @@ func (r *RequestData) SetBasicAuth(username string, password string) {
 
 type ExtraRequestInfo struct {
 	PbsEntryPoint              metrics.RequestType
+	BidderRequestStartTime     time.Time
 	GlobalPrivacyControlHeader string
 	CurrencyConversions        currency.Conversions
+	MakeBidsTimeInfo           MakeBidsTimeInfo
+}
+
+type MakeBidsTimeInfo struct {
+	AfterMakeBidsStartTime time.Time
+	Durations              []time.Duration
 }
 
 func NewExtraRequestInfo(c currency.Conversions) ExtraRequestInfo {
diff --git a/adapters/bidstack/bidstack.go b/adapters/bidstack/bidstack.go
index a1a4e6ec..c5c7f5b4 100644
--- a/adapters/bidstack/bidstack.go
+++ b/adapters/bidstack/bidstack.go
@@ -37,7 +37,7 @@ func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (
 	return bidder, nil
 }
 
-func (a adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
 	headers, err := prepareHeaders(request)
 	if err != nil {
 		return nil, []error{fmt.Errorf("headers prepare: %v", err)}
diff --git a/adapters/conversant/conversant.go b/adapters/conversant/conversant.go
index 55e6e2d1..ec4d2078 100644
--- a/adapters/conversant/conversant.go
+++ b/adapters/conversant/conversant.go
@@ -17,7 +17,7 @@ type ConversantAdapter struct {
 	URI string
 }
 
-func (c ConversantAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+func (c *ConversantAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
 	for i := 0; i < len(request.Imp); i++ {
 		var bidderExt adapters.ExtImpBidder
 		if err := json.Unmarshal(request.Imp[i].Ext, &bidderExt); err != nil {
@@ -132,7 +132,7 @@ func parseCnvrParams(imp *openrtb2.Imp, cnvrExt openrtb_ext.ExtImpConversant) {
 	}
 }
 
-func (c ConversantAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+func (c *ConversantAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
 	if response.StatusCode == http.StatusNoContent {
 		return nil, nil // no bid response
 	}
diff --git a/adapters/richaudience/richaudience.go b/adapters/richaudience/richaudience.go
index 71356bf2..16fbe229 100644
--- a/adapters/richaudience/richaudience.go
+++ b/adapters/richaudience/richaudience.go
@@ -52,10 +52,16 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapte
 		}
 
 		if request.App != nil {
+			appCopy := *request.App
+			request.App = &appCopy
+
 			request.App.Keywords = "tagid=" + imp.TagID
 		}
 
 		if request.Site != nil {
+			siteCopy := *request.Site
+			request.Site = &siteCopy
+
 			request.Site.Keywords = "tagid=" + imp.TagID
 		}
 
@@ -65,6 +71,13 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapte
 			}
 
 			if raiExt.Test {
+				if request.Device != nil {
+					deviceCopy := *request.Device
+					request.Device = &deviceCopy
+				} else {
+					request.Device = &openrtb2.Device{}
+				}
+
 				request.Device.IP = "11.222.33.44"
 				request.Test = int8(1)
 			}
diff --git a/adapters/sharethrough/sharethrough.go b/adapters/sharethrough/sharethrough.go
index 70045cc7..8ec31fe9 100644
--- a/adapters/sharethrough/sharethrough.go
+++ b/adapters/sharethrough/sharethrough.go
@@ -28,7 +28,7 @@ func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server co
 	return bidder, nil
 }
 
-func (a adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
 	var requests []*adapters.RequestData
 	var errors []error
 
@@ -103,7 +103,7 @@ func (a adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.Ex
 	return requests, errors
 }
 
-func (a adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
 	if response.StatusCode == http.StatusNoContent {
 		return nil, nil
 	}
diff --git a/adapters/vidoomy/vidoomy.go b/adapters/vidoomy/vidoomy.go
index 74f4a543..7e7e9d64 100644
--- a/adapters/vidoomy/vidoomy.go
+++ b/adapters/vidoomy/vidoomy.go
@@ -17,7 +17,7 @@ type adapter struct {
 	endpoint string
 }
 
-func (a adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
 	var errors []error
 
 	reqs := make([]*adapters.RequestData, 0, len(request.Imp))
@@ -103,7 +103,7 @@ func changeRequestForBidService(request *openrtb2.BidRequest) error {
 	return nil
 }
 
-func (a adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
 	if response.StatusCode == http.StatusNoContent {
 		return nil, nil
 	}
diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go
index 8634710e..fb9af8ca 100644
--- a/endpoints/openrtb2/amp_auction.go
+++ b/endpoints/openrtb2/amp_auction.go
@@ -221,7 +221,7 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
 
 	secGPC := r.Header.Get("Sec-GPC")
 
-	auctionRequest := exchange.AuctionRequest{
+	auctionRequest := &exchange.AuctionRequest{
 		BidRequestWrapper:          reqWrapper,
 		Account:                    *account,
 		UserSyncs:                  usersyncs,
@@ -266,6 +266,9 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
 	}
 
 	labels, ao = sendAmpResponse(w, hookExecutor, response, reqWrapper, account, labels, ao, errL)
+	if len(ao.Errors) == 0 {
+		recordResponsePreparationMetrics(auctionRequest.MakeBidsTimeInfo, deps.metricsEngine)
+	}
 }
 
 func rejectAmpRequest(
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index 813cbe81..2811a9e7 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -24,6 +24,7 @@ import (
 	nativeRequests "github.com/prebid/openrtb/v19/native1/request"
 	"github.com/prebid/openrtb/v19/openrtb2"
 	"github.com/prebid/openrtb/v19/openrtb3"
+	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/hooks"
 	"github.com/prebid/prebid-server/ortb"
 	"golang.org/x/net/publicsuffix"
@@ -213,7 +214,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 
 	warnings := errortypes.WarningOnly(errL)
 
-	auctionRequest := exchange.AuctionRequest{
+	auctionRequest := &exchange.AuctionRequest{
 		BidRequestWrapper:          req,
 		Account:                    *account,
 		UserSyncs:                  usersyncs,
@@ -252,6 +253,9 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 	}
 
 	labels, ao = sendAuctionResponse(w, hookExecutor, response, req.BidRequest, account, labels, ao)
+	if len(ao.Errors) == 0 {
+		recordResponsePreparationMetrics(auctionRequest.MakeBidsTimeInfo, deps.metricsEngine)
+	}
 }
 
 func rejectAuctionRequest(
@@ -1666,11 +1670,9 @@ func (deps *endpointDeps) validateUser(req *openrtb_ext.RequestWrapper, aliases
 	// Check Universal User ID
 	eids := userExt.GetEid()
 	if eids != nil {
-		if len(*eids) == 0 {
-			return append(errL, errors.New("request.user.ext.eids must contain at least one element or be undefined"))
-		}
-		uniqueSources := make(map[string]struct{}, len(*eids))
-		for eidIndex, eid := range *eids {
+		eidsValue := *eids
+		uniqueSources := make(map[string]struct{}, len(eidsValue))
+		for eidIndex, eid := range eidsValue {
 			if eid.Source == "" {
 				return append(errL, fmt.Errorf("request.user.ext.eids[%d] missing required field: \"source\"", eidIndex))
 			}
@@ -1706,6 +1708,7 @@ func validateRegs(req *openrtb_ext.RequestWrapper, gpp gpplib.GppContainer) []er
 		for _, id := range req.BidRequest.Regs.GPPSID {
 			if id == int8(constants.SectionTCFEU2) {
 				gdpr = 1
+				break
 			}
 		}
 		if gdpr != *req.BidRequest.Regs.GDPR {
@@ -2326,3 +2329,13 @@ func validateStoredBidRespAndImpExtBidders(bidderExts map[string]json.RawMessage
 func generateStoredBidResponseValidationError(impID string) error {
 	return fmt.Errorf("request validation failed. Stored bid responses are specified for imp %s. Bidders specified in imp.ext should match with bidders specified in imp.ext.prebid.storedbidresponse", impID)
 }
+
+func recordResponsePreparationMetrics(mbti map[openrtb_ext.BidderName]adapters.MakeBidsTimeInfo, me metrics.MetricsEngine) {
+	for _, info := range mbti {
+		duration := time.Since(info.AfterMakeBidsStartTime)
+		for _, makeBidsDuration := range info.Durations {
+			duration += makeBidsDuration
+		}
+		me.RecordOverheadTime(metrics.MakeAuctionResponse, duration)
+	}
+}
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-empty.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-empty.json
deleted file mode 100644
index 0baf9895..00000000
--- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-eids-empty.json
+++ /dev/null
@@ -1,39 +0,0 @@
-{
-  "description": "Bid request with empty eids array in user.ext",
-  "mockBidRequest": {
-    "id": "anyRequestID",
-    "site": {
-      "page": "prebid.org",
-      "publisher": {
-        "id": "anyPublisher"
-      }
-    },
-    "imp": [{
-      "id": "anyImpID",
-      "ext": {
-        "appnexus": {
-          "placementId": 42
-        }
-      },
-      "banner": {
-        "format": [{
-            "w": 300,
-            "h": 250
-          },
-          {
-            "w": 300,
-            "h": 300
-          }
-        ]
-      }
-    }],
-    "tmax": 1000,
-    "user": {
-      "ext": {
-        "eids": []
-      }
-    }
-  },
-  "expectedReturnCode": 400,
-  "expectedErrorMessage": "Invalid request: request.user.ext.eids must contain at least one element or be undefined\n"
-}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/user-ext-eids-empty.json b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/user-ext-eids-empty.json
new file mode 100644
index 00000000..9f047613
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/user-ext-eids-empty.json
@@ -0,0 +1,56 @@
+{
+    "description": "Bid request with empty eids array in user.ext",
+    "mockBidRequest": {
+        "id": "some-request-id",
+        "site": {
+            "page": "prebid.org"
+        },
+        "imp": [
+            {
+                "id": "some-impression-id",
+                "ext": {
+                    "appnexus": {
+                        "placementId": 123456
+                    }
+                },
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                }
+            }
+        ],
+        "tmax": 1000,
+        "user": {
+            "ext": {
+                "eids": []
+            }
+        }
+    },
+    "expectedBidResponse": {
+        "id": "some-request-id",
+        "seatbid": [
+            {
+                "bid": [
+                    {
+                        "id": "appnexus-bid",
+                        "impid": "some-impression-id",
+                        "price": 0
+                    }
+                ],
+                "seat": "appnexus"
+            }
+        ],
+        "bidid": "test bid id",
+        "cur": "USD",
+        "nbr": 0
+    },
+    "expectedReturnCode": 200
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/empty-gppsid-conflict.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/empty-gppsid-conflict.json
new file mode 100644
index 00000000..9f7c9231
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/empty-gppsid-conflict.json
@@ -0,0 +1,56 @@
+{
+  "description": "Well formed amp request with  conflicting (2.6 gdpr consent vs. GPP) gdpr signals",
+  "mockBidRequest": {
+    "id": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5",
+    "site": {
+      "page": "prebid.org",
+      "publisher": {
+        "id": "a3de7af2-a86a-4043-a77b-c7e86744155e"
+      }
+    },
+    "source": {
+      "tid": "b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5"
+    },
+    "tmax": 1000,
+    "imp": [
+      {
+        "id": "/19968336/header-bid-tag-0",
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          }
+        },
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 300
+            }
+          ]
+        }
+      }
+    ],
+    "regs": {
+      "gpp": "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1NYN",
+      "gpp_sid": [ ],
+      "gdpr": 1,
+      "ext": {
+        "us_privacy": "1YYY"
+      }
+    },
+    "user": {
+      "consent": "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+      "ext": {}
+    }
+  },
+  "expectedBidResponse": {
+      "id":"b9c97a4b-cbc4-483d-b2c4-58a19ed5cfc5",
+      "bidid":"test bid id",
+      "nbr":0
+  },
+  "expectedReturnCode": 200
+}
diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go
index 829b1f88..79ac2614 100644
--- a/endpoints/openrtb2/video_auction.go
+++ b/endpoints/openrtb2/video_auction.go
@@ -297,7 +297,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 	}
 
 	secGPC := r.Header.Get("Sec-GPC")
-	auctionRequest := exchange.AuctionRequest{
+	auctionRequest := &exchange.AuctionRequest{
 		BidRequestWrapper:          bidReqWrapper,
 		Account:                    *account,
 		UserSyncs:                  usersyncs,
@@ -354,6 +354,10 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 		return
 	}
 
+	if len(vo.Errors) == 0 {
+		recordResponsePreparationMetrics(auctionRequest.MakeBidsTimeInfo, deps.metricsEngine)
+	}
+
 	w.Header().Set("Content-Type", "application/json")
 	w.Write(resp)
 
diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go
index 34bb320e..784ae8ae 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -38,6 +38,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/audienceNetwork"
 	"github.com/prebid/prebid-server/adapters/automatad"
 	"github.com/prebid/prebid-server/adapters/avocet"
+	"github.com/prebid/prebid-server/adapters/axis"
 	"github.com/prebid/prebid-server/adapters/axonix"
 	"github.com/prebid/prebid-server/adapters/beachfront"
 	"github.com/prebid/prebid-server/adapters/beintoo"
@@ -212,6 +213,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderAudienceNetwork:   audienceNetwork.Builder,
 		openrtb_ext.BidderAutomatad:         automatad.Builder,
 		openrtb_ext.BidderAvocet:            avocet.Builder,
+		openrtb_ext.BidderAxis:              axis.Builder,
 		openrtb_ext.BidderAxonix:            axonix.Builder,
 		openrtb_ext.BidderBeachfront:        beachfront.Builder,
 		openrtb_ext.BidderBeintoo:           beintoo.Builder,
diff --git a/exchange/bidder.go b/exchange/bidder.go
index f2cf790f..58dc1789 100644
--- a/exchange/bidder.go
+++ b/exchange/bidder.go
@@ -121,9 +121,11 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest Bidde
 		return nil, []error{reject}
 	}
 
-	var reqData []*adapters.RequestData
-	var errs []error
-	var responseChannel chan *httpCallInfo
+	var (
+		reqData         []*adapters.RequestData
+		errs            []error
+		responseChannel chan *httpCallInfo
+	)
 
 	//check if real request exists for this bidder or it only has stored responses
 	dataLen := 0
@@ -169,11 +171,11 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest Bidde
 		dataLen = len(reqData) + len(bidderRequest.BidderStoredResponses)
 		responseChannel = make(chan *httpCallInfo, dataLen)
 		if len(reqData) == 1 {
-			responseChannel <- bidder.doRequest(ctx, reqData[0])
+			responseChannel <- bidder.doRequest(ctx, reqData[0], reqInfo.BidderRequestStartTime)
 		} else {
 			for _, oneReqData := range reqData {
 				go func(data *adapters.RequestData) {
-					responseChannel <- bidder.doRequest(ctx, data)
+					responseChannel <- bidder.doRequest(ctx, data, reqInfo.BidderRequestStartTime)
 				}(oneReqData) // Method arg avoids a race condition on oneReqData
 			}
 		}
@@ -227,6 +229,7 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest Bidde
 		}
 
 		if httpInfo.err == nil {
+			startTime := time.Now()
 			bidResponse, moreErrs := bidder.Bidder.MakeBids(bidderRequest.BidRequest, httpInfo.request, httpInfo.response)
 			errs = append(errs, moreErrs...)
 
@@ -363,11 +366,12 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest Bidde
 					errs = append(errs, err)
 				}
 			}
+			reqInfo.MakeBidsTimeInfo.Durations = append(reqInfo.MakeBidsTimeInfo.Durations, time.Since(startTime))
 		} else {
 			errs = append(errs, httpInfo.err)
 		}
 	}
-
+	reqInfo.MakeBidsTimeInfo.AfterMakeBidsStartTime = time.Now()
 	seatBids := make([]*entities.PbsOrtbSeatBid, 0, len(seatBidMap))
 	for _, seatBid := range seatBidMap {
 		seatBids = append(seatBids, seatBid)
@@ -487,11 +491,11 @@ func makeExt(httpInfo *httpCallInfo) *openrtb_ext.ExtHttpCall {
 
 // doRequest makes a request, handles the response, and returns the data needed by the
 // Bidder interface.
-func (bidder *bidderAdapter) doRequest(ctx context.Context, req *adapters.RequestData) *httpCallInfo {
-	return bidder.doRequestImpl(ctx, req, glog.Warningf)
+func (bidder *bidderAdapter) doRequest(ctx context.Context, req *adapters.RequestData, pbsRequestStartTime time.Time) *httpCallInfo {
+	return bidder.doRequestImpl(ctx, req, glog.Warningf, pbsRequestStartTime)
 }
 
-func (bidder *bidderAdapter) doRequestImpl(ctx context.Context, req *adapters.RequestData, logger util.LogMsg) *httpCallInfo {
+func (bidder *bidderAdapter) doRequestImpl(ctx context.Context, req *adapters.RequestData, logger util.LogMsg, pbsRequestStartTime time.Time) *httpCallInfo {
 	var requestBody []byte
 
 	switch strings.ToUpper(bidder.config.EndpointCompression) {
@@ -515,6 +519,7 @@ func (bidder *bidderAdapter) doRequestImpl(ctx context.Context, req *adapters.Re
 	if !bidder.config.DisableConnMetrics {
 		ctx = bidder.addClientTrace(ctx)
 	}
+	bidder.me.RecordOverheadTime(metrics.PreBidder, time.Since(pbsRequestStartTime))
 	httpResp, err := ctxhttp.Do(ctx, bidder.Client, httpReq)
 	if err != nil {
 		if err == context.DeadlineExceeded {
diff --git a/exchange/exchange.go b/exchange/exchange.go
index fa0ea6af..c3728180 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -47,7 +47,7 @@ type extCacheInstructions struct {
 // Exchange runs Auctions. Implementations must be threadsafe, and will be shared across many goroutines.
 type Exchange interface {
 	// HoldAuction executes an OpenRTB v2.5 Auction.
-	HoldAuction(ctx context.Context, r AuctionRequest, debugLog *DebugLog) (*openrtb2.BidResponse, error)
+	HoldAuction(ctx context.Context, r *AuctionRequest, debugLog *DebugLog) (*openrtb2.BidResponse, error)
 }
 
 // IdFetcher can find the user's ID for a specific Bidder.
@@ -86,7 +86,8 @@ type seatResponseExtra struct {
 	Warnings           []openrtb_ext.ExtBidderMessage
 	// httpCalls is the list of debugging info. It should only be populated if the request.test == 1.
 	// This will become response.ext.debug.httpcalls.{bidder} on the final Response.
-	HttpCalls []*openrtb_ext.ExtHttpCall
+	HttpCalls        []*openrtb_ext.ExtHttpCall
+	MakeBidsTimeInfo adapters.MakeBidsTimeInfo
 }
 
 type bidResponseWrapper struct {
@@ -205,6 +206,8 @@ type AuctionRequest struct {
 	PubID                 string
 	HookExecutor          hookexecution.StageExecutor
 	QueryParams           url.Values
+	// map of bidder to store duration needed for the MakeBids() calls and start time after MakeBids() calls
+	MakeBidsTimeInfo map[openrtb_ext.BidderName]adapters.MakeBidsTimeInfo
 }
 
 // BidderRequest holds the bidder specific request and all other
@@ -218,7 +221,11 @@ type BidderRequest struct {
 	ImpReplaceImpId       map[string]bool
 }
 
-func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *DebugLog) (*openrtb2.BidResponse, error) {
+func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog *DebugLog) (*openrtb2.BidResponse, error) {
+	if r == nil {
+		return nil, nil
+	}
+
 	var floorErrs []error
 	err := r.HookExecutor.ExecuteProcessedAuctionStage(r.BidRequestWrapper)
 	if err != nil {
@@ -307,7 +314,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *
 		Prebid: *requestExtPrebid,
 		SChain: requestExt.GetSChain(),
 	}
-	bidderRequests, privacyLabels, errs := e.requestSplitter.cleanOpenRTBRequests(ctx, r, requestExtLegacy, gdprDefaultValue)
+	bidderRequests, privacyLabels, errs := e.requestSplitter.cleanOpenRTBRequests(ctx, *r, requestExtLegacy, gdprDefaultValue)
 	errs = append(errs, floorErrs...)
 
 	e.me.RecordRequestPrivacy(privacyLabels)
@@ -321,13 +328,14 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *
 	auctionCtx, cancel := e.makeAuctionContext(ctx, cacheInstructions.cacheBids)
 	defer cancel()
 
-	var adapterBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid
-	var adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra
-	var fledge *openrtb_ext.Fledge
-	var anyBidsReturned bool
-
-	// List of bidders we have requests for.
-	var liveAdapters []openrtb_ext.BidderName
+	var (
+		adapterBids     map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid
+		adapterExtra    map[openrtb_ext.BidderName]*seatResponseExtra
+		fledge          *openrtb_ext.Fledge
+		anyBidsReturned bool
+		// List of bidders we have requests for.
+		liveAdapters []openrtb_ext.BidderName
+	)
 
 	if len(r.StoredAuctionResponses) > 0 {
 		adapterBids, fledge, liveAdapters, err = buildStoredAuctionResponse(r.StoredAuctionResponses)
@@ -348,12 +356,16 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *
 			alternateBidderCodes = *r.Account.AlternateBidderCodes
 		}
 
-		adapterBids, adapterExtra, fledge, anyBidsReturned = e.getAllBids(auctionCtx, bidderRequests, bidAdjustmentFactors, conversions, accountDebugAllow, r.GlobalPrivacyControlHeader, debugLog.DebugOverride, alternateBidderCodes, requestExtLegacy.Prebid.Experiment, r.HookExecutor)
+		adapterBids, adapterExtra, fledge, anyBidsReturned = e.getAllBids(auctionCtx, bidderRequests, bidAdjustmentFactors, conversions, accountDebugAllow, r.GlobalPrivacyControlHeader, debugLog.DebugOverride, alternateBidderCodes, requestExtLegacy.Prebid.Experiment, r.HookExecutor, r.StartTime)
+		r.MakeBidsTimeInfo = buildMakeBidsTimeInfoMap(adapterExtra)
 	}
 
-	var auc *auction
-	var cacheErrs []error
-	var bidResponseExt *openrtb_ext.ExtBidResponse
+	var (
+		auc            *auction
+		cacheErrs      []error
+		bidResponseExt *openrtb_ext.ExtBidResponse
+	)
+
 	if anyBidsReturned {
 		var bidCategory map[string]string
 		//If includebrandcategory is present in ext then CE feature is on.
@@ -397,7 +409,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *
 				errs = append(errs, dealErrs...)
 			}
 
-			bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, r, responseDebugAllow, requestExtPrebid.Passthrough, fledge, errs)
+			bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, *r, responseDebugAllow, requestExtPrebid.Passthrough, fledge, errs)
 			if debugLog.DebugEnabledOrOverridden {
 				if bidRespExtBytes, err := json.Marshal(bidResponseExt); err == nil {
 					debugLog.Data.Response = string(bidRespExtBytes)
@@ -414,9 +426,9 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *
 
 			targData.setTargeting(auc, r.BidRequestWrapper.BidRequest.App != nil, bidCategory, r.Account.TruncateTargetAttribute, multiBidMap)
 		}
-		bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, r, responseDebugAllow, requestExtPrebid.Passthrough, fledge, errs)
+		bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, *r, responseDebugAllow, requestExtPrebid.Passthrough, fledge, errs)
 	} else {
-		bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, r, responseDebugAllow, requestExtPrebid.Passthrough, fledge, errs)
+		bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, *r, responseDebugAllow, requestExtPrebid.Passthrough, fledge, errs)
 
 		if debugLog.DebugEnabledOrOverridden {
 
@@ -453,10 +465,17 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *
 	bidResponse = adservertargeting.Apply(r.BidRequestWrapper, r.ResolvedBidRequest, bidResponse, r.QueryParams, bidResponseExt, r.Account.TruncateTargetAttribute)
 
 	bidResponse.Ext, err = encodeBidResponseExt(bidResponseExt)
-
 	return bidResponse, err
 }
 
+func buildMakeBidsTimeInfoMap(adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra) map[openrtb_ext.BidderName]adapters.MakeBidsTimeInfo {
+	makeBidsInfoMap := make(map[openrtb_ext.BidderName]adapters.MakeBidsTimeInfo)
+	for bidderName, responseExtra := range adapterExtra {
+		makeBidsInfoMap[bidderName] = responseExtra.MakeBidsTimeInfo
+	}
+	return makeBidsInfoMap
+}
+
 func buildMultiBidMap(prebid *openrtb_ext.ExtRequestPrebid) map[string]openrtb_ext.ExtMultiBid {
 	if prebid == nil || prebid.MultiBid == nil {
 		return nil
@@ -609,7 +628,8 @@ func (e *exchange) getAllBids(
 	headerDebugAllowed bool,
 	alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes,
 	experiment *openrtb_ext.Experiment,
-	hookExecutor hookexecution.StageExecutor) (
+	hookExecutor hookexecution.StageExecutor,
+	pbsRequestStartTime time.Time) (
 	map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid,
 	map[openrtb_ext.BidderName]*seatResponseExtra,
 	*openrtb_ext.Fledge,
@@ -620,6 +640,8 @@ func (e *exchange) getAllBids(
 	chBids := make(chan *bidResponseWrapper, len(bidderRequests))
 	bidsFound := false
 
+	e.me.RecordOverheadTime(metrics.MakeBidderRequests, time.Since(pbsRequestStartTime))
+
 	for _, bidder := range bidderRequests {
 		// Here we actually call the adapters and collect the bids.
 		bidderRunner := e.recoverSafely(bidderRequests, func(bidderRequest BidderRequest, conversions currency.Conversions) {
@@ -640,6 +662,7 @@ func (e *exchange) getAllBids(
 			reqInfo := adapters.NewExtraRequestInfo(conversions)
 			reqInfo.PbsEntryPoint = bidderRequest.BidderLabels.RType
 			reqInfo.GlobalPrivacyControlHeader = globalPrivacyControlHeader
+			reqInfo.BidderRequestStartTime = start
 
 			bidReqOptions := bidRequestOptions{
 				accountDebugAllowed: accountDebugAllowed,
@@ -658,9 +681,13 @@ func (e *exchange) getAllBids(
 			if len(seatBids) != 0 {
 				ae.HttpCalls = seatBids[0].HttpCalls
 			}
-
+			// SeatBidsPreparationStartTime is needed to calculate duration for openrtb response preparation time metric
+			//  No metric needs to be logged for requests which error out
+			if err == nil {
+				ae.MakeBidsTimeInfo = reqInfo.MakeBidsTimeInfo
+			}
 			// Timing statistics
-			e.me.RecordAdapterTime(bidderRequest.BidderLabels, time.Since(start))
+			e.me.RecordAdapterTime(bidderRequest.BidderLabels, elapsed)
 			bidderRequest.BidderLabels.AdapterBids = bidsToMetric(brw.adapterSeatBids)
 			bidderRequest.BidderLabels.AdapterErrors = errorsToMetric(err)
 			// Append any bid validation errors to the error list
diff --git a/metrics/config/metrics.go b/metrics/config/metrics.go
index cc9f4da0..95e2614d 100644
--- a/metrics/config/metrics.go
+++ b/metrics/config/metrics.go
@@ -170,6 +170,13 @@ func (me *MultiMetricsEngine) RecordAdapterTime(labels metrics.AdapterLabels, le
 	}
 }
 
+// RecordOverheadTime across all engines
+func (me *MultiMetricsEngine) RecordOverheadTime(overhead metrics.OverheadType, length time.Duration) {
+	for _, thisME := range *me {
+		thisME.RecordOverheadTime(overhead, length)
+	}
+}
+
 // RecordCookieSync across all engines
 func (me *MultiMetricsEngine) RecordCookieSync(status metrics.CookieSyncStatus) {
 	for _, thisME := range *me {
@@ -430,6 +437,10 @@ func (me *NilMetricsEngine) RecordAdapterPrice(labels metrics.AdapterLabels, cpm
 func (me *NilMetricsEngine) RecordAdapterTime(labels metrics.AdapterLabels, length time.Duration) {
 }
 
+// RecordOverheadTime as a noop
+func (me *NilMetricsEngine) RecordOverheadTime(overhead metrics.OverheadType, length time.Duration) {
+}
+
 // RecordCookieSync as a noop
 func (me *NilMetricsEngine) RecordCookieSync(status metrics.CookieSyncStatus) {
 }
diff --git a/metrics/go_metrics.go b/metrics/go_metrics.go
index d01bf3ba..44f2ecd7 100644
--- a/metrics/go_metrics.go
+++ b/metrics/go_metrics.go
@@ -77,6 +77,8 @@ type Metrics struct {
 
 	// Module metrics
 	ModuleMetrics map[string]map[string]*ModuleMetrics
+
+	OverheadTimer map[OverheadType]metrics.Timer
 }
 
 // AdapterMetrics houses the metrics for a particular adapter
@@ -216,6 +218,8 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa
 
 		exchanges: exchanges,
 		modules:   getModuleNames(moduleStageNames),
+
+		OverheadTimer: makeBlankOverheadTimerMetrics(),
 	}
 
 	for _, a := range exchanges {
@@ -300,6 +304,7 @@ func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, d
 	newMetrics.PrebidCacheRequestTimerSuccess = metrics.GetOrRegisterTimer("prebid_cache_request_time.ok", registry)
 	newMetrics.PrebidCacheRequestTimerError = metrics.GetOrRegisterTimer("prebid_cache_request_time.err", registry)
 	newMetrics.StoredResponsesMeter = metrics.GetOrRegisterMeter("stored_responses", registry)
+	newMetrics.OverheadTimer = makeOverheadTimerMetrics(registry)
 
 	for _, dt := range StoredDataTypes() {
 		for _, ft := range StoredDataFetchTypes() {
@@ -377,6 +382,15 @@ func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, d
 	return newMetrics
 }
 
+func makeBlankOverheadTimerMetrics() map[OverheadType]metrics.Timer {
+	m := make(map[OverheadType]metrics.Timer)
+	overheads := OverheadTypes()
+	for idx := range overheads {
+		m[overheads[idx]] = &metrics.NilTimer{}
+	}
+	return m
+}
+
 // Part of setting up blank metrics, the adapter metrics.
 func makeBlankAdapterMetrics(disabledMetrics config.DisabledMetrics) *AdapterMetrics {
 	blankMeter := &metrics.NilMeter{}
@@ -443,6 +457,15 @@ func makeBlankMarkupDeliveryMetrics() *MarkupDeliveryMetrics {
 	}
 }
 
+func makeOverheadTimerMetrics(registry metrics.Registry) map[OverheadType]metrics.Timer {
+	m := make(map[OverheadType]metrics.Timer)
+	overheads := OverheadTypes()
+	for idx := range overheads {
+		m[overheads[idx]] = metrics.GetOrRegisterTimer("request_over_head_time."+overheads[idx].String(), registry)
+	}
+	return m
+}
+
 func registerAdapterMetrics(registry metrics.Registry, adapterOrAccount string, exchange string, am *AdapterMetrics) {
 	am.NoCookieMeter = metrics.GetOrRegisterMeter(fmt.Sprintf("%[1]s.%[2]s.no_cookie_requests", adapterOrAccount, exchange), registry)
 	am.NoBidMeter = metrics.GetOrRegisterMeter(fmt.Sprintf("%[1]s.%[2]s.requests.nobid", adapterOrAccount, exchange), registry)
@@ -845,6 +868,11 @@ func (me *Metrics) RecordAdapterTime(labels AdapterLabels, length time.Duration)
 	}
 }
 
+// RecordOverheadTime implements a part of the MetricsEngine interface. Records the adapter overhead time
+func (me *Metrics) RecordOverheadTime(overhead OverheadType, length time.Duration) {
+	me.OverheadTimer[overhead].Update(length)
+}
+
 // RecordCookieSync implements a part of the MetricsEngine interface. Records a cookie sync request
 func (me *Metrics) RecordCookieSync(status CookieSyncStatus) {
 	me.CookieSyncMeter.Mark(1)
diff --git a/metrics/metrics.go b/metrics/metrics.go
index 48543ea4..186e1ffa 100644
--- a/metrics/metrics.go
+++ b/metrics/metrics.go
@@ -26,6 +26,26 @@ type AdapterLabels struct {
 	AdapterErrors map[AdapterError]struct{}
 }
 
+// OverheadType: overhead type enumeration
+type OverheadType string
+
+const (
+	// PreBidder - measures the time needed to execute the adapter's MakeRequests() implementation, build Prebid headers and apply GZip compression if needed
+	PreBidder OverheadType = "pre-bidder"
+	// MakeAuctionResponse - measures the amount of time spent doing all the MakeBids() calls as well as preparing PBS's response
+	MakeAuctionResponse OverheadType = "make-auction-response"
+	// MakeBidderRequests - measures the time needed to fetch a stored request (if needed), parse, unmarshal, and validate the OpenRTB request, interpret its privacy policies, and split it into multiple requests sanitized for each bidder
+	MakeBidderRequests OverheadType = "make-bidder-requests"
+)
+
+func (t OverheadType) String() string {
+	return string(t)
+}
+
+func OverheadTypes() []OverheadType {
+	return []OverheadType{PreBidder, MakeAuctionResponse, MakeBidderRequests}
+}
+
 // ImpLabels defines metric labels describing the impression type.
 type ImpLabels struct {
 	BannerImps bool
@@ -405,6 +425,7 @@ type MetricsEngine interface {
 	RecordRequest(labels Labels)                           // ignores adapter. only statusOk and statusErr fom status
 	RecordImps(labels ImpLabels)                           // RecordImps across openRTB2 engines that support the 'Native' Imp Type
 	RecordRequestTime(labels Labels, length time.Duration) // ignores adapter. only statusOk and statusErr fom status
+	RecordOverheadTime(overHead OverheadType, length time.Duration)
 	RecordAdapterRequest(labels AdapterLabels)
 	RecordAdapterConnections(adapterName openrtb_ext.BidderName, connWasReused bool, connWaitTime time.Duration)
 	RecordDNSTime(dnsLookupTime time.Duration)
diff --git a/metrics/metrics_mock.go b/metrics/metrics_mock.go
index cc73682e..cbca8570 100644
--- a/metrics/metrics_mock.go
+++ b/metrics/metrics_mock.go
@@ -86,6 +86,11 @@ func (me *MetricsEngineMock) RecordAdapterTime(labels AdapterLabels, length time
 	me.Called(labels, length)
 }
 
+// RecordOverheadTime mock
+func (me *MetricsEngineMock) RecordOverheadTime(overhead OverheadType, length time.Duration) {
+	me.Called(overhead, length)
+}
+
 // RecordCookieSync mock
 func (me *MetricsEngineMock) RecordCookieSync(status CookieSyncStatus) {
 	me.Called(status)
diff --git a/metrics/prometheus/preload.go b/metrics/prometheus/preload.go
index 61ae568d..dae7c14d 100644
--- a/metrics/prometheus/preload.go
+++ b/metrics/prometheus/preload.go
@@ -16,6 +16,7 @@ func preloadLabelValues(m *Metrics, syncerKeys []string, moduleStageNames map[st
 		connectionErrorValues     = []string{connectionAcceptError, connectionCloseError}
 		cookieValues              = cookieTypesAsString()
 		cookieSyncStatusValues    = cookieSyncStatusesAsString()
+		overheadTypes             = overheadTypesAsString()
 		requestTypeValues         = requestTypesAsString()
 		requestStatusValues       = requestStatusesAsString()
 		storedDataFetchTypeValues = storedDataFetchTypesAsString()
@@ -187,6 +188,10 @@ func preloadLabelValues(m *Metrics, syncerKeys []string, moduleStageNames map[st
 		adapterLabel: adapterValues,
 	})
 
+	preloadLabelValuesForHistogram(m.overheadTimer, map[string][]string{
+		overheadTypeLabel: overheadTypes,
+	})
+
 	preloadLabelValuesForCounter(m.syncerRequests, map[string][]string{
 		syncerLabel: syncerKeys,
 		statusLabel: syncerRequestStatusValues,
diff --git a/metrics/prometheus/prometheus.go b/metrics/prometheus/prometheus.go
index 14f7f998..ee11dccf 100644
--- a/metrics/prometheus/prometheus.go
+++ b/metrics/prometheus/prometheus.go
@@ -62,6 +62,7 @@ type Metrics struct {
 	adapterPanics                         *prometheus.CounterVec
 	adapterPrices                         *prometheus.HistogramVec
 	adapterRequests                       *prometheus.CounterVec
+	overheadTimer                         *prometheus.HistogramVec
 	adapterRequestsTimer                  *prometheus.HistogramVec
 	adapterReusedConnections              *prometheus.CounterVec
 	adapterCreatedConnections             *prometheus.CounterVec
@@ -129,6 +130,7 @@ const (
 	isVideoLabel         = "video"
 	markupDeliveryLabel  = "delivery"
 	optOutLabel          = "opt_out"
+	overheadTypeLabel    = "overhead_type"
 	privacyBlockedLabel  = "privacy_blocked"
 	requestStatusLabel   = "request_status"
 	requestTypeLabel     = "request_type"
@@ -175,6 +177,7 @@ func NewMetrics(cfg config.PrometheusMetrics, disabledMetrics config.DisabledMet
 	cacheWriteTimeBuckets := []float64{0.001, 0.002, 0.005, 0.01, 0.025, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 1}
 	priceBuckets := []float64{250, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000}
 	queuedRequestTimeBuckets := []float64{0, 1, 5, 30, 60, 120, 180, 240, 300}
+	overheadTimeBuckets := []float64{0.00005, 0.0001, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.05}
 
 	metrics := Metrics{}
 	reg := prometheus.NewRegistry()
@@ -425,6 +428,12 @@ func NewMetrics(cfg config.PrometheusMetrics, disabledMetrics config.DisabledMet
 		"Count that tracks number of bids removed from bid response that had a invalid bidAdm (warn)",
 		[]string{adapterLabel, successLabel})
 
+	metrics.overheadTimer = newHistogramVec(cfg, reg,
+		"overhead_time_seconds",
+		"Seconds to prepare adapter request or resolve adapter response",
+		[]string{overheadTypeLabel},
+		overheadTimeBuckets)
+
 	metrics.adapterRequestsTimer = newHistogramVec(cfg, reg,
 		"adapter_request_time_seconds",
 		"Seconds to resolve each successful request labeled by adapter.",
@@ -908,6 +917,12 @@ func (m *Metrics) RecordAdapterPrice(labels metrics.AdapterLabels, cpm float64)
 	}).Observe(cpm)
 }
 
+func (m *Metrics) RecordOverheadTime(overhead metrics.OverheadType, duration time.Duration) {
+	m.overheadTimer.With(prometheus.Labels{
+		overheadTypeLabel: overhead.String(),
+	}).Observe(duration.Seconds())
+}
+
 func (m *Metrics) RecordAdapterTime(labels metrics.AdapterLabels, length time.Duration) {
 	if len(labels.AdapterErrors) == 0 {
 		m.adapterRequestsTimer.With(prometheus.Labels{
diff --git a/metrics/prometheus/type_conversion.go b/metrics/prometheus/type_conversion.go
index d99f2f6f..07e4e89c 100644
--- a/metrics/prometheus/type_conversion.go
+++ b/metrics/prometheus/type_conversion.go
@@ -77,6 +77,15 @@ func syncerRequestStatusesAsString() []string {
 	return valuesAsString
 }
 
+func overheadTypesAsString() []string {
+	overheadTypes := metrics.OverheadTypes()
+	overheadTypesAsString := make([]string, len(overheadTypes))
+	for i, ot := range overheadTypes {
+		overheadTypesAsString[i] = ot.String()
+	}
+	return overheadTypesAsString
+}
+
 func syncerSetStatusesAsString() []string {
 	values := metrics.SyncerSetUidStatuses()
 	valuesAsString := make([]string, len(values))
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index c2a7d62f..90f80d4f 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -123,6 +123,7 @@ const (
 	BidderAudienceNetwork   BidderName = "audienceNetwork"
 	BidderAutomatad         BidderName = "automatad"
 	BidderAvocet            BidderName = "avocet"
+	BidderAxis              BidderName = "axis"
 	BidderAxonix            BidderName = "axonix"
 	BidderBeachfront        BidderName = "beachfront"
 	BidderBeintoo           BidderName = "beintoo"
@@ -308,6 +309,7 @@ func CoreBidderNames() []BidderName {
 		BidderAudienceNetwork,
 		BidderAutomatad,
 		BidderAvocet,
+		BidderAxis,
 		BidderAxonix,
 		BidderBeachfront,
 		BidderBeintoo,
diff --git a/openrtb_ext/imp_axis.go b/openrtb_ext/imp_axis.go
new file mode 100644
index 00000000..0381e4c5
--- /dev/null
+++ b/openrtb_ext/imp_axis.go
@@ -0,0 +1,6 @@
+package openrtb_ext
+
+type ImpExtAxis struct {
+	Integration string `json:"integration,omitempty"`
+	Token       string `json:"token,omitempty"`
+}
diff --git a/ortb/clone.go b/ortb/clone.go
new file mode 100644
index 00000000..c0e5a4dd
--- /dev/null
+++ b/ortb/clone.go
@@ -0,0 +1,267 @@
+package ortb
+
+import (
+	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/util/ptrutil"
+	"github.com/prebid/prebid-server/util/sliceutil"
+)
+
+func CloneApp(s *openrtb2.App) *openrtb2.App {
+	if s == nil {
+		return nil
+	}
+
+	// Shallow Copy (Value Fields)
+	c := *s
+
+	// Deep Copy (Pointers)
+	c.Cat = sliceutil.Clone(s.Cat)
+	c.SectionCat = sliceutil.Clone(s.SectionCat)
+	c.PageCat = sliceutil.Clone(s.PageCat)
+	c.Publisher = ClonePublisher(s.Publisher)
+	c.Content = CloneContent(s.Content)
+	c.KwArray = sliceutil.Clone(s.KwArray)
+	c.Ext = sliceutil.Clone(s.Ext)
+
+	return &c
+}
+
+func ClonePublisher(s *openrtb2.Publisher) *openrtb2.Publisher {
+	if s == nil {
+		return nil
+	}
+
+	// Shallow Copy (Value Fields)
+	c := *s
+
+	// Deep Copy (Pointers)
+	c.Cat = sliceutil.Clone(s.Cat)
+	c.Ext = sliceutil.Clone(s.Ext)
+
+	return &c
+}
+
+func CloneContent(s *openrtb2.Content) *openrtb2.Content {
+	if s == nil {
+		return nil
+	}
+
+	// Shallow Copy (Value Fields)
+	c := *s
+
+	// Deep Copy (Pointers)
+	c.Producer = CloneProducer(s.Producer)
+	c.Cat = sliceutil.Clone(s.Cat)
+	c.ProdQ = ptrutil.Clone(s.ProdQ)
+	c.VideoQuality = ptrutil.Clone(s.VideoQuality)
+	c.KwArray = sliceutil.Clone(s.KwArray)
+	c.Data = CloneDataSlice(s.Data)
+	c.Network = CloneNetwork(s.Network)
+	c.Channel = CloneChannel(s.Channel)
+	c.Ext = sliceutil.Clone(s.Ext)
+
+	return &c
+}
+
+func CloneProducer(s *openrtb2.Producer) *openrtb2.Producer {
+	if s == nil {
+		return nil
+	}
+
+	// Shallow Copy (Value Fields)
+	c := *s
+
+	// Deep Copy (Pointers)
+	c.Cat = sliceutil.Clone(s.Cat)
+	c.Ext = sliceutil.Clone(s.Ext)
+
+	return &c
+}
+
+func CloneDataSlice(s []openrtb2.Data) []openrtb2.Data {
+	if s == nil {
+		return nil
+	}
+
+	c := make([]openrtb2.Data, len(s))
+	for i, d := range s {
+		c[i] = CloneData(d)
+	}
+
+	return c
+}
+
+func CloneData(s openrtb2.Data) openrtb2.Data {
+	// Shallow Copy (Value Fields) Occurred By Passing Argument By Value
+
+	// Deep Copy (Pointers)
+	s.Segment = CloneSegmentSlice(s.Segment)
+	s.Ext = sliceutil.Clone(s.Ext)
+
+	return s
+}
+
+func CloneSegmentSlice(s []openrtb2.Segment) []openrtb2.Segment {
+	if s == nil {
+		return nil
+	}
+
+	c := make([]openrtb2.Segment, len(s))
+	for i, d := range s {
+		c[i] = CloneSegment(d)
+	}
+
+	return c
+}
+
+func CloneSegment(s openrtb2.Segment) openrtb2.Segment {
+	// Shallow Copy (Value Fields) Occurred By Passing Argument By Value
+
+	// Deep Copy (Pointers)
+	s.Ext = sliceutil.Clone(s.Ext)
+
+	return s
+}
+
+func CloneNetwork(s *openrtb2.Network) *openrtb2.Network {
+	if s == nil {
+		return nil
+	}
+
+	// Shallow Copy (Value Fields)
+	c := *s
+
+	// Deep Copy (Pointers)
+	c.Ext = sliceutil.Clone(s.Ext)
+
+	return &c
+}
+
+func CloneChannel(s *openrtb2.Channel) *openrtb2.Channel {
+	if s == nil {
+		return nil
+	}
+
+	// Shallow Copy (Value Fields)
+	c := *s
+
+	// Deep Copy (Pointers)
+	c.Ext = sliceutil.Clone(s.Ext)
+
+	return &c
+}
+
+func CloneSite(s *openrtb2.Site) *openrtb2.Site {
+	if s == nil {
+		return nil
+	}
+
+	// Shallow Copy (Value Fields)
+	c := *s
+
+	// Deep Copy (Pointers)
+	c.Cat = sliceutil.Clone(s.Cat)
+	c.SectionCat = sliceutil.Clone(s.SectionCat)
+	c.PageCat = sliceutil.Clone(s.PageCat)
+	c.Publisher = ClonePublisher(s.Publisher)
+	c.Content = CloneContent(s.Content)
+	c.KwArray = sliceutil.Clone(s.KwArray)
+	c.Ext = sliceutil.Clone(s.Ext)
+
+	return &c
+}
+
+func CloneUser(s *openrtb2.User) *openrtb2.User {
+	if s == nil {
+		return nil
+	}
+
+	// Shallow Copy (Value Fields)
+	c := *s
+
+	// Deep Copy (Pointers)
+	c.KwArray = sliceutil.Clone(s.KwArray)
+	c.Geo = CloneGeo(s.Geo)
+	c.Data = CloneDataSlice(s.Data)
+	c.EIDs = CloneEIDSlice(s.EIDs)
+	c.Ext = sliceutil.Clone(s.Ext)
+
+	return &c
+}
+
+func CloneGeo(s *openrtb2.Geo) *openrtb2.Geo {
+	if s == nil {
+		return nil
+	}
+
+	// Shallow Copy (Value Fields)
+	c := *s
+
+	// Deep Copy (Pointers)
+	c.Ext = sliceutil.Clone(s.Ext)
+
+	return &c
+}
+
+func CloneEIDSlice(s []openrtb2.EID) []openrtb2.EID {
+	if s == nil {
+		return nil
+	}
+
+	c := make([]openrtb2.EID, len(s))
+	for i, d := range s {
+		c[i] = CloneEID(d)
+	}
+
+	return c
+}
+
+func CloneEID(s openrtb2.EID) openrtb2.EID {
+	// Shallow Copy (Value Fields) Occurred By Passing Argument By Value
+
+	// Deep Copy (Pointers)
+	s.UIDs = CloneUIDSlice(s.UIDs)
+	s.Ext = sliceutil.Clone(s.Ext)
+
+	return s
+}
+
+func CloneUIDSlice(s []openrtb2.UID) []openrtb2.UID {
+	if s == nil {
+		return nil
+	}
+
+	c := make([]openrtb2.UID, len(s))
+	for i, d := range s {
+		c[i] = CloneUID(d)
+	}
+
+	return c
+}
+
+func CloneUID(s openrtb2.UID) openrtb2.UID {
+	// Shallow Copy (Value Fields) Occurred By Passing Argument By Value
+
+	// Deep Copy (Pointers)
+	s.Ext = sliceutil.Clone(s.Ext)
+
+	return s
+}
+
+func CloneDOOH(s *openrtb2.DOOH) *openrtb2.DOOH {
+	if s == nil {
+		return nil
+	}
+
+	// Shallow Copy (Value Fields)
+	c := *s
+
+	// Deep Copy (Pointers)
+	c.VenueType = sliceutil.Clone(s.VenueType)
+	c.VenueTypeTax = ptrutil.Clone(s.VenueTypeTax)
+	c.Publisher = ClonePublisher(s.Publisher)
+	c.Content = CloneContent(s.Content)
+	c.Ext = sliceutil.Clone(s.Ext)
+
+	return &c
+}
diff --git a/static/bidder-info/adsinteractive.yaml b/static/bidder-info/adsinteractive.yaml
index e70c15b6..0fb63903 100644
--- a/static/bidder-info/adsinteractive.yaml
+++ b/static/bidder-info/adsinteractive.yaml
@@ -11,6 +11,6 @@ capabilities:
       - banner
 userSync:
   redirect:
-    url: "http://pb.adsinteractive.com/getuid?{{.RedirectURL}}"
+    url: "http://bid.adsinteractive.com/getuid?{{.RedirectURL}}"
     userMacro: "$AUID"
- 
\ No newline at end of file
+ 
diff --git a/static/bidder-info/axis.yaml b/static/bidder-info/axis.yaml
new file mode 100644
index 00000000..494091a1
--- /dev/null
+++ b/static/bidder-info/axis.yaml
@@ -0,0 +1,18 @@
+endpoint: "http://prebid.axis-marketplace.com/{{.AccountID}}?token={{.SourceId}}"
+maintainer:
+  email: "help@axis-marketplace.com"
+capabilities:
+  app:
+    mediaTypes:
+      - banner
+      - video
+      - native
+  site:
+    mediaTypes:
+      - banner
+      - video
+      - native
+userSync:
+  redirect:
+    url: "https://cs.axis-marketplace.com/pbserver?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&ccpa={{.USPrivacy}}&redir={{.RedirectURL}}"
+    userMacro: "[UID]"
diff --git a/static/bidder-params/axis.json b/static/bidder-params/axis.json
new file mode 100644
index 00000000..791f4df2
--- /dev/null
+++ b/static/bidder-params/axis.json
@@ -0,0 +1,24 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "Axis Adapter Params",
+  "description": "A schema which validates params accepted by the Axis adapter",
+  "type": "object",
+  "properties": {
+    "integration": {
+      "type": "string",
+      "description": "Integration",
+      "minLength": 6,
+      "maxLength": 6
+    },
+    "token": {
+      "type": "string",
+      "description": "Token",
+      "minLength": 6,
+      "maxLength": 6
+    }
+  },
+  "required": [
+    "integration",
+    "token"
+  ]
+}
diff --git a/util/ptrutil/ptrutil.go b/util/ptrutil/ptrutil.go
index 999ac34a..90888f3e 100644
--- a/util/ptrutil/ptrutil.go
+++ b/util/ptrutil/ptrutil.go
@@ -3,3 +3,12 @@ package ptrutil
 func ToPtr[T any](v T) *T {
 	return &v
 }
+
+func Clone[T any](v *T) *T {
+	if v == nil {
+		return nil
+	}
+
+	clone := *v
+	return &clone
+}
diff --git a/util/sliceutil/clone.go b/util/sliceutil/clone.go
new file mode 100644
index 00000000..2077a933
--- /dev/null
+++ b/util/sliceutil/clone.go
@@ -0,0 +1,12 @@
+package sliceutil
+
+func Clone[T any](s []T) []T {
+	if s == nil {
+		return nil
+	}
+
+	c := make([]T, len(s))
+	copy(c, s)
+
+	return c
+}


Test patch:
diff --git a/adapters/axis/axis_test.go b/adapters/axis/axis_test.go
new file mode 100644
index 00000000..14163157
--- /dev/null
+++ b/adapters/axis/axis_test.go
@@ -0,0 +1,20 @@
+package axis
+
+import (
+	"testing"
+
+	"github.com/prebid/prebid-server/adapters/adapterstest"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+func TestJsonSamples(t *testing.T) {
+	bidder, buildErr := Builder(openrtb_ext.BidderAxis, config.Adapter{
+		Endpoint: "http://prebid.axis-marketplace.com/{{.AccountID}}?token={{.SourceId}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})
+
+	if buildErr != nil {
+		t.Fatalf("Builder returned unexpected error %v", buildErr)
+	}
+
+	adapterstest.RunJSONBidderTest(t, "axistest", bidder)
+}
diff --git a/adapters/axis/axistest/exemplary/simple-banner.json b/adapters/axis/axistest/exemplary/simple-banner.json
new file mode 100644
index 00000000..cd68d432
--- /dev/null
+++ b/adapters/axis/axistest/exemplary/simple-banner.json
@@ -0,0 +1,134 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "device": {
+            "ip": "123.123.123.123",
+            "ua": "iPad"
+        },
+        "app": {
+            "id": "1",
+            "bundle": "com.test.testapplication"
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "integration": "000000",
+                        "token": "000000"
+                    }
+                }
+            }
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://prebid.axis-marketplace.com/000000?token=000000",
+                "body": {
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "tagid": "test",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 300,
+                                        "h": 250
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "integration": "000000",
+                                    "token": "000000"
+                                }
+                            }
+                        }
+                    ],
+                    "app": {
+                        "id": "1",
+                        "bundle": "com.test.testapplication"
+                    },
+                    "device": {
+                        "ip": "123.123.123.123",
+                        "ua": "iPad"
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "bid": [
+                                {
+                                    "id": "test_bid_id",
+                                    "impid": "test-imp-id",
+                                    "price": 0.27543,
+                                    "adm": "<span>Test</span>",
+                                    "cid": "test_cid",
+                                    "crid": "test_crid",
+                                    "dealid": "test_dealid",
+                                    "w": 300,
+                                    "h": 250,
+                                    "ext": {
+                                        "prebid": {
+                                            "type": "banner"
+                                        }
+                                    }
+                                }
+                            ],
+                            "seat": "seat"
+                        }
+                    ],
+                    "cur": "USD"
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "bids": [
+                {
+                    "bid": {
+                        "id": "test_bid_id",
+                        "impid": "test-imp-id",
+                        "price": 0.27543,
+                        "adm": "<span>Test</span>",
+                        "cid": "test_cid",
+                        "crid": "test_crid",
+                        "dealid": "test_dealid",
+                        "w": 300,
+                        "h": 250,
+                        "ext": {
+                            "prebid": {
+                                "type": "banner"
+                            }
+                        }
+                    },
+                    "type": "banner"
+                }
+            ]
+        }
+    ]
+}
diff --git a/adapters/axis/axistest/exemplary/simple-native.json b/adapters/axis/axistest/exemplary/simple-native.json
new file mode 100644
index 00000000..60f8621a
--- /dev/null
+++ b/adapters/axis/axistest/exemplary/simple-native.json
@@ -0,0 +1,118 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "device": {
+            "ip": "123.123.123.123",
+            "ua": "iPad"
+        },
+        "app": {
+            "id": "1",
+            "bundle": "com.test.testapplication"
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "native": {
+                    "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}",
+                    "ver": "1.1"
+                },
+                "ext": {
+                    "bidder": {
+                        "integration": "000000",
+                        "token": "000000"
+                    }
+                }
+            }
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://prebid.axis-marketplace.com/000000?token=000000",
+                "body": {
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "tagid": "test",
+                            "native": {
+                                "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}",
+                                "ver": "1.1"
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "integration": "000000",
+                                    "token": "000000"
+                                }
+                            }
+                        }
+                    ],
+                    "app": {
+                        "id": "1",
+                        "bundle": "com.test.testapplication"
+                    },
+                    "device": {
+                        "ip": "123.123.123.123",
+                        "ua": "iPad"
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "bid": [
+                                {
+                                    "id": "test_bid_id",
+                                    "impid": "test-imp-id",
+                                    "price": 0.27543,
+                                    "adm": "{}",
+                                    "cid": "test_cid",
+                                    "crid": "test_crid",
+                                    "dealid": "test_dealid",
+                                    "w": 300,
+                                    "h": 250,
+                                    "ext": {
+                                        "prebid": {
+                                            "type": "native"
+                                        }
+                                    }
+                                }
+                            ],
+                            "seat": "seat"
+                        }
+                    ],
+                    "cur": "USD"
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "bids": [
+                {
+                    "bid": {
+                        "id": "test_bid_id",
+                        "impid": "test-imp-id",
+                        "price": 0.27543,
+                        "adm": "{}",
+                        "cid": "test_cid",
+                        "crid": "test_crid",
+                        "dealid": "test_dealid",
+                        "w": 300,
+                        "h": 250,
+                        "ext": {
+                            "prebid": {
+                                "type": "native"
+                            }
+                        }
+                    },
+                    "type": "native"
+                }
+            ]
+        }
+    ]
+}
diff --git a/adapters/axis/axistest/exemplary/simple-video.json b/adapters/axis/axistest/exemplary/simple-video.json
new file mode 100644
index 00000000..78c90cc3
--- /dev/null
+++ b/adapters/axis/axistest/exemplary/simple-video.json
@@ -0,0 +1,129 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "device": {
+            "ip": "123.123.123.123",
+            "ua": "iPad"
+        },
+        "app": {
+            "id": "1",
+            "bundle": "com.test.testapplication"
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "video": {
+                    "mimes": [
+                        "video/mp4"
+                    ],
+                    "protocols": [
+                        2,
+                        5
+                    ],
+                    "w": 1024,
+                    "h": 576
+                },
+                "ext": {
+                    "bidder": {
+                        "integration": "000000",
+                        "token": "000000"
+                    }
+                }
+            }
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://prebid.axis-marketplace.com/000000?token=000000",
+                "body": {
+                    "id": "test-request-id",
+                    "device": {
+                        "ip": "123.123.123.123",
+                        "ua": "iPad"
+                    },
+                    "app": {
+                        "id": "1",
+                        "bundle": "com.test.testapplication"
+                    },
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "tagid": "test",
+                            "video": {
+                                "mimes": [
+                                    "video/mp4"
+                                ],
+                                "protocols": [
+                                    2,
+                                    5
+                                ],
+                                "w": 1024,
+                                "h": 576
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "integration": "000000",
+                                    "token": "000000"
+                                }
+                            }
+                        }
+                    ]
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "bid": [
+                                {
+                                    "id": "test_bid_id",
+                                    "impid": "test-imp-id",
+                                    "price": 0.27543,
+                                    "adm": "<VAST version=\"3.0\"><Ad id=\"id\" sequence=\"1\"><InLine><AdSystem version=\"4.0\"><![CDATA[iabtechlab]]></AdSystem><AdTitle><![CDATA[Inline Simple Ad]]></AdTitle><Impression><![CDATA[]]></Impression><Creatives><Creative id=\"id\" sequence=\"1\"><Linear><Duration>00:01:00</Duration><MediaFiles><MediaFile id=\"id\" delivery=\"progressive\" type=\"video/mp4\" bitrate=\"600\" width=\"640\" height=\"360\" minBitrate=\"500\" maxBitrate=\"700\" scalable=\"1\" maintainAspectRatio=\"1\" codec=\"0\"><![CDATA[https://test.com]]></MediaFile></MediaFiles><VideoClicks><ClickThrough id=\"blog\"><![CDATA[https://test.com]]></ClickThrough></VideoClicks></Linear></Creative></Creatives></InLine></Ad></VAST>",
+                                    "cid": "test_cid",
+                                    "crid": "test_crid",
+                                    "dealid": "test_dealid",
+                                    "ext": {
+                                        "prebid": {
+                                            "type": "video"
+                                        }
+                                    }
+                                }
+                            ],
+                            "seat": "seat"
+                        }
+                    ],
+                    "cur": "USD"
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "currency": "USD",
+            "bids": [
+                {
+                    "bid": {
+                        "id": "test_bid_id",
+                        "impid": "test-imp-id",
+                        "price": 0.27543,
+                        "adm": "<VAST version=\"3.0\"><Ad id=\"id\" sequence=\"1\"><InLine><AdSystem version=\"4.0\"><![CDATA[iabtechlab]]></AdSystem><AdTitle><![CDATA[Inline Simple Ad]]></AdTitle><Impression><![CDATA[]]></Impression><Creatives><Creative id=\"id\" sequence=\"1\"><Linear><Duration>00:01:00</Duration><MediaFiles><MediaFile id=\"id\" delivery=\"progressive\" type=\"video/mp4\" bitrate=\"600\" width=\"640\" height=\"360\" minBitrate=\"500\" maxBitrate=\"700\" scalable=\"1\" maintainAspectRatio=\"1\" codec=\"0\"><![CDATA[https://test.com]]></MediaFile></MediaFiles><VideoClicks><ClickThrough id=\"blog\"><![CDATA[https://test.com]]></ClickThrough></VideoClicks></Linear></Creative></Creatives></InLine></Ad></VAST>",
+                        "cid": "test_cid",
+                        "crid": "test_crid",
+                        "dealid": "test_dealid",
+                        "ext": {
+                            "prebid": {
+                                "type": "video"
+                            }
+                        }
+                    },
+                    "type": "video"
+                }
+            ]
+        }
+    ]
+}
diff --git a/adapters/axis/axistest/exemplary/simple-web-banner.json b/adapters/axis/axistest/exemplary/simple-web-banner.json
new file mode 100644
index 00000000..a6e325dc
--- /dev/null
+++ b/adapters/axis/axistest/exemplary/simple-web-banner.json
@@ -0,0 +1,134 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "integration": "000000",
+                        "token": "000000"
+                    }
+                }
+            }
+        ],
+        "site": {
+            "id": "1",
+            "domain": "test.com"
+        },
+        "device": {
+            "ip": "123.123.123.123",
+            "ua": "Ubuntu"
+        }
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://prebid.axis-marketplace.com/000000?token=000000",
+                "body": {
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "tagid": "test",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 300,
+                                        "h": 250
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "integration": "000000",
+                                    "token": "000000"
+                                }
+                            }
+                        }
+                    ],
+                    "site": {
+                        "id": "1",
+                        "domain": "test.com"
+                    },
+                    "device": {
+                        "ip": "123.123.123.123",
+                        "ua": "Ubuntu"
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "bid": [
+                                {
+                                    "id": "test_bid_id",
+                                    "impid": "test-imp-id",
+                                    "price": 0.27543,
+                                    "adm": "<span>Test</span>",
+                                    "cid": "test_cid",
+                                    "crid": "test_crid",
+                                    "dealid": "test_dealid",
+                                    "w": 468,
+                                    "h": 60,
+                                    "ext": {
+                                        "prebid": {
+                                            "type": "banner"
+                                        }
+                                    }
+                                }
+                            ],
+                            "seat": "seat"
+                        }
+                    ],
+                    "cur": "USD"
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "bids": [
+                {
+                    "bid": {
+                        "id": "test_bid_id",
+                        "impid": "test-imp-id",
+                        "price": 0.27543,
+                        "adm": "<span>Test</span>",
+                        "cid": "test_cid",
+                        "crid": "test_crid",
+                        "dealid": "test_dealid",
+                        "w": 468,
+                        "h": 60,
+                        "ext": {
+                            "prebid": {
+                                "type": "banner"
+                            }
+                        }
+                    },
+                    "type": "banner"
+                }
+            ]
+        }
+    ]
+}
diff --git a/adapters/axis/axistest/supplemental/bad_media_type.json b/adapters/axis/axistest/supplemental/bad_media_type.json
new file mode 100644
index 00000000..eb2abc86
--- /dev/null
+++ b/adapters/axis/axistest/supplemental/bad_media_type.json
@@ -0,0 +1,87 @@
+{
+	"mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "ext": {
+                    "bidder": {
+                        "integration": "000000",
+                        "token": "000000"
+                    }
+                }
+            }
+        ],
+        "app": {
+            "id": "1",
+            "bundle": "com.test.testapplication"
+        },
+        "device": {
+            "ip": "123.123.123.123",
+            "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+        }
+    },
+	"httpCalls": [{
+		"expectedRequest": {
+			"uri": "http://prebid.axis-marketplace.com/000000?token=000000",
+			"body": {
+                "id": "test-request-id",
+                "imp": [
+                    {
+                        "id": "test-imp-id",
+                        "ext": {
+                            "bidder": {
+                                "integration": "000000",
+                                "token": "000000"
+                            }
+                        }
+                    }
+                ],
+                "app": {
+                    "id": "1",
+                    "bundle": "com.test.testapplication"
+                },
+                "device": {
+                    "ip": "123.123.123.123",
+                    "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+                }
+            }
+		},
+		"mockResponse": {
+			"status": 200,
+			"body": {
+                "id": "test-request-id",
+                "seatbid": [
+                    {
+                        "bid": [
+                            {
+                                "id": "test_bid_id",
+                                "impid": "test-imp-id",
+                                "price": 0.27543,
+                                "adm": "<span>Test</span>",
+                                "cid": "test_cid",
+                                "crid": "test_crid",
+                                "dealid": "test_dealid",
+                                "w": 300,
+                                "h": 250,
+                                "ext": {
+                                    "prebid": {
+                                        "type": "banner"
+                                    }
+                                }
+                            }
+                        ],
+                        "seat": "seat"
+                    }
+                ],
+                "cur": "USD"
+            }
+        }
+    }],
+    "expectedMakeBidsErrors": [
+        {
+          "value": "Failed to find impression \"test-imp-id\"",
+          "comparison": "literal"
+        }
+    ]
+}
diff --git a/adapters/axis/axistest/supplemental/bad_response.json b/adapters/axis/axistest/supplemental/bad_response.json
new file mode 100644
index 00000000..4467c2a5
--- /dev/null
+++ b/adapters/axis/axistest/supplemental/bad_response.json
@@ -0,0 +1,85 @@
+{
+	"mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "integration": "000000",
+                        "token": "000000"
+                    }
+                }
+            }
+        ],
+        "app": {
+            "id": "1",
+            "bundle": "com.test.testapplication"
+        },
+        "device": {
+            "ip": "123.123.123.123",
+            "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+        }
+    },
+	"httpCalls": [{
+		"expectedRequest": {
+			"uri": "http://prebid.axis-marketplace.com/000000?token=000000",
+			"body": {
+                "id": "test-request-id",
+                "imp": [
+                    {
+                        "id": "test-imp-id",
+                        "banner": {
+                            "format": [
+                                {
+                                    "w": 300,
+                                    "h": 250
+                                },
+                                {
+                                    "w": 300,
+                                    "h": 600
+                                }
+                            ]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "integration": "000000",
+                                "token": "000000"
+                            }
+                        }
+                    }
+                ],
+                "app": {
+                    "id": "1",
+                    "bundle": "com.test.testapplication"
+                },
+                "device": {
+                    "ip": "123.123.123.123",
+                    "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+                }
+            }
+		},
+		"mockResponse": {
+			"status": 200,
+			"body": ""
+        }
+    }],
+    "expectedMakeBidsErrors": [
+        {
+          "value": "json: cannot unmarshal string into Go value of type openrtb2.BidResponse",
+          "comparison": "literal"
+        }
+    ]
+}
diff --git a/adapters/axis/axistest/supplemental/status-204.json b/adapters/axis/axistest/supplemental/status-204.json
new file mode 100644
index 00000000..0f778c44
--- /dev/null
+++ b/adapters/axis/axistest/supplemental/status-204.json
@@ -0,0 +1,80 @@
+{
+	"mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "integration": "000000",
+                        "token": "000000"
+                    }
+                }
+            }
+        ],
+        "app": {
+            "id": "1",
+            "bundle": "com.test.testapplication"
+        },
+        "device": {
+            "ip": "123.123.123.123",
+            "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+        }
+    },
+	"httpCalls": [{
+		"expectedRequest": {
+			"uri": "http://prebid.axis-marketplace.com/000000?token=000000",
+			"body": {
+                "id": "test-request-id",
+                "imp": [
+                    {
+                        "id": "test-imp-id",
+                        "banner": {
+                            "format": [
+                                {
+                                    "w": 300,
+                                    "h": 250
+                                },
+                                {
+                                    "w": 300,
+                                    "h": 600
+                                }
+                            ]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "integration": "000000",
+                                "token": "000000"
+                            }
+                        }
+                    }
+                ],
+                "app": {
+                    "id": "1",
+                    "bundle": "com.test.testapplication"
+                },
+                "device": {
+                    "ip": "123.123.123.123",
+                    "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+                }
+            }
+		},
+		"mockResponse": {
+			"status": 204,
+			"body": {}
+		}
+    }],
+    "expectedBidResponses": []
+}
diff --git a/adapters/axis/axistest/supplemental/status-not-200.json b/adapters/axis/axistest/supplemental/status-not-200.json
new file mode 100644
index 00000000..5b4888fb
--- /dev/null
+++ b/adapters/axis/axistest/supplemental/status-not-200.json
@@ -0,0 +1,85 @@
+{
+	"mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "integration": "000000",
+                        "token": "000000"
+                    }
+                }
+            }
+        ],
+        "app": {
+            "id": "1",
+            "bundle": "com.test.testapplication"
+        },
+        "device": {
+            "ip": "123.123.123.123",
+            "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+        }
+    },
+	"httpCalls": [{
+		"expectedRequest": {
+			"uri": "http://prebid.axis-marketplace.com/000000?token=000000",
+			"body": {
+                "id": "test-request-id",
+                "imp": [
+                    {
+                        "id": "test-imp-id",
+                        "banner": {
+                            "format": [
+                                {
+                                    "w": 300,
+                                    "h": 250
+                                },
+                                {
+                                    "w": 300,
+                                    "h": 600
+                                }
+                            ]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "integration": "000000",
+                                "token": "000000"
+                            }
+                        }
+                    }
+                ],
+                "app": {
+                    "id": "1",
+                    "bundle": "com.test.testapplication"
+                },
+                "device": {
+                    "ip": "123.123.123.123",
+                    "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+                }
+            }
+		},
+		"mockResponse": {
+			"status": 404,
+			"body": {}
+		}
+    }],
+    "expectedMakeBidsErrors": [
+        {
+          "value": "Unexpected status code: 404. Run with request.debug = 1 for more info.",
+          "comparison": "literal"
+        }
+    ]
+}
diff --git a/adapters/axis/params_test.go b/adapters/axis/params_test.go
new file mode 100644
index 00000000..ba374c80
--- /dev/null
+++ b/adapters/axis/params_test.go
@@ -0,0 +1,54 @@
+package axis
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+func TestValidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json schema. %v", err)
+	}
+
+	for _, p := range validParams {
+		if err := validator.Validate(openrtb_ext.BidderAxis, json.RawMessage(p)); err != nil {
+			t.Errorf("Schema rejected valid params: %s", p)
+		}
+	}
+}
+
+func TestInvalidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json schema. %v", err)
+	}
+
+	for _, p := range invalidParams {
+		if err := validator.Validate(openrtb_ext.BidderAxis, json.RawMessage(p)); err == nil {
+			t.Errorf("Schema allowed unexpected params: %s", p)
+		}
+	}
+}
+
+var validParams = []string{
+	`{"integration":"000000", "token":"000000"}`,
+}
+
+var invalidParams = []string{
+	``,
+	`null`,
+	`true`,
+	`5`,
+	`[]`,
+	`{}`,
+	`{"anyparam": "anyvalue"}`,
+	`{"integration":"9Q20EdGxzgWdfPYShScl"}`,
+	`{"token":"Y9Evrh40ejsrCR4EtidUt1cSxhJsz8X1"}`,
+	`{"integration":"9Q20EdGxzgWdfPYShScl", "token":"alNYtemWggraDVbhJrsOs9pXc3Eld32E"}`,
+	`{"integration":"", "token":""}`,
+	`{"integration":"9Q20EdGxzgWdfPYShScl", "token":""}`,
+	`{"integration":"", "token":"alNYtemWggraDVbhJrsOs9pXc3Eld32E"}`,
+}
diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-app.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-app.json
index 4b760494..672d8ab0 100644
--- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-app.json
+++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-app.json
@@ -13,6 +13,7 @@
           },
           "bidfloor": 1e-05,
           "bidfloorcur": "USD",
+          "tagid": "testTag",
           "ext": {
            "bidder": {
               "pid": "OsNsyeF68q",
@@ -24,7 +25,6 @@
       ],
       "app": {
         "id": "12345678",
-        "keywords": "tagid=",
         "name": "Richaudience TV",
         "bundle": "R12345678901011",
         "publisher": {
@@ -41,7 +41,7 @@
         }
     },
       "device": {
-        "ip": "11.222.33.44",
+        "ip": "10.20.30.40",
         "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx"
     },
       "user": {
@@ -86,7 +86,7 @@
             ],
             "app": {
               "id": "12345678",
-              "keywords": "tagid=",
+              "keywords": "tagid=testTag",
               "name": "Richaudience TV",
               "bundle": "R12345678901011",
               "publisher": {
diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-defaultCurrency.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-defaultCurrency.json
index 7583c13f..0ddec0a4 100644
--- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-defaultCurrency.json
+++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-defaultCurrency.json
@@ -14,6 +14,7 @@
           ]
         },
         "bidfloor": 1e-05,
+        "tagid": "testTag",
         "ext": {
           "bidder": {
             "pid": "OsNsyeF68q",
@@ -24,12 +25,11 @@
       }
     ],
     "site": {
-      "keywords": "tagid=",
       "domain": "bridge.richmediastudio.com",
       "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true"
     },
     "device": {
-      "ip": "11.222.33.44",
+      "ip": "10.20.30.40",
       "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"
     },
     "user": {
@@ -80,7 +80,7 @@
             }
           ],
           "site": {
-            "keywords": "tagid=",
+            "keywords": "tagid=testTag",
             "domain": "bridge.richmediastudio.com",
             "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true"
           },
diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-deviceConfig.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-deviceConfig.json
index e4bc3327..5ccc8f14 100644
--- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-deviceConfig.json
+++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-deviceConfig.json
@@ -13,6 +13,7 @@
           },
           "bidfloor": 1e-05,
           "bidfloorcur": "USD",
+          "tagid": "testTag",
           "ext": {
            "bidder": {
               "pid": "OsNsyeF68q",
@@ -23,14 +24,13 @@
         }
       ],
       "site": {
-        "keywords": "tagid=",
         "domain": "bridge.richmediastudio.com",
         "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true"
       },
       "device": {
           "dnt": 1,
           "lmt": 1,
-        "ip": "11.222.33.44"
+        "ip": "10.20.30.40"
     },
       "user": {
         "buyeruid": "189f4055-78a3-46eb-b7fd-0915a1a43bd2a",
@@ -73,7 +73,7 @@
               }
             ],
             "site": {
-              "keywords": "tagid=",
+              "keywords": "tagid=testTag",
               "domain": "bridge.richmediastudio.com",
               "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true"
             },
diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-extUser.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-extUser.json
index d22e1cf6..b6227d37 100644
--- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-extUser.json
+++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-extUser.json
@@ -25,12 +25,11 @@
       }
     ],
     "site": {
-      "keywords": "tagid=",
       "domain": "bridge.richmediastudio.com",
       "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true"
     },
     "device": {
-      "ip": "11.222.33.44",
+      "ip": "10.20.30.40",
       "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"
     },
     "user": {
diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-floorPrice.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-floorPrice.json
index 7d76a12b..9fec4d46 100644
--- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-floorPrice.json
+++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-floorPrice.json
@@ -24,12 +24,11 @@
         }
       ],
       "site": {
-        "keywords": "tagid=",
         "domain": "bridge.richmediastudio.com",
         "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true"
       },
       "device": {
-        "ip": "11.222.33.44",
+        "ip": "10.20.30.40",
         "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"
     },
       "user": {
diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-iPv6.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-iPv6.json
index 93fabba3..b40e9f65 100644
--- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-iPv6.json
+++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-iPv6.json
@@ -25,12 +25,11 @@
       }
     ],
     "site": {
-      "keywords": "tagid=",
       "domain": "bridge.richmediastudio.com",
       "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true"
     },
     "device": {
-      "ip": "11.222.33.44",
+      "ip": "10.20.30.40",
       "ipv6": "2607:fb90:f27:4512:d800:cb23:a603:e245",
       "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"
     },
diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-nil-device.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-nil-device.json
new file mode 100644
index 00000000..f5e38991
--- /dev/null
+++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-nil-device.json
@@ -0,0 +1,168 @@
+{
+  "mockBidRequest": {
+    "id": "4d3f84eb-787b-42fb-a0cf-062690dadce3",
+    "test": 0,
+    "imp": [
+      {
+        "id": "div-gpt-ad-1460505748561-0",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "bidfloor": 1e-05,
+        "bidfloorcur": "USD",
+        "tagid": "testTag",
+        "ext": {
+          "bidder": {
+            "pid": "OsNsyeF68q",
+            "supplyType": "site",
+            "test": true
+          }
+        }
+      }
+    ],
+    "app": {
+      "id": "12345678",
+      "name": "Richaudience TV",
+      "bundle": "R12345678901011",
+      "publisher": {
+        "id": "1234567",
+        "ext": {
+          "prebid": {
+            "parentAccount": "891011"
+          }
+        }
+      },
+      "content": {
+        "title": "Richaudience TV",
+        "series": "Richaudience TV"
+      }
+    },
+    "user": {
+      "buyeruid": "189f4055-78a3-46eb-b7fd-0915a1a43bd2a",
+      "ext": {}
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://ortb.richaudience.com/ortb/?bidder=pbs",
+        "headers": {
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "Accept": [
+            "application/json"
+          ],
+          "X-Openrtb-Version": [
+            "2.5"
+          ]
+        },
+        "body": {
+          "id": "4d3f84eb-787b-42fb-a0cf-062690dadce3",
+          "test": 1,
+          "imp": [
+            {
+              "id": "div-gpt-ad-1460505748561-0",
+              "tagid": "OsNsyeF68q",
+              "secure": 0,
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ]
+              },
+              "bidfloor": 1e-05,
+              "bidfloorcur": "USD",
+              "ext": {
+                "bidder": {
+                  "pid": "OsNsyeF68q",
+                  "supplyType": "site",
+                  "test": true
+                }
+              }
+            }
+          ],
+          "app": {
+            "id": "12345678",
+            "keywords": "tagid=testTag",
+            "name": "Richaudience TV",
+            "bundle": "R12345678901011",
+            "publisher": {
+              "id": "1234567",
+              "ext": {
+                "prebid": {
+                  "parentAccount": "891011"
+                }
+              }
+            },
+            "content": {
+              "title": "Richaudience TV",
+              "series": "Richaudience TV"
+            }
+          },
+          "device": {
+            "ip": "11.222.33.44"
+          },
+          "user": {
+            "buyeruid": "189f4055-78a3-46eb-b7fd-0915a1a43bd2a",
+            "ext": {
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "ff935bea-4661-40bf-95b7-80c354cf0cdc",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "47286888",
+                  "impid": "div-gpt-ad-1460505748561-0",
+                  "price": 99,
+                  "crid": "999999",
+                  "adm": "<!-- creative -->",
+                  "adomain": [
+                    "richaudience.com"
+                  ],
+                  "h": 250,
+                  "w": 300
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "47286888",
+            "impid": "div-gpt-ad-1460505748561-0",
+            "price": 99,
+            "adm": "<!-- creative -->",
+            "adomain": [
+              "richaudience.com"
+            ],
+            "crid": "999999",
+            "w": 300,
+            "h": 250
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-nosecure.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-nosecure.json
index 68cc0af6..78886f38 100644
--- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-nosecure.json
+++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-nosecure.json
@@ -23,12 +23,11 @@
         }
       ],
       "site": {
-        "keywords": "tagid=",
         "domain": "bridge.richmediastudio.com",
         "page": "http://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true"
       },
       "device": {
-        "ip": "11.222.33.44",
+        "ip": "10.20.30.40",
         "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"
     },
       "user": {
diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-setCurrency.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-setCurrency.json
index 70662052..5490197d 100644
--- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-setCurrency.json
+++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-setCurrency.json
@@ -23,12 +23,11 @@
         }
       ],
       "site": {
-        "keywords": "tagid=",
         "domain": "bridge.richmediastudio.com",
         "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true"
       },
       "device": {
-        "ip": "11.222.33.44",
+        "ip": "10.20.30.40",
         "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"
     },
       "user": {
diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner-sitePage.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner-sitePage.json
index 901af4a8..e6a00cbf 100644
--- a/adapters/richaudience/richaudiencetest/exemplary/single-banner-sitePage.json
+++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner-sitePage.json
@@ -23,12 +23,11 @@
         }
       ],
       "site": {
-        "keywords": "tagid=",
         "domain": "bridge.richmediastudio.com",
         "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true"
       },
       "device": {
-        "ip": "11.222.33.44",
+        "ip": "10.20.30.40",
         "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"
     },
       "user": {
diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-banner.json b/adapters/richaudience/richaudiencetest/exemplary/single-banner.json
index 901af4a8..e6a00cbf 100644
--- a/adapters/richaudience/richaudiencetest/exemplary/single-banner.json
+++ b/adapters/richaudience/richaudiencetest/exemplary/single-banner.json
@@ -23,12 +23,11 @@
         }
       ],
       "site": {
-        "keywords": "tagid=",
         "domain": "bridge.richmediastudio.com",
         "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true"
       },
       "device": {
-        "ip": "11.222.33.44",
+        "ip": "10.20.30.40",
         "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"
     },
       "user": {
diff --git a/adapters/richaudience/richaudiencetest/exemplary/single-video.json b/adapters/richaudience/richaudiencetest/exemplary/single-video.json
index 2d18c01a..0219906f 100644
--- a/adapters/richaudience/richaudiencetest/exemplary/single-video.json
+++ b/adapters/richaudience/richaudiencetest/exemplary/single-video.json
@@ -23,12 +23,11 @@
         }
       ],
       "site": {
-        "keywords": "tagid=",
         "domain": "bridge.richmediastudio.com",
         "page": "https://bridge.richmediastudio.com//ab083674fb8200b877a6983126e4477d/PrebidServer/indexRa.html?pbjs_debug=true"
       },
       "device": {
-        "ip": "11.222.33.44",
+        "ip": "10.20.30.40",
         "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"
     },
       "user": {
diff --git a/config/config_test.go b/config/config_test.go
index d63dab8d..f45d7fcd 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -145,53 +145,53 @@ func TestExternalCacheURLValidate(t *testing.T) {
 func TestDefaults(t *testing.T) {
 	cfg, _ := newDefaultConfig(t)
 
-	cmpInts(t, "port", cfg.Port, 8000)
-	cmpInts(t, "admin_port", cfg.AdminPort, 6060)
-	cmpInts(t, "auction_timeouts_ms.max", int(cfg.AuctionTimeouts.Max), 0)
-	cmpInts(t, "max_request_size", int(cfg.MaxRequestSize), 1024*256)
-	cmpInts(t, "host_cookie.ttl_days", int(cfg.HostCookie.TTL), 90)
-	cmpInts(t, "host_cookie.max_cookie_size_bytes", cfg.HostCookie.MaxCookieSizeBytes, 0)
-	cmpInts(t, "currency_converter.fetch_interval_seconds", cfg.CurrencyConverter.FetchIntervalSeconds, 1800)
-	cmpStrings(t, "currency_converter.fetch_url", cfg.CurrencyConverter.FetchURL, "https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json")
-	cmpBools(t, "account_required", cfg.AccountRequired, false)
-	cmpInts(t, "metrics.influxdb.collection_rate_seconds", cfg.Metrics.Influxdb.MetricSendInterval, 20)
-	cmpBools(t, "account_adapter_details", cfg.Metrics.Disabled.AccountAdapterDetails, false)
-	cmpBools(t, "account_debug", cfg.Metrics.Disabled.AccountDebug, true)
-	cmpBools(t, "account_stored_responses", cfg.Metrics.Disabled.AccountStoredResponses, true)
-	cmpBools(t, "adapter_connections_metrics", cfg.Metrics.Disabled.AdapterConnectionMetrics, true)
-	cmpBools(t, "adapter_gdpr_request_blocked", cfg.Metrics.Disabled.AdapterGDPRRequestBlocked, false)
-	cmpStrings(t, "certificates_file", cfg.PemCertsFile, "")
+	cmpInts(t, "port", 8000, cfg.Port)
+	cmpInts(t, "admin_port", 6060, cfg.AdminPort)
+	cmpInts(t, "auction_timeouts_ms.max", 0, int(cfg.AuctionTimeouts.Max))
+	cmpInts(t, "max_request_size", 1024*256, int(cfg.MaxRequestSize))
+	cmpInts(t, "host_cookie.ttl_days", 90, int(cfg.HostCookie.TTL))
+	cmpInts(t, "host_cookie.max_cookie_size_bytes", 0, cfg.HostCookie.MaxCookieSizeBytes)
+	cmpInts(t, "currency_converter.fetch_interval_seconds", 1800, cfg.CurrencyConverter.FetchIntervalSeconds)
+	cmpStrings(t, "currency_converter.fetch_url", "https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json", cfg.CurrencyConverter.FetchURL)
+	cmpBools(t, "account_required", false, cfg.AccountRequired)
+	cmpInts(t, "metrics.influxdb.collection_rate_seconds", 20, cfg.Metrics.Influxdb.MetricSendInterval)
+	cmpBools(t, "account_adapter_details", false, cfg.Metrics.Disabled.AccountAdapterDetails)
+	cmpBools(t, "account_debug", true, cfg.Metrics.Disabled.AccountDebug)
+	cmpBools(t, "account_stored_responses", true, cfg.Metrics.Disabled.AccountStoredResponses)
+	cmpBools(t, "adapter_connections_metrics", true, cfg.Metrics.Disabled.AdapterConnectionMetrics)
+	cmpBools(t, "adapter_gdpr_request_blocked", false, cfg.Metrics.Disabled.AdapterGDPRRequestBlocked)
+	cmpStrings(t, "certificates_file", "", cfg.PemCertsFile)
 	cmpBools(t, "stored_requests.filesystem.enabled", false, cfg.StoredRequests.Files.Enabled)
 	cmpStrings(t, "stored_requests.filesystem.directorypath", "./stored_requests/data/by_id", cfg.StoredRequests.Files.Path)
-	cmpBools(t, "auto_gen_source_tid", cfg.AutoGenSourceTID, true)
-	cmpBools(t, "generate_bid_id", cfg.GenerateBidID, false)
-	cmpStrings(t, "experiment.adscert.mode", cfg.Experiment.AdCerts.Mode, "off")
-	cmpStrings(t, "experiment.adscert.inprocess.origin", cfg.Experiment.AdCerts.InProcess.Origin, "")
-	cmpStrings(t, "experiment.adscert.inprocess.key", cfg.Experiment.AdCerts.InProcess.PrivateKey, "")
-	cmpInts(t, "experiment.adscert.inprocess.domain_check_interval_seconds", cfg.Experiment.AdCerts.InProcess.DNSCheckIntervalInSeconds, 30)
-	cmpInts(t, "experiment.adscert.inprocess.domain_renewal_interval_seconds", cfg.Experiment.AdCerts.InProcess.DNSRenewalIntervalInSeconds, 30)
-	cmpStrings(t, "experiment.adscert.remote.url", cfg.Experiment.AdCerts.Remote.Url, "")
-	cmpInts(t, "experiment.adscert.remote.signing_timeout_ms", cfg.Experiment.AdCerts.Remote.SigningTimeoutMs, 5)
+	cmpBools(t, "auto_gen_source_tid", true, cfg.AutoGenSourceTID)
+	cmpBools(t, "generate_bid_id", false, cfg.GenerateBidID)
+	cmpStrings(t, "experiment.adscert.mode", "off", cfg.Experiment.AdCerts.Mode)
+	cmpStrings(t, "experiment.adscert.inprocess.origin", "", cfg.Experiment.AdCerts.InProcess.Origin)
+	cmpStrings(t, "experiment.adscert.inprocess.key", "", cfg.Experiment.AdCerts.InProcess.PrivateKey)
+	cmpInts(t, "experiment.adscert.inprocess.domain_check_interval_seconds", 30, cfg.Experiment.AdCerts.InProcess.DNSCheckIntervalInSeconds)
+	cmpInts(t, "experiment.adscert.inprocess.domain_renewal_interval_seconds", 30, cfg.Experiment.AdCerts.InProcess.DNSRenewalIntervalInSeconds)
+	cmpStrings(t, "experiment.adscert.remote.url", "", cfg.Experiment.AdCerts.Remote.Url)
+	cmpInts(t, "experiment.adscert.remote.signing_timeout_ms", 5, cfg.Experiment.AdCerts.Remote.SigningTimeoutMs)
 	cmpNils(t, "host_schain_node", cfg.HostSChainNode)
-	cmpStrings(t, "datacenter", cfg.DataCenter, "")
+	cmpStrings(t, "datacenter", "", cfg.DataCenter)
 
 	//Assert the price floor default values
-	cmpBools(t, "price_floors.enabled", cfg.PriceFloors.Enabled, false)
-
-	cmpBools(t, "account_defaults.price_floors.enabled", cfg.AccountDefaults.PriceFloors.Enabled, false)
-	cmpInts(t, "account_defaults.price_floors.enforce_floors_rate", cfg.AccountDefaults.PriceFloors.EnforceFloorsRate, 100)
-	cmpBools(t, "account_defaults.price_floors.adjust_for_bid_adjustment", cfg.AccountDefaults.PriceFloors.AdjustForBidAdjustment, true)
-	cmpBools(t, "account_defaults.price_floors.enforce_deal_floors", cfg.AccountDefaults.PriceFloors.EnforceDealFloors, false)
-	cmpBools(t, "account_defaults.price_floors.use_dynamic_data", cfg.AccountDefaults.PriceFloors.UseDynamicData, false)
-	cmpInts(t, "account_defaults.price_floors.max_rules", cfg.AccountDefaults.PriceFloors.MaxRule, 100)
-	cmpInts(t, "account_defaults.price_floors.max_schema_dims", cfg.AccountDefaults.PriceFloors.MaxSchemaDims, 3)
-
-	cmpBools(t, "hooks.enabled", cfg.Hooks.Enabled, false)
-	cmpStrings(t, "validations.banner_creative_max_size", cfg.Validations.BannerCreativeMaxSize, "skip")
-	cmpStrings(t, "validations.secure_markup", cfg.Validations.SecureMarkup, "skip")
-	cmpInts(t, "validations.max_creative_width", int(cfg.Validations.MaxCreativeWidth), 0)
-	cmpInts(t, "validations.max_creative_height", int(cfg.Validations.MaxCreativeHeight), 0)
-	cmpBools(t, "account_modules_metrics", cfg.Metrics.Disabled.AccountModulesMetrics, false)
+	cmpBools(t, "price_floors.enabled", false, cfg.PriceFloors.Enabled)
+
+	cmpBools(t, "account_defaults.price_floors.enabled", false, cfg.AccountDefaults.PriceFloors.Enabled)
+	cmpInts(t, "account_defaults.price_floors.enforce_floors_rate", 100, cfg.AccountDefaults.PriceFloors.EnforceFloorsRate)
+	cmpBools(t, "account_defaults.price_floors.adjust_for_bid_adjustment", true, cfg.AccountDefaults.PriceFloors.AdjustForBidAdjustment)
+	cmpBools(t, "account_defaults.price_floors.enforce_deal_floors", false, cfg.AccountDefaults.PriceFloors.EnforceDealFloors)
+	cmpBools(t, "account_defaults.price_floors.use_dynamic_data", false, cfg.AccountDefaults.PriceFloors.UseDynamicData)
+	cmpInts(t, "account_defaults.price_floors.max_rules", 100, cfg.AccountDefaults.PriceFloors.MaxRule)
+	cmpInts(t, "account_defaults.price_floors.max_schema_dims", 3, cfg.AccountDefaults.PriceFloors.MaxSchemaDims)
+
+	cmpBools(t, "hooks.enabled", false, cfg.Hooks.Enabled)
+	cmpStrings(t, "validations.banner_creative_max_size", "skip", cfg.Validations.BannerCreativeMaxSize)
+	cmpStrings(t, "validations.secure_markup", "skip", cfg.Validations.SecureMarkup)
+	cmpInts(t, "validations.max_creative_width", 0, int(cfg.Validations.MaxCreativeWidth))
+	cmpInts(t, "validations.max_creative_height", 0, int(cfg.Validations.MaxCreativeHeight))
+	cmpBools(t, "account_modules_metrics", false, cfg.Metrics.Disabled.AccountModulesMetrics)
 
 	//Assert purpose VendorExceptionMap hash tables were built correctly
 	expectedTCF2 := TCF2{
@@ -462,24 +462,24 @@ stored_requests:
   directorypath: "/somepath"
 `)
 
-func cmpStrings(t *testing.T, key string, a string, b string) {
+func cmpStrings(t *testing.T, key, expected, actual string) {
 	t.Helper()
-	assert.Equal(t, a, b, "%s: %s != %s", key, a, b)
+	assert.Equal(t, expected, actual, "%s: %s != %s", key, expected, actual)
 }
 
-func cmpInts(t *testing.T, key string, a int, b int) {
+func cmpInts(t *testing.T, key string, expected, actual int) {
 	t.Helper()
-	assert.Equal(t, a, b, "%s: %d != %d", key, a, b)
+	assert.Equal(t, expected, actual, "%s: %d != %d", key, expected, actual)
 }
 
-func cmpInt8s(t *testing.T, key string, a *int8, b *int8) {
+func cmpInt8s(t *testing.T, key string, expected, actual *int8) {
 	t.Helper()
-	assert.Equal(t, a, b, "%s: %d != %d", key, a, b)
+	assert.Equal(t, expected, actual, "%s: %d != %d", key, expected, actual)
 }
 
-func cmpBools(t *testing.T, key string, a bool, b bool) {
+func cmpBools(t *testing.T, key string, expected, actual bool) {
 	t.Helper()
-	assert.Equal(t, a, b, "%s: %t != %t", key, a, b)
+	assert.Equal(t, expected, actual, "%s: %t != %t", key, expected, actual)
 }
 
 func cmpNils(t *testing.T, key string, a interface{}) {
@@ -496,53 +496,53 @@ func TestFullConfig(t *testing.T) {
 	v.ReadConfig(bytes.NewBuffer(fullConfig))
 	cfg, err := New(v, bidderInfos, mockNormalizeBidderName)
 	assert.NoError(t, err, "Setting up config should work but it doesn't")
-	cmpStrings(t, "cookie domain", cfg.HostCookie.Domain, "cookies.prebid.org")
-	cmpStrings(t, "cookie name", cfg.HostCookie.CookieName, "userid")
-	cmpStrings(t, "cookie family", cfg.HostCookie.Family, "prebid")
-	cmpStrings(t, "opt out", cfg.HostCookie.OptOutURL, "http://prebid.org/optout")
-	cmpStrings(t, "opt in", cfg.HostCookie.OptInURL, "http://prebid.org/optin")
-	cmpStrings(t, "external url", cfg.ExternalURL, "http://prebid-server.prebid.org/")
-	cmpStrings(t, "host", cfg.Host, "prebid-server.prebid.org")
-	cmpInts(t, "port", cfg.Port, 1234)
-	cmpInts(t, "admin_port", cfg.AdminPort, 5678)
-	cmpInts(t, "garbage_collector_threshold", cfg.GarbageCollectorThreshold, 1)
-	cmpInts(t, "auction_timeouts_ms.default", int(cfg.AuctionTimeouts.Default), 50)
-	cmpInts(t, "auction_timeouts_ms.max", int(cfg.AuctionTimeouts.Max), 123)
-	cmpStrings(t, "cache.scheme", cfg.CacheURL.Scheme, "http")
-	cmpStrings(t, "cache.host", cfg.CacheURL.Host, "prebidcache.net")
-	cmpStrings(t, "cache.query", cfg.CacheURL.Query, "uuid=%PBS_CACHE_UUID%")
-	cmpStrings(t, "external_cache.scheme", cfg.ExtCacheURL.Scheme, "https")
-	cmpStrings(t, "external_cache.host", cfg.ExtCacheURL.Host, "www.externalprebidcache.net")
-	cmpStrings(t, "external_cache.path", cfg.ExtCacheURL.Path, "/endpoints/cache")
-	cmpInts(t, "http_client.max_connections_per_host", cfg.Client.MaxConnsPerHost, 10)
-	cmpInts(t, "http_client.max_idle_connections", cfg.Client.MaxIdleConns, 500)
-	cmpInts(t, "http_client.max_idle_connections_per_host", cfg.Client.MaxIdleConnsPerHost, 20)
-	cmpInts(t, "http_client.idle_connection_timeout_seconds", cfg.Client.IdleConnTimeout, 30)
-	cmpInts(t, "http_client_cache.max_connections_per_host", cfg.CacheClient.MaxConnsPerHost, 5)
-	cmpInts(t, "http_client_cache.max_idle_connections", cfg.CacheClient.MaxIdleConns, 1)
-	cmpInts(t, "http_client_cache.max_idle_connections_per_host", cfg.CacheClient.MaxIdleConnsPerHost, 2)
-	cmpInts(t, "http_client_cache.idle_connection_timeout_seconds", cfg.CacheClient.IdleConnTimeout, 3)
-	cmpInts(t, "gdpr.host_vendor_id", cfg.GDPR.HostVendorID, 15)
-	cmpStrings(t, "gdpr.default_value", cfg.GDPR.DefaultValue, "1")
-	cmpStrings(t, "host_schain_node.asi", cfg.HostSChainNode.ASI, "pbshostcompany.com")
-	cmpStrings(t, "host_schain_node.sid", cfg.HostSChainNode.SID, "00001")
-	cmpStrings(t, "host_schain_node.rid", cfg.HostSChainNode.RID, "BidRequest")
-	cmpInt8s(t, "host_schain_node.hp", cfg.HostSChainNode.HP, &int8One)
-	cmpStrings(t, "datacenter", cfg.DataCenter, "1")
-	cmpStrings(t, "validations.banner_creative_max_size", cfg.Validations.BannerCreativeMaxSize, "skip")
-	cmpStrings(t, "validations.secure_markup", cfg.Validations.SecureMarkup, "skip")
-	cmpInts(t, "validations.max_creative_width", int(cfg.Validations.MaxCreativeWidth), 0)
-	cmpInts(t, "validations.max_creative_height", int(cfg.Validations.MaxCreativeHeight), 0)
+	cmpStrings(t, "cookie domain", "cookies.prebid.org", cfg.HostCookie.Domain)
+	cmpStrings(t, "cookie name", "userid", cfg.HostCookie.CookieName)
+	cmpStrings(t, "cookie family", "prebid", cfg.HostCookie.Family)
+	cmpStrings(t, "opt out", "http://prebid.org/optout", cfg.HostCookie.OptOutURL)
+	cmpStrings(t, "opt in", "http://prebid.org/optin", cfg.HostCookie.OptInURL)
+	cmpStrings(t, "external url", "http://prebid-server.prebid.org/", cfg.ExternalURL)
+	cmpStrings(t, "host", "prebid-server.prebid.org", cfg.Host)
+	cmpInts(t, "port", 1234, cfg.Port)
+	cmpInts(t, "admin_port", 5678, cfg.AdminPort)
+	cmpInts(t, "garbage_collector_threshold", 1, cfg.GarbageCollectorThreshold)
+	cmpInts(t, "auction_timeouts_ms.default", 50, int(cfg.AuctionTimeouts.Default))
+	cmpInts(t, "auction_timeouts_ms.max", 123, int(cfg.AuctionTimeouts.Max))
+	cmpStrings(t, "cache.scheme", "http", cfg.CacheURL.Scheme)
+	cmpStrings(t, "cache.host", "prebidcache.net", cfg.CacheURL.Host)
+	cmpStrings(t, "cache.query", "uuid=%PBS_CACHE_UUID%", cfg.CacheURL.Query)
+	cmpStrings(t, "external_cache.scheme", "https", cfg.ExtCacheURL.Scheme)
+	cmpStrings(t, "external_cache.host", "www.externalprebidcache.net", cfg.ExtCacheURL.Host)
+	cmpStrings(t, "external_cache.path", "/endpoints/cache", cfg.ExtCacheURL.Path)
+	cmpInts(t, "http_client.max_connections_per_host", 10, cfg.Client.MaxConnsPerHost)
+	cmpInts(t, "http_client.max_idle_connections", 500, cfg.Client.MaxIdleConns)
+	cmpInts(t, "http_client.max_idle_connections_per_host", 20, cfg.Client.MaxIdleConnsPerHost)
+	cmpInts(t, "http_client.idle_connection_timeout_seconds", 30, cfg.Client.IdleConnTimeout)
+	cmpInts(t, "http_client_cache.max_connections_per_host", 5, cfg.CacheClient.MaxConnsPerHost)
+	cmpInts(t, "http_client_cache.max_idle_connections", 1, cfg.CacheClient.MaxIdleConns)
+	cmpInts(t, "http_client_cache.max_idle_connections_per_host", 2, cfg.CacheClient.MaxIdleConnsPerHost)
+	cmpInts(t, "http_client_cache.idle_connection_timeout_seconds", 3, cfg.CacheClient.IdleConnTimeout)
+	cmpInts(t, "gdpr.host_vendor_id", 15, cfg.GDPR.HostVendorID)
+	cmpStrings(t, "gdpr.default_value", "1", cfg.GDPR.DefaultValue)
+	cmpStrings(t, "host_schain_node.asi", "pbshostcompany.com", cfg.HostSChainNode.ASI)
+	cmpStrings(t, "host_schain_node.sid", "00001", cfg.HostSChainNode.SID)
+	cmpStrings(t, "host_schain_node.rid", "BidRequest", cfg.HostSChainNode.RID)
+	cmpInt8s(t, "host_schain_node.hp", &int8One, cfg.HostSChainNode.HP)
+	cmpStrings(t, "datacenter", "1", cfg.DataCenter)
+	cmpStrings(t, "validations.banner_creative_max_size", "skip", cfg.Validations.BannerCreativeMaxSize)
+	cmpStrings(t, "validations.secure_markup", "skip", cfg.Validations.SecureMarkup)
+	cmpInts(t, "validations.max_creative_width", 0, int(cfg.Validations.MaxCreativeWidth))
+	cmpInts(t, "validations.max_creative_height", 0, int(cfg.Validations.MaxCreativeHeight))
 
 	//Assert the price floor values
-	cmpBools(t, "price_floors.enabled", cfg.PriceFloors.Enabled, true)
-	cmpBools(t, "account_defaults.price_floors.enabled", cfg.AccountDefaults.PriceFloors.Enabled, true)
-	cmpInts(t, "account_defaults.price_floors.enforce_floors_rate", cfg.AccountDefaults.PriceFloors.EnforceFloorsRate, 50)
-	cmpBools(t, "account_defaults.price_floors.adjust_for_bid_adjustment", cfg.AccountDefaults.PriceFloors.AdjustForBidAdjustment, false)
-	cmpBools(t, "account_defaults.price_floors.enforce_deal_floors", cfg.AccountDefaults.PriceFloors.EnforceDealFloors, true)
-	cmpBools(t, "account_defaults.price_floors.use_dynamic_data", cfg.AccountDefaults.PriceFloors.UseDynamicData, true)
-	cmpInts(t, "account_defaults.price_floors.max_rules", cfg.AccountDefaults.PriceFloors.MaxRule, 120)
-	cmpInts(t, "account_defaults.price_floors.max_schema_dims", cfg.AccountDefaults.PriceFloors.MaxSchemaDims, 5)
+	cmpBools(t, "price_floors.enabled", true, cfg.PriceFloors.Enabled)
+	cmpBools(t, "account_defaults.price_floors.enabled", true, cfg.AccountDefaults.PriceFloors.Enabled)
+	cmpInts(t, "account_defaults.price_floors.enforce_floors_rate", 50, cfg.AccountDefaults.PriceFloors.EnforceFloorsRate)
+	cmpBools(t, "account_defaults.price_floors.adjust_for_bid_adjustment", false, cfg.AccountDefaults.PriceFloors.AdjustForBidAdjustment)
+	cmpBools(t, "account_defaults.price_floors.enforce_deal_floors", true, cfg.AccountDefaults.PriceFloors.EnforceDealFloors)
+	cmpBools(t, "account_defaults.price_floors.use_dynamic_data", true, cfg.AccountDefaults.PriceFloors.UseDynamicData)
+	cmpInts(t, "account_defaults.price_floors.max_rules", 120, cfg.AccountDefaults.PriceFloors.MaxRule)
+	cmpInts(t, "account_defaults.price_floors.max_schema_dims", 5, cfg.AccountDefaults.PriceFloors.MaxSchemaDims)
 
 	//Assert the NonStandardPublishers was correctly unmarshalled
 	assert.Equal(t, []string{"pub1", "pub2"}, cfg.GDPR.NonStandardPublishers, "gdpr.non_standard_publishers")
@@ -552,16 +552,16 @@ func TestFullConfig(t *testing.T) {
 	assert.Equal(t, []string{"eea1", "eea2"}, cfg.GDPR.EEACountries, "gdpr.eea_countries")
 	assert.Equal(t, map[string]struct{}{"eea1": {}, "eea2": {}}, cfg.GDPR.EEACountriesMap, "gdpr.eea_countries Hash Map")
 
-	cmpBools(t, "ccpa.enforce", cfg.CCPA.Enforce, true)
-	cmpBools(t, "lmt.enforce", cfg.LMT.Enforce, true)
+	cmpBools(t, "ccpa.enforce", true, cfg.CCPA.Enforce)
+	cmpBools(t, "lmt.enforce", true, cfg.LMT.Enforce)
 
 	//Assert the NonStandardPublishers was correctly unmarshalled
-	cmpStrings(t, "blacklisted_apps", cfg.BlacklistedApps[0], "spamAppID")
-	cmpStrings(t, "blacklisted_apps", cfg.BlacklistedApps[1], "sketchy-app-id")
+	cmpStrings(t, "blacklisted_apps", "spamAppID", cfg.BlacklistedApps[0])
+	cmpStrings(t, "blacklisted_apps", "sketchy-app-id", cfg.BlacklistedApps[1])
 
 	//Assert the BlacklistedAppMap hash table was built correctly
 	for i := 0; i < len(cfg.BlacklistedApps); i++ {
-		cmpBools(t, "cfg.BlacklistedAppMap", cfg.BlacklistedAppMap[cfg.BlacklistedApps[i]], true)
+		cmpBools(t, "cfg.BlacklistedAppMap", true, cfg.BlacklistedAppMap[cfg.BlacklistedApps[i]])
 	}
 
 	//Assert purpose VendorExceptionMap hash tables were built correctly
@@ -681,40 +681,40 @@ func TestFullConfig(t *testing.T) {
 	}
 	assert.Equal(t, expectedTCF2, cfg.GDPR.TCF2, "gdpr.tcf2")
 
-	cmpStrings(t, "currency_converter.fetch_url", cfg.CurrencyConverter.FetchURL, "https://currency.prebid.org")
-	cmpInts(t, "currency_converter.fetch_interval_seconds", cfg.CurrencyConverter.FetchIntervalSeconds, 1800)
-	cmpStrings(t, "recaptcha_secret", cfg.RecaptchaSecret, "asdfasdfasdfasdf")
-	cmpStrings(t, "metrics.influxdb.host", cfg.Metrics.Influxdb.Host, "upstream:8232")
-	cmpStrings(t, "metrics.influxdb.database", cfg.Metrics.Influxdb.Database, "metricsdb")
-	cmpStrings(t, "metrics.influxdb.measurement", cfg.Metrics.Influxdb.Measurement, "anyMeasurement")
-	cmpStrings(t, "metrics.influxdb.username", cfg.Metrics.Influxdb.Username, "admin")
-	cmpStrings(t, "metrics.influxdb.password", cfg.Metrics.Influxdb.Password, "admin1324")
-	cmpBools(t, "metrics.influxdb.align_timestamps", cfg.Metrics.Influxdb.AlignTimestamps, true)
-	cmpInts(t, "metrics.influxdb.metric_send_interval", cfg.Metrics.Influxdb.MetricSendInterval, 30)
-	cmpStrings(t, "", cfg.CacheURL.GetBaseURL(), "http://prebidcache.net")
-	cmpStrings(t, "", cfg.GetCachedAssetURL("a0eebc99-9c0b-4ef8-bb00-6bb9bd380a11"), "http://prebidcache.net/cache?uuid=a0eebc99-9c0b-4ef8-bb00-6bb9bd380a11")
-	cmpBools(t, "account_required", cfg.AccountRequired, true)
-	cmpBools(t, "auto_gen_source_tid", cfg.AutoGenSourceTID, false)
-	cmpBools(t, "account_adapter_details", cfg.Metrics.Disabled.AccountAdapterDetails, true)
-	cmpBools(t, "account_debug", cfg.Metrics.Disabled.AccountDebug, false)
-	cmpBools(t, "account_stored_responses", cfg.Metrics.Disabled.AccountStoredResponses, false)
-	cmpBools(t, "adapter_connections_metrics", cfg.Metrics.Disabled.AdapterConnectionMetrics, true)
-	cmpBools(t, "adapter_gdpr_request_blocked", cfg.Metrics.Disabled.AdapterGDPRRequestBlocked, true)
-	cmpStrings(t, "certificates_file", cfg.PemCertsFile, "/etc/ssl/cert.pem")
-	cmpStrings(t, "request_validation.ipv4_private_networks", cfg.RequestValidation.IPv4PrivateNetworks[0], "1.1.1.0/24")
-	cmpStrings(t, "request_validation.ipv6_private_networks", cfg.RequestValidation.IPv6PrivateNetworks[0], "1111::/16")
-	cmpStrings(t, "request_validation.ipv6_private_networks", cfg.RequestValidation.IPv6PrivateNetworks[1], "2222::/16")
-	cmpBools(t, "generate_bid_id", cfg.GenerateBidID, true)
-	cmpStrings(t, "debug.override_token", cfg.Debug.OverrideToken, "")
-	cmpStrings(t, "experiment.adscert.mode", cfg.Experiment.AdCerts.Mode, "inprocess")
-	cmpStrings(t, "experiment.adscert.inprocess.origin", cfg.Experiment.AdCerts.InProcess.Origin, "http://test.com")
-	cmpStrings(t, "experiment.adscert.inprocess.key", cfg.Experiment.AdCerts.InProcess.PrivateKey, "ABC123")
-	cmpInts(t, "experiment.adscert.inprocess.domain_check_interval_seconds", cfg.Experiment.AdCerts.InProcess.DNSCheckIntervalInSeconds, 40)
-	cmpInts(t, "experiment.adscert.inprocess.domain_renewal_interval_seconds", cfg.Experiment.AdCerts.InProcess.DNSRenewalIntervalInSeconds, 60)
-	cmpStrings(t, "experiment.adscert.remote.url", cfg.Experiment.AdCerts.Remote.Url, "")
-	cmpInts(t, "experiment.adscert.remote.signing_timeout_ms", cfg.Experiment.AdCerts.Remote.SigningTimeoutMs, 10)
-	cmpBools(t, "hooks.enabled", cfg.Hooks.Enabled, true)
-	cmpBools(t, "account_modules_metrics", cfg.Metrics.Disabled.AccountModulesMetrics, true)
+	cmpStrings(t, "currency_converter.fetch_url", "https://currency.prebid.org", cfg.CurrencyConverter.FetchURL)
+	cmpInts(t, "currency_converter.fetch_interval_seconds", 1800, cfg.CurrencyConverter.FetchIntervalSeconds)
+	cmpStrings(t, "recaptcha_secret", "asdfasdfasdfasdf", cfg.RecaptchaSecret)
+	cmpStrings(t, "metrics.influxdb.host", "upstream:8232", cfg.Metrics.Influxdb.Host)
+	cmpStrings(t, "metrics.influxdb.database", "metricsdb", cfg.Metrics.Influxdb.Database)
+	cmpStrings(t, "metrics.influxdb.measurement", "anyMeasurement", cfg.Metrics.Influxdb.Measurement)
+	cmpStrings(t, "metrics.influxdb.username", "admin", cfg.Metrics.Influxdb.Username)
+	cmpStrings(t, "metrics.influxdb.password", "admin1324", cfg.Metrics.Influxdb.Password)
+	cmpBools(t, "metrics.influxdb.align_timestamps", true, cfg.Metrics.Influxdb.AlignTimestamps)
+	cmpInts(t, "metrics.influxdb.metric_send_interval", 30, cfg.Metrics.Influxdb.MetricSendInterval)
+	cmpStrings(t, "", "http://prebidcache.net", cfg.CacheURL.GetBaseURL())
+	cmpStrings(t, "", "http://prebidcache.net/cache?uuid=a0eebc99-9c0b-4ef8-bb00-6bb9bd380a11", cfg.GetCachedAssetURL("a0eebc99-9c0b-4ef8-bb00-6bb9bd380a11"))
+	cmpBools(t, "account_required", true, cfg.AccountRequired)
+	cmpBools(t, "auto_gen_source_tid", false, cfg.AutoGenSourceTID)
+	cmpBools(t, "account_adapter_details", true, cfg.Metrics.Disabled.AccountAdapterDetails)
+	cmpBools(t, "account_debug", false, cfg.Metrics.Disabled.AccountDebug)
+	cmpBools(t, "account_stored_responses", false, cfg.Metrics.Disabled.AccountStoredResponses)
+	cmpBools(t, "adapter_connections_metrics", true, cfg.Metrics.Disabled.AdapterConnectionMetrics)
+	cmpBools(t, "adapter_gdpr_request_blocked", true, cfg.Metrics.Disabled.AdapterGDPRRequestBlocked)
+	cmpStrings(t, "certificates_file", "/etc/ssl/cert.pem", cfg.PemCertsFile)
+	cmpStrings(t, "request_validation.ipv4_private_networks", "1.1.1.0/24", cfg.RequestValidation.IPv4PrivateNetworks[0])
+	cmpStrings(t, "request_validation.ipv6_private_networks", "1111::/16", cfg.RequestValidation.IPv6PrivateNetworks[0])
+	cmpStrings(t, "request_validation.ipv6_private_networks", "2222::/16", cfg.RequestValidation.IPv6PrivateNetworks[1])
+	cmpBools(t, "generate_bid_id", true, cfg.GenerateBidID)
+	cmpStrings(t, "debug.override_token", "", cfg.Debug.OverrideToken)
+	cmpStrings(t, "experiment.adscert.mode", "inprocess", cfg.Experiment.AdCerts.Mode)
+	cmpStrings(t, "experiment.adscert.inprocess.origin", "http://test.com", cfg.Experiment.AdCerts.InProcess.Origin)
+	cmpStrings(t, "experiment.adscert.inprocess.key", "ABC123", cfg.Experiment.AdCerts.InProcess.PrivateKey)
+	cmpInts(t, "experiment.adscert.inprocess.domain_check_interval_seconds", 40, cfg.Experiment.AdCerts.InProcess.DNSCheckIntervalInSeconds)
+	cmpInts(t, "experiment.adscert.inprocess.domain_renewal_interval_seconds", 60, cfg.Experiment.AdCerts.InProcess.DNSRenewalIntervalInSeconds)
+	cmpStrings(t, "experiment.adscert.remote.url", "", cfg.Experiment.AdCerts.Remote.Url)
+	cmpInts(t, "experiment.adscert.remote.signing_timeout_ms", 10, cfg.Experiment.AdCerts.Remote.SigningTimeoutMs)
+	cmpBools(t, "hooks.enabled", true, cfg.Hooks.Enabled)
+	cmpBools(t, "account_modules_metrics", true, cfg.Metrics.Disabled.AccountModulesMetrics)
 }
 
 func TestValidateConfig(t *testing.T) {
diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go
index 2d4341d0..8eb80672 100644
--- a/endpoints/openrtb2/amp_auction_test.go
+++ b/endpoints/openrtb2/amp_auction_test.go
@@ -1420,7 +1420,7 @@ var expectedErrorsFromHoldAuction map[openrtb_ext.BidderName][]openrtb_ext.ExtBi
 	},
 }
 
-func (m *mockAmpExchange) HoldAuction(ctx context.Context, auctionRequest exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
+func (m *mockAmpExchange) HoldAuction(ctx context.Context, auctionRequest *exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
 	r := auctionRequest.BidRequestWrapper
 	m.lastRequest = r.BidRequest
 
@@ -1459,7 +1459,7 @@ func (m *mockAmpExchange) HoldAuction(ctx context.Context, auctionRequest exchan
 
 type mockAmpExchangeWarnings struct{}
 
-func (m *mockAmpExchangeWarnings) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
+func (m *mockAmpExchangeWarnings) HoldAuction(ctx context.Context, r *exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
 	response := &openrtb2.BidResponse{
 		SeatBid: []openrtb2.SeatBid{{
 			Bid: []openrtb2.Bid{{
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index 35ad9ee1..f291d2e8 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -20,11 +20,13 @@ import (
 	"github.com/prebid/openrtb/v19/native1"
 	nativeRequests "github.com/prebid/openrtb/v19/native1/request"
 	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/analytics"
 	"github.com/prebid/prebid-server/hooks"
 	"github.com/prebid/prebid-server/hooks/hookexecution"
 	"github.com/prebid/prebid-server/hooks/hookstage"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 
 	analyticsConf "github.com/prebid/prebid-server/analytics/config"
 	"github.com/prebid/prebid-server/config"
@@ -3588,6 +3590,11 @@ func TestAuctionWarnings(t *testing.T) {
 			file:            "us-privacy-conflict.json",
 			expectedWarning: "regs.us_privacy consent does not match uspv1 in GPP, using regs.gpp",
 		},
+		{
+			name:            "empty-gppsid-array-conflicts-with-regs-gdpr", // gdpr set to 1, an empty non-nil gpp_sid array doesn't match
+			file:            "empty-gppsid-conflict.json",
+			expectedWarning: "regs.gdpr signal conflicts with GPP (regs.gpp_sid) and will be ignored",
+		},
 		{
 			name:            "gdpr-signals-conflict", // gdpr signals do not match
 			file:            "gdpr-conflict.json",
@@ -5335,3 +5342,12 @@ type mockStageExecutor struct {
 func (e mockStageExecutor) GetOutcomes() []hookexecution.StageOutcome {
 	return e.outcomes
 }
+
+func TestRecordResponsePreparationMetrics(t *testing.T) {
+	mbi := map[openrtb_ext.BidderName]adapters.MakeBidsTimeInfo{
+		openrtb_ext.BidderAppnexus: {Durations: []time.Duration{10, 15}, AfterMakeBidsStartTime: time.Now()},
+	}
+	mockMetricEngine := &metrics.MetricsEngineMock{}
+	mockMetricEngine.On("RecordOverheadTime", metrics.MakeAuctionResponse, mock.Anything)
+	recordResponsePreparationMetrics(mbi, mockMetricEngine)
+}
diff --git a/endpoints/openrtb2/test_utils.go b/endpoints/openrtb2/test_utils.go
index 094250fa..0ad47982 100644
--- a/endpoints/openrtb2/test_utils.go
+++ b/endpoints/openrtb2/test_utils.go
@@ -94,7 +94,7 @@ type testConfigValues struct {
 
 type brokenExchange struct{}
 
-func (e *brokenExchange) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
+func (e *brokenExchange) HoldAuction(ctx context.Context, r *exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
 	return nil, errors.New("Critical, unrecoverable error.")
 }
 
@@ -848,7 +848,7 @@ type mockExchange struct {
 	lastRequest *openrtb2.BidRequest
 }
 
-func (m *mockExchange) HoldAuction(ctx context.Context, auctionRequest exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
+func (m *mockExchange) HoldAuction(ctx context.Context, auctionRequest *exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
 	r := auctionRequest.BidRequestWrapper
 	m.lastRequest = r.BidRequest
 	return &openrtb2.BidResponse{
@@ -885,8 +885,8 @@ type warningsCheckExchange struct {
 	auctionRequest exchange.AuctionRequest
 }
 
-func (e *warningsCheckExchange) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
-	e.auctionRequest = r
+func (e *warningsCheckExchange) HoldAuction(ctx context.Context, r *exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
+	e.auctionRequest = *r
 	return nil, nil
 }
 
@@ -896,7 +896,7 @@ type nobidExchange struct {
 	gotRequest *openrtb2.BidRequest
 }
 
-func (e *nobidExchange) HoldAuction(ctx context.Context, auctionRequest exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
+func (e *nobidExchange) HoldAuction(ctx context.Context, auctionRequest *exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
 	r := auctionRequest.BidRequestWrapper
 	e.gotRequest = r.BidRequest
 	return &openrtb2.BidResponse{
@@ -1181,7 +1181,7 @@ type exchangeTestWrapper struct {
 	actualValidatedBidReq *openrtb2.BidRequest
 }
 
-func (te *exchangeTestWrapper) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
+func (te *exchangeTestWrapper) HoldAuction(ctx context.Context, r *exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
 
 	// rebuild/resync the request in the request wrapper.
 	if err := r.BidRequestWrapper.RebuildRequest(); err != nil {
diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go
index 4ad69e53..b75a7b99 100644
--- a/endpoints/openrtb2/video_auction_test.go
+++ b/endpoints/openrtb2/video_auction_test.go
@@ -1354,7 +1354,7 @@ type mockExchangeVideo struct {
 	cache       *mockCacheClient
 }
 
-func (m *mockExchangeVideo) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
+func (m *mockExchangeVideo) HoldAuction(ctx context.Context, r *exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
 	if err := r.BidRequestWrapper.RebuildRequest(); err != nil {
 		return nil, err
 	}
@@ -1394,7 +1394,7 @@ type mockExchangeAppendBidderNames struct {
 	cache       *mockCacheClient
 }
 
-func (m *mockExchangeAppendBidderNames) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
+func (m *mockExchangeAppendBidderNames) HoldAuction(ctx context.Context, r *exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
 	m.lastRequest = r.BidRequestWrapper.BidRequest
 	if debugLog != nil && debugLog.Enabled {
 		m.cache.called = true
@@ -1430,7 +1430,7 @@ type mockExchangeVideoNoBids struct {
 	cache       *mockCacheClient
 }
 
-func (m *mockExchangeVideoNoBids) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
+func (m *mockExchangeVideoNoBids) HoldAuction(ctx context.Context, r *exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
 	m.lastRequest = r.BidRequestWrapper.BidRequest
 	return &openrtb2.BidResponse{
 		SeatBid: []openrtb2.SeatBid{{}},
diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go
index dca2b4f7..2aa03573 100644
--- a/exchange/bidder_test.go
+++ b/exchange/bidder_test.go
@@ -108,7 +108,11 @@ func TestSingleBidder(t *testing.T) {
 			addCallSignHeader:   false,
 			bidAdjustments:      bidAdjustments,
 		}
-		seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{})
+		extraInfo := &adapters.ExtraRequestInfo{}
+		seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{})
+		assert.NotEmpty(t, extraInfo.MakeBidsTimeInfo.Durations)
+		assert.False(t, extraInfo.MakeBidsTimeInfo.AfterMakeBidsStartTime.IsZero())
+
 		assert.Len(t, seatBids, 1)
 		seatBid := seatBids[0]
 
@@ -230,7 +234,10 @@ func TestSingleBidderGzip(t *testing.T) {
 			addCallSignHeader:   false,
 			bidAdjustments:      bidAdjustments,
 		}
-		seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{})
+		extraInfo := &adapters.ExtraRequestInfo{}
+		seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{})
+		assert.NotEmpty(t, extraInfo.MakeBidsTimeInfo.Durations)
+		assert.False(t, extraInfo.MakeBidsTimeInfo.AfterMakeBidsStartTime.IsZero())
 		assert.Len(t, seatBids, 1)
 		seatBid := seatBids[0]
 
@@ -330,8 +337,8 @@ func TestRequestBidRemovesSensitiveHeaders(t *testing.T) {
 		addCallSignHeader:   false,
 		bidAdjustments:      bidAdjustments,
 	}
-	seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{})
-
+	extraInfo := &adapters.ExtraRequestInfo{}
+	seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{})
 	expectedHttpCalls := []*openrtb_ext.ExtHttpCall{
 		{
 			Uri:            server.URL,
@@ -344,6 +351,8 @@ func TestRequestBidRemovesSensitiveHeaders(t *testing.T) {
 
 	assert.Empty(t, errs)
 	assert.Len(t, seatBids, 1)
+	assert.NotEmpty(t, extraInfo.MakeBidsTimeInfo.Durations)
+	assert.False(t, extraInfo.MakeBidsTimeInfo.AfterMakeBidsStartTime.IsZero())
 	assert.ElementsMatch(t, seatBids[0].HttpCalls, expectedHttpCalls)
 }
 
@@ -382,7 +391,8 @@ func TestSetGPCHeader(t *testing.T) {
 		addCallSignHeader:   false,
 		bidAdjustments:      bidAdjustments,
 	}
-	seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{GlobalPrivacyControlHeader: "1"}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{})
+	extraInfo := &adapters.ExtraRequestInfo{GlobalPrivacyControlHeader: "1"}
+	seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{})
 
 	expectedHttpCall := []*openrtb_ext.ExtHttpCall{
 		{
@@ -396,6 +406,8 @@ func TestSetGPCHeader(t *testing.T) {
 
 	assert.Empty(t, errs)
 	assert.Len(t, seatBids, 1)
+	assert.NotEmpty(t, extraInfo.MakeBidsTimeInfo.Durations)
+	assert.False(t, extraInfo.MakeBidsTimeInfo.AfterMakeBidsStartTime.IsZero())
 	assert.ElementsMatch(t, seatBids[0].HttpCalls, expectedHttpCall)
 }
 
@@ -432,7 +444,8 @@ func TestSetGPCHeaderNil(t *testing.T) {
 		addCallSignHeader:   false,
 		bidAdjustments:      bidAdjustments,
 	}
-	seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{GlobalPrivacyControlHeader: "1"}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{})
+	extraInfo := &adapters.ExtraRequestInfo{GlobalPrivacyControlHeader: "1"}
+	seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{})
 
 	expectedHttpCall := []*openrtb_ext.ExtHttpCall{
 		{
@@ -445,6 +458,8 @@ func TestSetGPCHeaderNil(t *testing.T) {
 	}
 
 	assert.Empty(t, errs)
+	assert.NotEmpty(t, extraInfo.MakeBidsTimeInfo.Durations)
+	assert.False(t, extraInfo.MakeBidsTimeInfo.AfterMakeBidsStartTime.IsZero())
 	assert.Len(t, seatBids, 1)
 	assert.ElementsMatch(t, seatBids[0].HttpCalls, expectedHttpCall)
 }
@@ -547,7 +562,7 @@ func TestBidderTimeout(t *testing.T) {
 	callInfo := bidder.doRequest(ctx, &adapters.RequestData{
 		Method: "POST",
 		Uri:    server.URL,
-	})
+	}, time.Now())
 	if callInfo.err == nil {
 		t.Errorf("The bidder should report an error if the context has expired already.")
 	}
@@ -566,7 +581,7 @@ func TestInvalidRequest(t *testing.T) {
 
 	callInfo := bidder.doRequest(context.Background(), &adapters.RequestData{
 		Method: "\"", // force http.NewRequest() to fail
-	})
+	}, time.Now())
 	if callInfo.err == nil {
 		t.Errorf("bidderAdapter.doRequest should return an error if the request data is malformed.")
 	}
@@ -590,7 +605,7 @@ func TestConnectionClose(t *testing.T) {
 	callInfo := bidder.doRequest(context.Background(), &adapters.RequestData{
 		Method: "POST",
 		Uri:    server.URL,
-	})
+	}, time.Now())
 	if callInfo.err == nil {
 		t.Errorf("bidderAdapter.doRequest should return an error if the connection closes unexpectedly.")
 	}
@@ -2004,15 +2019,16 @@ func TestCallRecordAdapterConnections(t *testing.T) {
 		bidResponse: &adapters.BidderResponse{},
 	}
 
-	// setup a mock metrics engine and its expectation
-	metrics := &metrics.MetricsEngineMock{}
+	// setup a mock mockMetricEngine engine and its expectation
+	mockMetricEngine := &metrics.MetricsEngineMock{}
 	expectedAdapterName := openrtb_ext.BidderAppnexus
 	compareConnWaitTime := func(dur time.Duration) bool { return dur.Nanoseconds() > 0 }
 
-	metrics.On("RecordAdapterConnections", expectedAdapterName, false, mock.MatchedBy(compareConnWaitTime)).Once()
+	mockMetricEngine.On("RecordAdapterConnections", expectedAdapterName, false, mock.MatchedBy(compareConnWaitTime)).Once()
+	mockMetricEngine.On("RecordOverheadTime", metrics.PreBidder, mock.Anything).Once()
 
 	// Run requestBid using an http.Client with a mock handler
-	bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, metrics, openrtb_ext.BidderAppnexus, nil, "")
+	bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, mockMetricEngine, openrtb_ext.BidderAppnexus, nil, "")
 	currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0))
 
 	bidderReq := BidderRequest{
@@ -2025,13 +2041,13 @@ func TestCallRecordAdapterConnections(t *testing.T) {
 		addCallSignHeader:   false,
 		bidAdjustments:      bidAdjustments,
 	}
-	_, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{})
+	_, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{PbsEntryPoint: metrics.ReqTypeORTB2Web}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{})
 
 	// Assert no errors
 	assert.Equal(t, 0, len(errs), "bidder.requestBid returned errors %v \n", errs)
 
 	// Assert RecordAdapterConnections() was called with the parameters we expected
-	metrics.AssertExpectations(t)
+	mockMetricEngine.AssertExpectations(t)
 }
 
 type DNSDoneTripper struct{}
@@ -2070,6 +2086,7 @@ func TestCallRecordDNSTime(t *testing.T) {
 	// setup a mock metrics engine and its expectation
 	metricsMock := &metrics.MetricsEngineMock{}
 	metricsMock.Mock.On("RecordDNSTime", mock.Anything).Return()
+	metricsMock.On("RecordOverheadTime", metrics.PreBidder, mock.Anything).Once()
 
 	// Instantiate the bidder that will send the request. We'll make sure to use an
 	// http.Client that runs our mock RoundTripper so DNSDone(httptrace.DNSDoneInfo{})
@@ -2081,7 +2098,7 @@ func TestCallRecordDNSTime(t *testing.T) {
 	}
 
 	// Run test
-	bidder.doRequest(context.Background(), &adapters.RequestData{Method: "POST", Uri: "http://www.example.com/"})
+	bidder.doRequest(context.Background(), &adapters.RequestData{Method: "POST", Uri: "http://www.example.com/"}, time.Now())
 
 	// Tried one or another, none seem to work without panicking
 	metricsMock.AssertExpectations(t)
@@ -2091,6 +2108,7 @@ func TestCallRecordTLSHandshakeTime(t *testing.T) {
 	// setup a mock metrics engine and its expectation
 	metricsMock := &metrics.MetricsEngineMock{}
 	metricsMock.Mock.On("RecordTLSHandshakeTime", mock.Anything).Return()
+	metricsMock.On("RecordOverheadTime", metrics.PreBidder, mock.Anything).Once()
 
 	// Instantiate the bidder that will send the request. We'll make sure to use an
 	// http.Client that runs our mock RoundTripper so DNSDone(httptrace.DNSDoneInfo{})
@@ -2102,7 +2120,7 @@ func TestCallRecordTLSHandshakeTime(t *testing.T) {
 	}
 
 	// Run test
-	bidder.doRequest(context.Background(), &adapters.RequestData{Method: "POST", Uri: "http://www.example.com/"})
+	bidder.doRequest(context.Background(), &adapters.RequestData{Method: "POST", Uri: "http://www.example.com/"}, time.Now())
 
 	// Tried one or another, none seem to work without panicking
 	metricsMock.AssertExpectations(t)
@@ -2190,7 +2208,7 @@ func TestTimeoutNotificationOn(t *testing.T) {
 		loggerBuffer.WriteString(fmt.Sprintf(fmt.Sprintln(msg), args...))
 	}
 
-	bidderAdapter.doRequestImpl(ctx, &bidRequest, logger)
+	bidderAdapter.doRequestImpl(ctx, &bidRequest, logger, time.Now())
 
 	// Wait a little longer than the 205ms mock server sleep.
 	time.Sleep(210 * time.Millisecond)
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index 3b2abb21..bef48be1 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -362,7 +362,7 @@ func TestDebugBehaviour(t *testing.T) {
 			bidRequest.Ext = nil
 		}
 
-		auctionRequest := AuctionRequest{
+		auctionRequest := &AuctionRequest{
 			BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: bidRequest},
 			Account:           config.Account{DebugAllow: test.debugData.accountLevelDebugAllowed},
 			UserSyncs:         &emptyUsersync{},
@@ -533,7 +533,7 @@ func TestTwoBiddersDebugDisabledAndEnabled(t *testing.T) {
 
 		bidRequest.Ext = json.RawMessage(`{"prebid":{"debug":true}}`)
 
-		auctionRequest := AuctionRequest{
+		auctionRequest := &AuctionRequest{
 			BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: bidRequest},
 			Account:           config.Account{DebugAllow: true},
 			UserSyncs:         &emptyUsersync{},
@@ -716,7 +716,7 @@ func TestOverrideWithCustomCurrency(t *testing.T) {
 		// Set bidRequest currency list
 		mockBidRequest.Cur = []string{test.in.bidRequestCurrency}
 
-		auctionRequest := AuctionRequest{
+		auctionRequest := &AuctionRequest{
 			BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: mockBidRequest},
 			Account:           config.Account{},
 			UserSyncs:         &emptyUsersync{},
@@ -811,7 +811,7 @@ func TestAdapterCurrency(t *testing.T) {
 	}
 
 	// Run Auction
-	auctionRequest := AuctionRequest{
+	auctionRequest := &AuctionRequest{
 		BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: request},
 		Account:           config.Account{},
 		UserSyncs:         &emptyUsersync{},
@@ -984,7 +984,7 @@ func TestFloorsSignalling(t *testing.T) {
 	}
 
 	for _, test := range testCases {
-		auctionRequest := AuctionRequest{
+		auctionRequest := &AuctionRequest{
 			BidRequestWrapper: test.req,
 			Account:           config.Account{DebugAllow: true, PriceFloors: config.AccountPriceFloors{Enabled: test.floorsEnable, MaxRule: 100, MaxSchemaDims: 5}},
 			UserSyncs:         &emptyUsersync{},
@@ -1368,7 +1368,7 @@ func TestReturnCreativeEndToEnd(t *testing.T) {
 		for _, test := range testGroup.testCases {
 			mockBidRequest.Ext = test.inExt
 
-			auctionRequest := AuctionRequest{
+			auctionRequest := &AuctionRequest{
 				BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: mockBidRequest},
 				Account:           config.Account{},
 				UserSyncs:         &emptyUsersync{},
@@ -2042,7 +2042,7 @@ func TestRaceIntegration(t *testing.T) {
 
 	currencyConverter := currency.NewRateConverter(&http.Client{}, "", time.Duration(0))
 
-	auctionRequest := AuctionRequest{
+	auctionRequest := &AuctionRequest{
 		BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: getTestBuildRequest(t)},
 		Account:           config.Account{},
 		UserSyncs:         &emptyUsersync{},
@@ -2267,7 +2267,7 @@ func TestPanicRecoveryHighLevel(t *testing.T) {
 		}},
 	}
 
-	auctionRequest := AuctionRequest{
+	auctionRequest := &AuctionRequest{
 		BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: request},
 		Account:           config.Account{},
 		UserSyncs:         &emptyUsersync{},
@@ -2395,7 +2395,7 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
 		impExtInfoMap[impID] = ImpExtInfo{}
 	}
 
-	auctionRequest := AuctionRequest{
+	auctionRequest := &AuctionRequest{
 		BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: &spec.IncomingRequest.OrtbRequest},
 		Account: config.Account{
 			ID:            "testaccount",
@@ -4310,7 +4310,7 @@ func TestStoredAuctionResponses(t *testing.T) {
 
 	for _, test := range testCases {
 
-		auctionRequest := AuctionRequest{
+		auctionRequest := &AuctionRequest{
 			BidRequestWrapper:      &openrtb_ext.RequestWrapper{BidRequest: mockBidRequest},
 			Account:                config.Account{},
 			UserSyncs:              &emptyUsersync{},
@@ -4646,7 +4646,7 @@ func TestAuctionDebugEnabled(t *testing.T) {
 		Test: 1,
 	}
 
-	auctionRequest := AuctionRequest{
+	auctionRequest := &AuctionRequest{
 		BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: bidRequest},
 		Account:           config.Account{DebugAllow: false},
 		UserSyncs:         &emptyUsersync{},
@@ -4720,7 +4720,7 @@ func TestPassExperimentConfigsToHoldAuction(t *testing.T) {
 		Ext:  json.RawMessage(`{"prebid":{"experiment":{"adscert":{"enabled": true}}}}`),
 	}
 
-	auctionRequest := AuctionRequest{
+	auctionRequest := &AuctionRequest{
 		BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: mockBidRequest},
 		Account:           config.Account{},
 		UserSyncs:         &emptyUsersync{},
@@ -5175,7 +5175,7 @@ func TestOverrideConfigAlternateBidderCodesWithRequestValues(t *testing.T) {
 
 		mockBidRequest.Ext = test.in.requestExt
 
-		auctionRequest := AuctionRequest{
+		auctionRequest := &AuctionRequest{
 			BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: mockBidRequest},
 			Account:           test.in.config.AccountDefaults,
 			UserSyncs:         &emptyUsersync{},
@@ -5595,7 +5595,7 @@ func TestModulesCanBeExecutedForMultipleBiddersSimultaneously(t *testing.T) {
 
 	exec := hookexecution.NewHookExecutor(TestApplyHookMutationsBuilder{}, "/openrtb2/auction", &metricsConfig.NilMetricsEngine{})
 
-	auctionRequest := AuctionRequest{
+	auctionRequest := &AuctionRequest{
 		BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: bidRequest},
 		Account:           config.Account{DebugAllow: true},
 		UserSyncs:         &emptyUsersync{},
@@ -5678,3 +5678,10 @@ func (e mockUpdateBidRequestHook) HandleBidderRequestHook(_ context.Context, mct
 
 	return hookstage.HookResult[hookstage.BidderRequestPayload]{ChangeSet: c, ModuleContext: mctx.ModuleContext}, nil
 }
+
+func TestNilAuctionRequest(t *testing.T) {
+	ex := &exchange{}
+	response, err := ex.HoldAuction(context.Background(), nil, &DebugLog{})
+	assert.Nil(t, response)
+	assert.Nil(t, err)
+}
diff --git a/exchange/targeting_test.go b/exchange/targeting_test.go
index 5ea508b9..49d8b5fa 100644
--- a/exchange/targeting_test.go
+++ b/exchange/targeting_test.go
@@ -125,7 +125,7 @@ func runTargetingAuction(t *testing.T, mockBids map[openrtb_ext.BidderName][]*op
 		req.Site = &openrtb2.Site{}
 	}
 
-	auctionRequest := AuctionRequest{
+	auctionRequest := &AuctionRequest{
 		BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req},
 		Account:           config.Account{},
 		UserSyncs:         &emptyUsersync{},
diff --git a/metrics/go_metrics_test.go b/metrics/go_metrics_test.go
index f5b860db..1b0b20e4 100644
--- a/metrics/go_metrics_test.go
+++ b/metrics/go_metrics_test.go
@@ -80,6 +80,10 @@ func TestNewMetrics(t *testing.T) {
 	ensureContains(t, registry, "ads_cert_requests.ok", m.AdsCertRequestsSuccess)
 	ensureContains(t, registry, "ads_cert_requests.failed", m.AdsCertRequestsFailure)
 
+	ensureContains(t, registry, "request_over_head_time.pre-bidder", m.OverheadTimer[PreBidder])
+	ensureContains(t, registry, "request_over_head_time.make-auction-response", m.OverheadTimer[MakeAuctionResponse])
+	ensureContains(t, registry, "request_over_head_time.make-bidder-requests", m.OverheadTimer[MakeBidderRequests])
+
 	for module, stages := range moduleStageNames {
 		for _, stage := range stages {
 			ensureContainsModuleMetrics(t, registry, fmt.Sprintf("modules.module.%s.stage.%s", module, stage), m.ModuleMetrics[module][stage])
@@ -1193,6 +1197,55 @@ func TestRecordAccountUpgradeStatusMetrics(t *testing.T) {
 	}
 }
 
+func TestRecordOverheadTime(t *testing.T) {
+	testCases := []struct {
+		name          string
+		time          time.Duration
+		overheadType  OverheadType
+		expectedCount int64
+		expectedSum   int64
+	}{
+		{
+			name:          "record-pre-bidder-overhead-time-1",
+			time:          time.Duration(500),
+			overheadType:  PreBidder,
+			expectedCount: 1,
+			expectedSum:   500,
+		},
+		{
+			name:          "record-pre-bidder-overhead-time-2",
+			time:          time.Duration(500),
+			overheadType:  PreBidder,
+			expectedCount: 2,
+			expectedSum:   1000,
+		},
+		{
+			name:          "record-auction-response-overhead-time",
+			time:          time.Duration(500),
+			overheadType:  MakeAuctionResponse,
+			expectedCount: 1,
+			expectedSum:   500,
+		},
+		{
+			name:          "record-make-bidder-requests-overhead-time",
+			time:          time.Duration(500),
+			overheadType:  MakeBidderRequests,
+			expectedCount: 1,
+			expectedSum:   500,
+		},
+	}
+	registry := metrics.NewRegistry()
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			m := NewMetrics(registry, []openrtb_ext.BidderName{}, config.DisabledMetrics{}, nil, nil)
+			m.RecordOverheadTime(test.overheadType, test.time)
+			overheadMetrics := m.OverheadTimer[test.overheadType]
+			assert.Equal(t, test.expectedCount, overheadMetrics.Count())
+			assert.Equal(t, test.expectedSum, overheadMetrics.Sum())
+		})
+	}
+}
+
 func ensureContainsBidTypeMetrics(t *testing.T, registry metrics.Registry, prefix string, mdm map[openrtb_ext.BidType]*MarkupDeliveryMetrics) {
 	ensureContains(t, registry, prefix+".banner.adm_bids_received", mdm[openrtb_ext.BidTypeBanner].AdmMeter)
 	ensureContains(t, registry, prefix+".banner.nurl_bids_received", mdm[openrtb_ext.BidTypeBanner].NurlMeter)
diff --git a/metrics/prometheus/prometheus_test.go b/metrics/prometheus/prometheus_test.go
index 4cb3cd7f..d2ede844 100644
--- a/metrics/prometheus/prometheus_test.go
+++ b/metrics/prometheus/prometheus_test.go
@@ -55,7 +55,6 @@ func TestMetricCountGatekeeping(t *testing.T) {
 	// Calculate Per-Adapter Cardinality
 	adapterCount := len(openrtb_ext.CoreBidderNames())
 	perAdapterCardinalityCount := adapterCardinalityCount / adapterCount
-
 	// Verify General Cardinality
 	// - This assertion provides a warning for newly added high-cardinality non-adapter specific metrics. The hardcoded limit
 	//   is an arbitrary soft ceiling. Thought should be given as to the value of the new metrics if you find yourself
@@ -520,6 +519,52 @@ func TestRequestTimeMetric(t *testing.T) {
 	}
 }
 
+func TestRecordOverheadTimeMetric(t *testing.T) {
+	testCases := []struct {
+		description   string
+		overheadType  metrics.OverheadType
+		timeInMs      float64
+		expectedCount uint64
+		expectedSum   float64
+	}{
+		{
+			description:   "record-pre-bidder-overhead-time-1",
+			overheadType:  metrics.PreBidder,
+			timeInMs:      500,
+			expectedCount: 1,
+			expectedSum:   0.5,
+		},
+		{
+			description:   "record-pre-bidder-overhead-time-2",
+			overheadType:  metrics.PreBidder,
+			timeInMs:      400,
+			expectedCount: 2,
+			expectedSum:   0.9,
+		},
+		{
+			description:   "record-auction-response-overhead-time",
+			overheadType:  metrics.MakeAuctionResponse,
+			timeInMs:      500,
+			expectedCount: 1,
+			expectedSum:   0.5,
+		},
+		{
+			description:   "record-make-bidder-requests-overhead-time",
+			overheadType:  metrics.MakeBidderRequests,
+			timeInMs:      500,
+			expectedCount: 1,
+			expectedSum:   0.5,
+		},
+	}
+
+	metric := createMetricsForTesting()
+	for _, test := range testCases {
+		metric.RecordOverheadTime(test.overheadType, time.Duration(test.timeInMs)*time.Millisecond)
+		resultingHistogram := getHistogramFromHistogramVec(metric.overheadTimer, overheadTypeLabel, test.overheadType.String())
+		assertHistogram(t, test.description, resultingHistogram, test.expectedCount, test.expectedSum)
+	}
+}
+
 func TestRecordStoredDataFetchTime(t *testing.T) {
 	tests := []struct {
 		description string
diff --git a/ortb/clone_test.go b/ortb/clone_test.go
new file mode 100644
index 00000000..24e43bda
--- /dev/null
+++ b/ortb/clone_test.go
@@ -0,0 +1,763 @@
+package ortb
+
+import (
+	"encoding/json"
+	"reflect"
+	"testing"
+
+	"github.com/prebid/openrtb/v19/adcom1"
+	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/util/ptrutil"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCloneApp(t *testing.T) {
+	t.Run("nil", func(t *testing.T) {
+		result := CloneApp(nil)
+		assert.Nil(t, result)
+	})
+
+	t.Run("empty", func(t *testing.T) {
+		given := &openrtb2.App{}
+		result := CloneApp(given)
+		assert.Equal(t, given, result)
+		assert.NotSame(t, given, result)
+	})
+
+	t.Run("populated", func(t *testing.T) {
+		given := &openrtb2.App{
+			ID:                     "anyID",
+			Name:                   "anyName",
+			Bundle:                 "anyBundle",
+			Domain:                 "anyDomain",
+			StoreURL:               "anyStoreURL",
+			CatTax:                 adcom1.CatTaxIABContent10,
+			Cat:                    []string{"cat1"},
+			SectionCat:             []string{"sectionCat1"},
+			PageCat:                []string{"pageCat1"},
+			Ver:                    "anyVer",
+			PrivacyPolicy:          1,
+			Paid:                   2,
+			Publisher:              &openrtb2.Publisher{ID: "anyPublisher", Ext: json.RawMessage(`{"publisher":1}`)},
+			Content:                &openrtb2.Content{ID: "anyContent", Ext: json.RawMessage(`{"content":1}`)},
+			Keywords:               "anyKeywords",
+			KwArray:                []string{"key1"},
+			InventoryPartnerDomain: "anyInventoryPartnerDomain",
+			Ext:                    json.RawMessage(`{"anyField":1}`),
+		}
+		result := CloneApp(given)
+		assert.Equal(t, given, result, "equality")
+		assert.NotSame(t, given, result, "pointer")
+		assert.NotSame(t, given.Cat, result.Cat, "cat")
+		assert.NotSame(t, given.SectionCat, result.SectionCat, "sectioncat")
+		assert.NotSame(t, given.PageCat, result.PageCat, "pagecat")
+		assert.NotSame(t, given.Publisher, result.Publisher, "publisher")
+		assert.NotSame(t, given.Publisher.Ext, result.Publisher.Ext, "publisher-ext")
+		assert.NotSame(t, given.Content, result.Content, "content")
+		assert.NotSame(t, given.Content.Ext, result.Content.Ext, "content-ext")
+		assert.NotSame(t, given.KwArray, result.KwArray, "kwarray")
+		assert.NotSame(t, given.Ext, result.Ext, "ext")
+	})
+
+	t.Run("assumptions", func(t *testing.T) {
+		assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.App{})),
+			[]string{
+				"Cat",
+				"SectionCat",
+				"PageCat",
+				"Publisher",
+				"Content",
+				"KwArray",
+				"Ext",
+			})
+	})
+}
+
+func TestClonePublisher(t *testing.T) {
+	t.Run("nil", func(t *testing.T) {
+		result := ClonePublisher(nil)
+		assert.Nil(t, result)
+	})
+
+	t.Run("empty", func(t *testing.T) {
+		given := &openrtb2.Publisher{}
+		result := ClonePublisher(given)
+		assert.Equal(t, given, result)
+		assert.NotSame(t, given, result)
+	})
+
+	t.Run("populated", func(t *testing.T) {
+		given := &openrtb2.Publisher{
+			ID:     "anyID",
+			Name:   "anyName",
+			CatTax: adcom1.CatTaxIABContent20,
+			Cat:    []string{"cat1"},
+			Domain: "anyDomain",
+			Ext:    json.RawMessage(`{"anyField":1}`),
+		}
+		result := ClonePublisher(given)
+		assert.Equal(t, given, result, "equality")
+		assert.NotSame(t, given, result, "pointer")
+		assert.NotSame(t, given.Cat, result.Cat, "cat")
+		assert.NotSame(t, given.Ext, result.Ext, "ext")
+	})
+
+	t.Run("assumptions", func(t *testing.T) {
+		assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.Publisher{})),
+			[]string{
+				"Cat",
+				"Ext",
+			})
+	})
+}
+
+func TestCloneContent(t *testing.T) {
+	t.Run("nil", func(t *testing.T) {
+		result := CloneContent(nil)
+		assert.Nil(t, result)
+	})
+
+	t.Run("empty", func(t *testing.T) {
+		given := &openrtb2.Content{}
+		result := CloneContent(given)
+		assert.Equal(t, given, result)
+		assert.NotSame(t, given, result)
+	})
+
+	t.Run("populated", func(t *testing.T) {
+		given := &openrtb2.Content{
+			ID:                 "anyID",
+			Episode:            1,
+			Title:              "anyTitle",
+			Series:             "anySeries",
+			Season:             "anySeason",
+			Artist:             "anyArtist",
+			Genre:              "anyGenre",
+			Album:              "anyAlbum",
+			ISRC:               "anyIsrc",
+			Producer:           &openrtb2.Producer{ID: "anyID", Cat: []string{"anyCat"}},
+			URL:                "anyUrl",
+			CatTax:             adcom1.CatTaxIABContent10,
+			Cat:                []string{"cat1"},
+			ProdQ:              ptrutil.ToPtr(adcom1.ProductionProsumer),
+			VideoQuality:       ptrutil.ToPtr(adcom1.ProductionProfessional),
+			Context:            adcom1.ContentApp,
+			ContentRating:      "anyContentRating",
+			UserRating:         "anyUserRating",
+			QAGMediaRating:     adcom1.MediaRatingAll,
+			Keywords:           "anyKeywords",
+			KwArray:            []string{"key1"},
+			LiveStream:         2,
+			SourceRelationship: 3,
+			Len:                4,
+			Language:           "anyLanguage",
+			LangB:              "anyLangB",
+			Embeddable:         5,
+			Data:               []openrtb2.Data{{ID: "1", Ext: json.RawMessage(`{"data":1}`)}},
+			Network:            &openrtb2.Network{ID: "anyNetwork", Ext: json.RawMessage(`{"network":1}`)},
+			Channel:            &openrtb2.Channel{ID: "anyChannel", Ext: json.RawMessage(`{"channel":1}`)},
+			Ext:                json.RawMessage(`{"anyField":1}`),
+		}
+		result := CloneContent(given)
+		assert.Equal(t, given, result, "equality")
+		assert.NotSame(t, given, result, "pointer")
+		assert.NotSame(t, given.Producer, result.Producer, "producer")
+		assert.NotSame(t, given.Producer.Cat, result.Producer.Cat, "producer-cat")
+		assert.NotSame(t, given.Cat, result.Cat, "cat")
+		assert.NotSame(t, given.ProdQ, result.ProdQ, "prodq")
+		assert.NotSame(t, given.VideoQuality, result.VideoQuality, "videoquality")
+		assert.NotSame(t, given.KwArray, result.KwArray, "kwarray")
+		assert.NotSame(t, given.Data, result.Data, "data")
+		assert.NotSame(t, given.Data[0], result.Data[0], "data-item")
+		assert.NotSame(t, given.Data[0].Ext, result.Data[0].Ext, "data-item-ext")
+		assert.NotSame(t, given.Network, result.Network, "network")
+		assert.NotSame(t, given.Network.Ext, result.Network.Ext, "network-ext")
+		assert.NotSame(t, given.Channel, result.Channel, "channel")
+		assert.NotSame(t, given.Channel.Ext, result.Channel.Ext, "channel-ext")
+		assert.NotSame(t, given.Ext, result.Ext, "ext")
+	})
+
+	t.Run("assumptions", func(t *testing.T) {
+		assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.Content{})),
+			[]string{
+				"Producer",
+				"Cat",
+				"ProdQ",
+				"VideoQuality",
+				"KwArray",
+				"Data",
+				"Network",
+				"Channel",
+				"Ext",
+			})
+	})
+}
+
+func TestCloneProducer(t *testing.T) {
+	t.Run("nil", func(t *testing.T) {
+		result := CloneProducer(nil)
+		assert.Nil(t, result)
+	})
+
+	t.Run("empty", func(t *testing.T) {
+		given := &openrtb2.Producer{}
+		result := CloneProducer(given)
+		assert.Equal(t, given, result)
+		assert.NotSame(t, given, result)
+	})
+
+	t.Run("populated", func(t *testing.T) {
+		given := &openrtb2.Producer{
+			ID:     "anyID",
+			Name:   "anyName",
+			CatTax: adcom1.CatTaxIABContent20,
+			Cat:    []string{"cat1"},
+			Domain: "anyDomain",
+			Ext:    json.RawMessage(`{"anyField":1}`),
+		}
+		result := CloneProducer(given)
+		assert.Equal(t, given, result, "equality")
+		assert.NotSame(t, given, result, "pointer")
+		assert.NotSame(t, given.Cat, result.Cat, "cat")
+		assert.NotSame(t, given.Ext, result.Ext, "ext")
+	})
+
+	t.Run("assumptions", func(t *testing.T) {
+		assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.Producer{})),
+			[]string{
+				"Cat",
+				"Ext",
+			})
+	})
+}
+
+func TestCloneDataSlice(t *testing.T) {
+	t.Run("nil", func(t *testing.T) {
+		result := CloneDataSlice(nil)
+		assert.Nil(t, result)
+	})
+
+	t.Run("empty", func(t *testing.T) {
+		given := []openrtb2.Data{}
+		result := CloneDataSlice(given)
+		assert.Empty(t, result)
+		assert.NotSame(t, given, result)
+	})
+
+	t.Run("one", func(t *testing.T) {
+		given := []openrtb2.Data{
+			{ID: "1", Ext: json.RawMessage(`{"anyField":1}`)},
+		}
+		result := CloneDataSlice(given)
+		assert.Equal(t, given, result, "equality")
+		assert.NotSame(t, given[0], result[0], "item-pointer")
+		assert.NotSame(t, given[0].Ext, result[0].Ext, "item-pointer-ext")
+	})
+
+	t.Run("many", func(t *testing.T) {
+		given := []openrtb2.Data{
+			{ID: "1", Ext: json.RawMessage(`{"anyField":1}`)},
+			{ID: "2", Ext: json.RawMessage(`{"anyField":2}`)},
+		}
+		result := CloneDataSlice(given)
+		assert.Equal(t, given, result, "equality")
+		assert.NotSame(t, given[0], result[0], "item0-pointer")
+		assert.NotSame(t, given[0].Ext, result[0].Ext, "item0-pointer-ext")
+		assert.NotSame(t, given[1], result[1], "item1-pointer")
+		assert.NotSame(t, given[1].Ext, result[1].Ext, "item1-pointer-ext")
+	})
+}
+
+func TestCloneData(t *testing.T) {
+	t.Run("populated", func(t *testing.T) {
+		given := openrtb2.Data{
+			ID:      "anyID",
+			Name:    "anyName",
+			Segment: []openrtb2.Segment{{ID: "1", Ext: json.RawMessage(`{"anyField":1}`)}},
+			Ext:     json.RawMessage(`{"anyField":1}`),
+		}
+		result := CloneData(given)
+		assert.Equal(t, given, result, "equality")
+		assert.NotSame(t, given, result, "pointer")
+		assert.NotSame(t, given.Segment, result.Segment, "segment")
+		assert.NotSame(t, given.Segment[0], result.Segment[0], "segment-item")
+		assert.NotSame(t, given.Segment[0].Ext, result.Segment[0].Ext, "segment-item-ext")
+		assert.NotSame(t, given.Ext, result.Ext, "ext")
+	})
+
+	t.Run("assumptions", func(t *testing.T) {
+		assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.Data{})),
+			[]string{
+				"Segment",
+				"Ext",
+			})
+	})
+}
+
+func TestCloneSegmentSlice(t *testing.T) {
+	t.Run("nil", func(t *testing.T) {
+		result := CloneSegmentSlice(nil)
+		assert.Nil(t, result)
+	})
+
+	t.Run("empty", func(t *testing.T) {
+		given := []openrtb2.Segment{}
+		result := CloneSegmentSlice(given)
+		assert.Empty(t, result)
+		assert.NotSame(t, given, result)
+	})
+
+	t.Run("one", func(t *testing.T) {
+		given := []openrtb2.Segment{
+			{ID: "1", Ext: json.RawMessage(`{"anyField":1}`)},
+		}
+		result := CloneSegmentSlice(given)
+		assert.Equal(t, given, result, "equality")
+		assert.NotSame(t, given[0], result[0], "item-pointer")
+		assert.NotSame(t, given[0].Ext, result[0].Ext, "item-pointer-ext")
+	})
+
+	t.Run("many", func(t *testing.T) {
+		given := []openrtb2.Segment{
+			{Ext: json.RawMessage(`{"anyField":1}`)},
+			{Ext: json.RawMessage(`{"anyField":2}`)},
+		}
+		result := CloneSegmentSlice(given)
+		assert.Equal(t, given, result, "equality")
+		assert.NotSame(t, given[0], result[0], "item0-pointer")
+		assert.NotSame(t, given[0].Ext, result[0].Ext, "item0-pointer-ext")
+		assert.NotSame(t, given[1], result[1], "item1-pointer")
+		assert.NotSame(t, given[1].Ext, result[1].Ext, "item1-pointer-ext")
+	})
+}
+
+func TestCloneSegment(t *testing.T) {
+	t.Run("populated", func(t *testing.T) {
+		given := openrtb2.Segment{
+			ID:    "anyID",
+			Name:  "anyName",
+			Value: "anyValue",
+			Ext:   json.RawMessage(`{"anyField":1}`),
+		}
+		result := CloneSegment(given)
+		assert.Equal(t, given, result, "equality")
+		assert.NotSame(t, given, result, "pointer")
+		assert.NotSame(t, given.Ext, result.Ext, "ext")
+	})
+
+	t.Run("assumptions", func(t *testing.T) {
+		assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.Segment{})),
+			[]string{
+				"Ext",
+			})
+	})
+}
+
+func TestCloneNetwork(t *testing.T) {
+	t.Run("nil", func(t *testing.T) {
+		result := CloneNetwork(nil)
+		assert.Nil(t, result)
+	})
+
+	t.Run("empty", func(t *testing.T) {
+		given := &openrtb2.Network{}
+		result := CloneNetwork(given)
+		assert.Empty(t, result)
+		assert.NotSame(t, given, result)
+	})
+
+	t.Run("populated", func(t *testing.T) {
+		given := &openrtb2.Network{
+			ID:     "anyID",
+			Name:   "anyName",
+			Domain: "anyDomain",
+			Ext:    json.RawMessage(`{"anyField":1}`),
+		}
+		result := CloneNetwork(given)
+		assert.Equal(t, given, result, "equality")
+		assert.NotSame(t, given, result, "pointer")
+		assert.NotSame(t, given.Ext, result.Ext, "ext")
+	})
+
+	t.Run("assumptions", func(t *testing.T) {
+		assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.Network{})),
+			[]string{
+				"Ext",
+			})
+	})
+}
+
+func TestCloneChannel(t *testing.T) {
+	t.Run("nil", func(t *testing.T) {
+		result := CloneChannel(nil)
+		assert.Nil(t, result)
+	})
+
+	t.Run("empty", func(t *testing.T) {
+		given := &openrtb2.Channel{}
+		result := CloneChannel(given)
+		assert.Empty(t, result)
+		assert.NotSame(t, given, result)
+	})
+
+	t.Run("populated", func(t *testing.T) {
+		given := &openrtb2.Channel{
+			ID:     "anyID",
+			Name:   "anyName",
+			Domain: "anyDomain",
+			Ext:    json.RawMessage(`{"anyField":1}`),
+		}
+		result := CloneChannel(given)
+		assert.Equal(t, given, result, "equality")
+		assert.NotSame(t, given, result, "pointer")
+		assert.NotSame(t, given.Ext, result.Ext, "ext")
+	})
+
+	t.Run("assumptions", func(t *testing.T) {
+		assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.Channel{})),
+			[]string{
+				"Ext",
+			})
+	})
+}
+
+func TestCloneSite(t *testing.T) {
+	t.Run("nil", func(t *testing.T) {
+		result := CloneSite(nil)
+		assert.Nil(t, result)
+	})
+
+	t.Run("empty", func(t *testing.T) {
+		given := &openrtb2.Site{}
+		result := CloneSite(given)
+		assert.Empty(t, result)
+		assert.NotSame(t, given, result)
+	})
+
+	t.Run("populated", func(t *testing.T) {
+		given := &openrtb2.Site{
+			ID:                     "anyID",
+			Name:                   "anyName",
+			Domain:                 "anyDomain",
+			CatTax:                 adcom1.CatTaxIABContent10,
+			Cat:                    []string{"cat1"},
+			SectionCat:             []string{"sectionCat1"},
+			PageCat:                []string{"pageCat1"},
+			Page:                   "anyPage",
+			Ref:                    "anyRef",
+			Search:                 "anySearch",
+			Mobile:                 1,
+			PrivacyPolicy:          2,
+			Publisher:              &openrtb2.Publisher{ID: "anyPublisher", Ext: json.RawMessage(`{"publisher":1}`)},
+			Content:                &openrtb2.Content{ID: "anyContent", Ext: json.RawMessage(`{"content":1}`)},
+			Keywords:               "anyKeywords",
+			KwArray:                []string{"key1"},
+			InventoryPartnerDomain: "anyInventoryPartnerDomain",
+			Ext:                    json.RawMessage(`{"anyField":1}`),
+		}
+		result := CloneSite(given)
+		assert.Equal(t, given, result, "equality")
+		assert.NotSame(t, given, result, "pointer")
+		assert.NotSame(t, given.Cat, result.Cat, "cat")
+		assert.NotSame(t, given.SectionCat, result.SectionCat, "sectioncat")
+		assert.NotSame(t, given.PageCat, result.PageCat, "pagecat")
+		assert.NotSame(t, given.Publisher, result.Publisher, "publisher")
+		assert.NotSame(t, given.Publisher.Ext, result.Publisher.Ext, "publisher-ext")
+		assert.NotSame(t, given.Content, result.Content, "content")
+		assert.NotSame(t, given.Content.Ext, result.Content.Ext, "content-ext")
+		assert.NotSame(t, given.KwArray, result.KwArray, "kwarray")
+		assert.NotSame(t, given.Ext, result.Ext, "ext")
+	})
+
+	t.Run("assumptions", func(t *testing.T) {
+		assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.Site{})),
+			[]string{
+				"Cat",
+				"SectionCat",
+				"PageCat",
+				"Publisher",
+				"Content",
+				"KwArray",
+				"Ext",
+			})
+	})
+}
+
+func TestCloneUser(t *testing.T) {
+	t.Run("nil", func(t *testing.T) {
+		result := CloneUser(nil)
+		assert.Nil(t, result)
+	})
+
+	t.Run("empty", func(t *testing.T) {
+		given := &openrtb2.User{}
+		result := CloneUser(given)
+		assert.Empty(t, result)
+		assert.NotSame(t, given, result)
+	})
+
+	t.Run("populated", func(t *testing.T) {
+		given := &openrtb2.User{
+			ID:         "anyID",
+			BuyerUID:   "anyBuyerUID",
+			Yob:        1,
+			Gender:     "anyGender",
+			Keywords:   "anyKeywords",
+			KwArray:    []string{"key1"},
+			CustomData: "anyCustomData",
+			Geo:        &openrtb2.Geo{Lat: 1.2, Lon: 2.3, Ext: json.RawMessage(`{"geo":1}`)},
+			Data:       []openrtb2.Data{{ID: "1", Ext: json.RawMessage(`{"data":1}`)}},
+			Consent:    "anyConsent",
+			EIDs:       []openrtb2.EID{{Source: "1", Ext: json.RawMessage(`{"eid":1}`)}},
+			Ext:        json.RawMessage(`{"anyField":1}`),
+		}
+		result := CloneUser(given)
+		assert.Equal(t, given, result, "equality")
+		assert.NotSame(t, given, result, "pointer")
+		assert.NotSame(t, given.KwArray, result.KwArray, "cat")
+		assert.NotSame(t, given.Geo, result.Geo, "geo")
+		assert.NotSame(t, given.Geo.Ext, result.Geo.Ext, "geo-ext")
+		assert.NotSame(t, given.Data[0], result.Data[0], "data-item")
+		assert.NotSame(t, given.Data[0].Ext, result.Data[0].Ext, "data-item-ext")
+		assert.NotSame(t, given.EIDs[0], result.EIDs[0], "eids-item")
+		assert.NotSame(t, given.EIDs[0].Ext, result.EIDs[0].Ext, "eids-item-ext")
+		assert.NotSame(t, given.Ext, result.Ext, "ext")
+	})
+
+	t.Run("assumptions", func(t *testing.T) {
+		assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.User{})),
+			[]string{
+				"KwArray",
+				"Geo",
+				"Data",
+				"EIDs",
+				"Ext",
+			})
+	})
+}
+
+func TestCloneGeo(t *testing.T) {
+	t.Run("nil", func(t *testing.T) {
+		result := CloneGeo(nil)
+		assert.Nil(t, result)
+	})
+
+	t.Run("empty", func(t *testing.T) {
+		given := &openrtb2.Geo{}
+		result := CloneGeo(given)
+		assert.Empty(t, result)
+		assert.NotSame(t, given, result)
+	})
+
+	t.Run("populated", func(t *testing.T) {
+		given := &openrtb2.Geo{
+			Lat:           1.234,
+			Lon:           5.678,
+			Type:          adcom1.LocationGPS,
+			Accuracy:      1,
+			LastFix:       2,
+			IPService:     adcom1.LocationServiceIP2Location,
+			Country:       "anyCountry",
+			Region:        "anyRegion",
+			RegionFIPS104: "anyRegionFIPS104",
+			Metro:         "anyMetro",
+			City:          "anyCity",
+			ZIP:           "anyZIP",
+			UTCOffset:     3,
+			Ext:           json.RawMessage(`{"anyField":1}`),
+		}
+		result := CloneGeo(given)
+		assert.Equal(t, given, result, "equality")
+		assert.NotSame(t, given, result, "pointer")
+		assert.NotSame(t, given.Ext, result.Ext, "ext")
+	})
+
+	t.Run("assumptions", func(t *testing.T) {
+		assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.Geo{})),
+			[]string{
+				"Ext",
+			})
+	})
+}
+
+func TestCloneEIDSlice(t *testing.T) {
+	t.Run("nil", func(t *testing.T) {
+		result := CloneEIDSlice(nil)
+		assert.Nil(t, result)
+	})
+
+	t.Run("empty", func(t *testing.T) {
+		given := []openrtb2.EID{}
+		result := CloneEIDSlice(given)
+		assert.Empty(t, result)
+		assert.NotSame(t, given, result)
+	})
+
+	t.Run("one", func(t *testing.T) {
+		given := []openrtb2.EID{
+			{Source: "1", Ext: json.RawMessage(`{"anyField":1}`)},
+		}
+		result := CloneEIDSlice(given)
+		assert.Equal(t, given, result, "equality")
+		assert.NotSame(t, given[0], result[0], "item-pointer")
+		assert.NotSame(t, given[0].Ext, result[0].Ext, "item-pointer-ext")
+	})
+
+	t.Run("many", func(t *testing.T) {
+		given := []openrtb2.EID{
+			{Source: "1", Ext: json.RawMessage(`{"anyField":1}`)},
+			{Source: "2", Ext: json.RawMessage(`{"anyField":2}`)},
+		}
+		result := CloneEIDSlice(given)
+		assert.Equal(t, given, result, "equality")
+		assert.NotSame(t, given[0], result[0], "item0-pointer")
+		assert.NotSame(t, given[0].Ext, result[0].Ext, "item0-pointer-ext")
+		assert.NotSame(t, given[1], result[1], "item1-pointer")
+		assert.NotSame(t, given[1].Ext, result[1].Ext, "item1-pointer-ext")
+	})
+}
+
+func TestCloneEID(t *testing.T) {
+	t.Run("populated", func(t *testing.T) {
+		given := openrtb2.EID{
+			Source: "anySource",
+			UIDs:   []openrtb2.UID{{ID: "1", Ext: json.RawMessage(`{"uid":1}`)}},
+			Ext:    json.RawMessage(`{"anyField":1}`),
+		}
+		result := CloneEID(given)
+		assert.Equal(t, given, result, "equality")
+		assert.NotSame(t, given, result, "pointer")
+		assert.NotSame(t, given.UIDs, result.UIDs, "uids")
+		assert.NotSame(t, given.UIDs[0], result.UIDs[0], "uids-item")
+		assert.NotSame(t, given.UIDs[0].Ext, result.UIDs[0].Ext, "uids-item-ext")
+		assert.NotSame(t, given.Ext, result.Ext, "ext")
+	})
+
+	t.Run("assumptions", func(t *testing.T) {
+		assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.EID{})),
+			[]string{
+				"UIDs",
+				"Ext",
+			})
+	})
+}
+
+func TestCloneUIDSlice(t *testing.T) {
+	t.Run("nil", func(t *testing.T) {
+		result := CloneUIDSlice(nil)
+		assert.Nil(t, result)
+	})
+
+	t.Run("empty", func(t *testing.T) {
+		given := []openrtb2.UID{}
+		result := CloneUIDSlice(given)
+		assert.Empty(t, result)
+		assert.NotSame(t, given, result)
+	})
+
+	t.Run("one", func(t *testing.T) {
+		given := []openrtb2.UID{
+			{ID: "1", Ext: json.RawMessage(`{"anyField":1}`)},
+		}
+		result := CloneUIDSlice(given)
+		assert.Equal(t, given, result, "equality")
+		assert.NotSame(t, given[0], result[0], "item-pointer")
+		assert.NotSame(t, given[0].Ext, result[0].Ext, "item-pointer-ext")
+	})
+
+	t.Run("many", func(t *testing.T) {
+		given := []openrtb2.UID{
+			{ID: "1", Ext: json.RawMessage(`{"anyField":1}`)},
+			{ID: "2", Ext: json.RawMessage(`{"anyField":2}`)},
+		}
+		result := CloneUIDSlice(given)
+		assert.Equal(t, given, result, "equality")
+		assert.NotSame(t, given[0], result[0], "item0-pointer")
+		assert.NotSame(t, given[0].Ext, result[0].Ext, "item0-pointer-ext")
+		assert.NotSame(t, given[1], result[1], "item1-pointer")
+		assert.NotSame(t, given[1].Ext, result[1].Ext, "item1-pointer-ext")
+	})
+}
+
+func TestCloneUID(t *testing.T) {
+	t.Run("populated", func(t *testing.T) {
+		given := openrtb2.UID{
+			ID:    "anyID",
+			AType: adcom1.AgentTypePerson,
+			Ext:   json.RawMessage(`{"anyField":1}`),
+		}
+		result := CloneUID(given)
+		assert.Equal(t, given, result, "equality")
+		assert.NotSame(t, given, result, "pointer")
+		assert.NotSame(t, given.Ext, result.Ext, "ext")
+	})
+
+	t.Run("assumptions", func(t *testing.T) {
+		assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.UID{})),
+			[]string{
+				"Ext",
+			})
+	})
+}
+
+func TestCloneDOOH(t *testing.T) {
+	t.Run("nil", func(t *testing.T) {
+		result := CloneDOOH(nil)
+		assert.Nil(t, result)
+	})
+
+	t.Run("empty", func(t *testing.T) {
+		given := &openrtb2.DOOH{}
+		result := CloneDOOH(given)
+		assert.Empty(t, result)
+		assert.NotSame(t, given, result)
+	})
+
+	t.Run("populated", func(t *testing.T) {
+		given := &openrtb2.DOOH{
+			ID:           "anyID",
+			Name:         "anyName",
+			VenueType:    []string{"venue1"},
+			VenueTypeTax: ptrutil.ToPtr(adcom1.VenueTaxonomyAdCom),
+			Publisher:    &openrtb2.Publisher{ID: "anyPublisher", Ext: json.RawMessage(`{"publisher":1}`)},
+			Domain:       "anyDomain",
+			Keywords:     "anyKeywords",
+			Content:      &openrtb2.Content{ID: "anyContent", Ext: json.RawMessage(`{"content":1}`)},
+			Ext:          json.RawMessage(`{"anyField":1}`),
+		}
+		result := CloneDOOH(given)
+		assert.Equal(t, given, result, "equality")
+		assert.NotSame(t, given, result, "pointer")
+		assert.NotSame(t, given.VenueType, result.VenueType, "venuetype")
+		assert.NotSame(t, given.VenueTypeTax, result.VenueTypeTax, "venuetypetax")
+		assert.NotSame(t, given.Publisher, result.Publisher, "publisher")
+		assert.NotSame(t, given.Publisher.Ext, result.Publisher.Ext, "publisher-ext")
+		assert.NotSame(t, given.Content, result.Content, "content")
+		assert.NotSame(t, given.Content.Ext, result.Content.Ext, "content-ext")
+		assert.NotSame(t, given.Ext, result.Ext, "ext")
+	})
+
+	t.Run("assumptions", func(t *testing.T) {
+		assert.ElementsMatch(t, discoverPointerFields(reflect.TypeOf(openrtb2.DOOH{})),
+			[]string{
+				"VenueType",
+				"VenueTypeTax",
+				"Publisher",
+				"Content",
+				"Ext",
+			})
+	})
+}
+
+// discoverPointerFields returns the names of all fields of an object that are
+// pointers and would need to be cloned. This method is specific to types which can
+// appear within an OpenRTB data model object.
+func discoverPointerFields(t reflect.Type) []string {
+	var fields []string
+	for _, f := range reflect.VisibleFields(t) {
+		if f.Type.Kind() == reflect.Slice || f.Type.Kind() == reflect.Pointer {
+			fields = append(fields, f.Name)
+		}
+	}
+	return fields
+}
diff --git a/util/sliceutil/clone_test.go b/util/sliceutil/clone_test.go
new file mode 100644
index 00000000..6e905b5c
--- /dev/null
+++ b/util/sliceutil/clone_test.go
@@ -0,0 +1,39 @@
+package sliceutil
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCloneSlice(t *testing.T) {
+	testCases := []struct {
+		name  string
+		given []int
+	}{
+		{
+			name:  "nil",
+			given: nil,
+		},
+		{
+			name:  "empty",
+			given: []int{},
+		},
+		{
+			name:  "one",
+			given: []int{1},
+		},
+		{
+			name:  "many",
+			given: []int{1, 2},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			result := Clone(test.given)
+			assert.Equal(t, test.given, result, "equality")
+			assert.NotSame(t, test.given, result, "pointer")
+		})
+	}
+}