Repository: prebid/prebid-server
Pull request #2703 (2023-04-14 13:09:25)

Coded wrapper clone
This does not yet actually do anything. A subsequent PR will make use of the clone functions, but this work was enough for its own PR.


Patch:
diff --git a/adapters/frvradn/frvradn.go b/adapters/frvradn/frvradn.go
new file mode 100644
index 00000000..3954343e
--- /dev/null
+++ b/adapters/frvradn/frvradn.go
@@ -0,0 +1,151 @@
+package frvradn
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"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/openrtb_ext"
+	"github.com/prebid/prebid-server/util/httputil"
+	"strings"
+)
+
+type adapter struct {
+	uri string
+}
+
+func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
+	if config.Endpoint == "" {
+		return nil, errors.New("missing endpoint adapter parameter")
+	}
+
+	bidder := &adapter{
+		uri: config.Endpoint,
+	}
+	return bidder, nil
+}
+
+func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	var requests []*adapters.RequestData
+	var errs []error
+
+	requestCopy := *request
+	for _, imp := range request.Imp {
+		frvrAdnExt, err := getImpressionExt(&imp)
+		if err != nil {
+			errs = append(errs, err)
+			continue
+		}
+
+		if imp.BidFloor > 0 && imp.BidFloorCur != "" && strings.ToUpper(imp.BidFloorCur) != "USD" {
+			convertedValue, err := requestInfo.ConvertCurrency(imp.BidFloor, imp.BidFloorCur, "USD")
+			if err != nil {
+				errs = append(errs, err)
+				continue
+			}
+			imp.BidFloorCur = "USD"
+			imp.BidFloor = convertedValue
+		}
+
+		ext, err := json.Marshal(frvrAdnExt)
+		if err != nil {
+			errs = append(errs, err)
+			continue
+		}
+		imp.Ext = ext
+
+		requestCopy.Imp = []openrtb2.Imp{imp}
+
+		requestJSON, err := json.Marshal(requestCopy)
+		if err != nil {
+			errs = append(errs, err)
+			continue
+		}
+
+		requestData := &adapters.RequestData{
+			Method: "POST",
+			Uri:    a.uri,
+			Body:   requestJSON,
+		}
+		requests = append(requests, requestData)
+	}
+	return requests, errs
+}
+
+func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	if httputil.IsResponseStatusCodeNoContent(responseData) {
+		return nil, nil
+	}
+
+	if err := httputil.CheckResponseStatusCodeForErrors(responseData); err != nil {
+		return nil, []error{err}
+	}
+
+	if len(responseData.Body) == 0 {
+		return nil, nil
+	}
+
+	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
+	var errs []error
+	for _, seatBid := range response.SeatBid {
+		for i := range seatBid.Bid {
+			bidType, err := getBidMediaType(&seatBid.Bid[i])
+			if err != nil {
+				errs = append(errs, err)
+				continue
+			}
+
+			b := &adapters.TypedBid{
+				Bid:     &seatBid.Bid[i],
+				BidType: bidType,
+			}
+			bidResponse.Bids = append(bidResponse.Bids, b)
+		}
+	}
+	return bidResponse, errs
+}
+
+func getImpressionExt(imp *openrtb2.Imp) (*openrtb_ext.ImpExtFRVRAdn, error) {
+	var extImpBidder adapters.ExtImpBidder
+	if err := json.Unmarshal(imp.Ext, &extImpBidder); err != nil {
+		return nil, &errortypes.BadInput{
+			Message: "missing ext",
+		}
+	}
+	var frvrAdnExt openrtb_ext.ImpExtFRVRAdn
+	if err := json.Unmarshal(extImpBidder.Bidder, &frvrAdnExt); err != nil {
+		return nil, &errortypes.BadInput{
+			Message: "missing ext.bidder",
+		}
+	}
+
+	if len(frvrAdnExt.PublisherID) == 0 || len(frvrAdnExt.AdUnitID) == 0 {
+		return nil, &errortypes.BadInput{
+			Message: "publisher_id and ad_unit_id are required",
+		}
+	}
+	return &frvrAdnExt, nil
+}
+
+func getBidMediaType(bid *openrtb2.Bid) (openrtb_ext.BidType, error) {
+	var extBid openrtb_ext.ExtBid
+	err := json.Unmarshal(bid.Ext, &extBid)
+	if err != nil {
+		return "", fmt.Errorf("unable to deserialize imp %v bid.ext", bid.ImpID)
+	}
+
+	if extBid.Prebid == nil {
+		return "", fmt.Errorf("imp %v with unknown media type", bid.ImpID)
+	}
+
+	return extBid.Prebid.Type, nil
+}
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/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index efc74613..2811a9e7 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -1670,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))
 			}
@@ -1710,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 {
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/exchange/adapter_builders.go b/exchange/adapter_builders.go
index 784ae8ae..7a2ffd54 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -77,6 +77,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/eplanning"
 	"github.com/prebid/prebid-server/adapters/epom"
 	"github.com/prebid/prebid-server/adapters/freewheelssp"
+	"github.com/prebid/prebid-server/adapters/frvradn"
 	"github.com/prebid/prebid-server/adapters/gamma"
 	"github.com/prebid/prebid-server/adapters/gamoshi"
 	"github.com/prebid/prebid-server/adapters/globalsun"
@@ -254,6 +255,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderEvtech:            limelightDigital.Builder,
 		openrtb_ext.BidderFreewheelSSP:      freewheelssp.Builder,
 		openrtb_ext.BidderFreewheelSSPOld:   freewheelssp.Builder,
+		openrtb_ext.BidderFRVRAdNetwork:     frvradn.Builder,
 		openrtb_ext.BidderGamma:             gamma.Builder,
 		openrtb_ext.BidderGamoshi:           gamoshi.Builder,
 		openrtb_ext.BidderGlobalsun:         globalsun.Builder,
diff --git a/exchange/bidder.go b/exchange/bidder.go
index 58dc1789..8e6f133f 100644
--- a/exchange/bidder.go
+++ b/exchange/bidder.go
@@ -520,6 +520,7 @@ func (bidder *bidderAdapter) doRequestImpl(ctx context.Context, req *adapters.Re
 		ctx = bidder.addClientTrace(ctx)
 	}
 	bidder.me.RecordOverheadTime(metrics.PreBidder, time.Since(pbsRequestStartTime))
+	httpCallStart := time.Now()
 	httpResp, err := ctxhttp.Do(ctx, bidder.Client, httpReq)
 	if err != nil {
 		if err == context.DeadlineExceeded {
@@ -560,6 +561,7 @@ func (bidder *bidderAdapter) doRequestImpl(ctx context.Context, req *adapters.Re
 		}
 	}
 
+	bidder.me.RecordBidderServerResponseTime(time.Since(httpCallStart))
 	return &httpCallInfo{
 		request: req,
 		response: &adapters.ResponseData{
diff --git a/metrics/config/metrics.go b/metrics/config/metrics.go
index 95e2614d..0cf0533d 100644
--- a/metrics/config/metrics.go
+++ b/metrics/config/metrics.go
@@ -149,6 +149,12 @@ func (me *MultiMetricsEngine) RecordTLSHandshakeTime(tlsHandshakeTime time.Durat
 	}
 }
 
+func (me *MultiMetricsEngine) RecordBidderServerResponseTime(bidderServerResponseTime time.Duration) {
+	for _, thisME := range *me {
+		thisME.RecordBidderServerResponseTime(bidderServerResponseTime)
+	}
+}
+
 // RecordAdapterBidReceived across all engines
 func (me *MultiMetricsEngine) RecordAdapterBidReceived(labels metrics.AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) {
 	for _, thisME := range *me {
@@ -425,6 +431,10 @@ func (me *NilMetricsEngine) RecordDNSTime(dnsLookupTime time.Duration) {
 func (me *NilMetricsEngine) RecordTLSHandshakeTime(tlsHandshakeTime time.Duration) {
 }
 
+// RecordBidderServerResponseTime as a noop
+func (me *NilMetricsEngine) RecordBidderServerResponseTime(bidderServerResponseTime time.Duration) {
+}
+
 // RecordAdapterBidReceived as a noop
 func (me *NilMetricsEngine) RecordAdapterBidReceived(labels metrics.AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) {
 }
diff --git a/metrics/go_metrics.go b/metrics/go_metrics.go
index 44f2ecd7..64726e30 100644
--- a/metrics/go_metrics.go
+++ b/metrics/go_metrics.go
@@ -31,6 +31,7 @@ type Metrics struct {
 	AccountCacheMeter              map[CacheResult]metrics.Meter
 	DNSLookupTimer                 metrics.Timer
 	TLSHandshakeTimer              metrics.Timer
+	BidderServerResponseTimer      metrics.Timer
 	StoredResponsesMeter           metrics.Meter
 
 	// Metrics for OpenRTB requests specifically
@@ -219,7 +220,8 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa
 		exchanges: exchanges,
 		modules:   getModuleNames(moduleStageNames),
 
-		OverheadTimer: makeBlankOverheadTimerMetrics(),
+		OverheadTimer:             makeBlankOverheadTimerMetrics(),
+		BidderServerResponseTimer: blankTimer,
 	}
 
 	for _, a := range exchanges {
@@ -305,6 +307,7 @@ func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, d
 	newMetrics.PrebidCacheRequestTimerError = metrics.GetOrRegisterTimer("prebid_cache_request_time.err", registry)
 	newMetrics.StoredResponsesMeter = metrics.GetOrRegisterMeter("stored_responses", registry)
 	newMetrics.OverheadTimer = makeOverheadTimerMetrics(registry)
+	newMetrics.BidderServerResponseTimer = metrics.GetOrRegisterTimer("bidder_server_response_time_seconds", registry)
 
 	for _, dt := range StoredDataTypes() {
 		for _, ft := range StoredDataFetchTypes() {
@@ -811,6 +814,10 @@ func (me *Metrics) RecordTLSHandshakeTime(tlsHandshakeTime time.Duration) {
 	me.TLSHandshakeTimer.Update(tlsHandshakeTime)
 }
 
+func (me *Metrics) RecordBidderServerResponseTime(bidderServerResponseTime time.Duration) {
+	me.BidderServerResponseTimer.Update(bidderServerResponseTime)
+}
+
 // RecordAdapterBidReceived implements a part of the MetricsEngine interface.
 // This tracks how many bids from each Bidder use `adm` vs. `nurl.
 func (me *Metrics) RecordAdapterBidReceived(labels AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) {
diff --git a/metrics/metrics.go b/metrics/metrics.go
index 186e1ffa..214e7c1d 100644
--- a/metrics/metrics.go
+++ b/metrics/metrics.go
@@ -430,6 +430,7 @@ type MetricsEngine interface {
 	RecordAdapterConnections(adapterName openrtb_ext.BidderName, connWasReused bool, connWaitTime time.Duration)
 	RecordDNSTime(dnsLookupTime time.Duration)
 	RecordTLSHandshakeTime(tlsHandshakeTime time.Duration)
+	RecordBidderServerResponseTime(bidderServerResponseTime time.Duration)
 	RecordAdapterPanic(labels AdapterLabels)
 	RecordAdapterBidReceived(labels AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool)
 	RecordAdapterPrice(labels AdapterLabels, cpm float64)
diff --git a/metrics/metrics_mock.go b/metrics/metrics_mock.go
index cbca8570..0ce238c5 100644
--- a/metrics/metrics_mock.go
+++ b/metrics/metrics_mock.go
@@ -71,6 +71,11 @@ func (me *MetricsEngineMock) RecordTLSHandshakeTime(tlsHandshakeTime time.Durati
 	me.Called(tlsHandshakeTime)
 }
 
+// RecordBidderServerResponseTime mock
+func (me *MetricsEngineMock) RecordBidderServerResponseTime(bidderServerResponseTime time.Duration) {
+	me.Called(bidderServerResponseTime)
+}
+
 // RecordAdapterBidReceived mock
 func (me *MetricsEngineMock) RecordAdapterBidReceived(labels AdapterLabels, bidType openrtb_ext.BidType, hasAdm bool) {
 	me.Called(labels, bidType, hasAdm)
diff --git a/metrics/prometheus/prometheus.go b/metrics/prometheus/prometheus.go
index ee11dccf..eb387789 100644
--- a/metrics/prometheus/prometheus.go
+++ b/metrics/prometheus/prometheus.go
@@ -55,6 +55,7 @@ type Metrics struct {
 	storedResponsesErrors        *prometheus.CounterVec
 	adsCertRequests              *prometheus.CounterVec
 	adsCertSignTimer             prometheus.Histogram
+	bidderServerResponseTimer    prometheus.Histogram
 
 	// Adapter Metrics
 	adapterBids                           *prometheus.CounterVec
@@ -440,6 +441,11 @@ func NewMetrics(cfg config.PrometheusMetrics, disabledMetrics config.DisabledMet
 		[]string{adapterLabel},
 		standardTimeBuckets)
 
+	metrics.bidderServerResponseTimer = newHistogram(cfg, reg,
+		"bidder_server_response_time_seconds",
+		"Duration needed to send HTTP request and receive response back from bidder server.",
+		standardTimeBuckets)
+
 	metrics.syncerRequests = newCounter(cfg, reg,
 		"syncer_requests",
 		"Count of cookie sync requests where a syncer is a candidate to be synced labeled by syncer key and status.",
@@ -893,6 +899,10 @@ func (m *Metrics) RecordTLSHandshakeTime(tlsHandshakeTime time.Duration) {
 	m.tlsHandhakeTimer.Observe(tlsHandshakeTime.Seconds())
 }
 
+func (m *Metrics) RecordBidderServerResponseTime(bidderServerResponseTime time.Duration) {
+	m.bidderServerResponseTimer.Observe(bidderServerResponseTime.Seconds())
+}
+
 func (m *Metrics) RecordAdapterPanic(labels metrics.AdapterLabels) {
 	m.adapterPanics.With(prometheus.Labels{
 		adapterLabel: string(labels.Adapter),
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 90f80d4f..a5a1ecff 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -164,6 +164,7 @@ const (
 	BidderEvtech            BidderName = "evtech"
 	BidderFreewheelSSP      BidderName = "freewheelssp"
 	BidderFreewheelSSPOld   BidderName = "freewheel-ssp"
+	BidderFRVRAdNetwork     BidderName = "frvradn"
 	BidderGamma             BidderName = "gamma"
 	BidderGamoshi           BidderName = "gamoshi"
 	BidderGlobalsun         BidderName = "globalsun"
@@ -350,6 +351,7 @@ func CoreBidderNames() []BidderName {
 		BidderEvtech,
 		BidderFreewheelSSP,
 		BidderFreewheelSSPOld,
+		BidderFRVRAdNetwork,
 		BidderGamma,
 		BidderGamoshi,
 		BidderGlobalsun,
diff --git a/openrtb_ext/imp_frvradn.go b/openrtb_ext/imp_frvradn.go
new file mode 100644
index 00000000..ac8f86b2
--- /dev/null
+++ b/openrtb_ext/imp_frvradn.go
@@ -0,0 +1,6 @@
+package openrtb_ext
+
+type ImpExtFRVRAdn struct {
+	PublisherID string `json:"publisher_id"`
+	AdUnitID    string `json:"ad_unit_id"`
+}
diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go
index 80a678fb..5e222ab7 100644
--- a/openrtb_ext/request.go
+++ b/openrtb_ext/request.go
@@ -5,6 +5,9 @@ import (
 	"fmt"
 
 	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/util/maputil"
+	"github.com/prebid/prebid-server/util/ptrutil"
+	"github.com/prebid/prebid-server/util/sliceutil"
 )
 
 // FirstPartyDataExtKey defines a field name within request.ext and request.imp.ext reserved for first party data.
@@ -314,3 +317,199 @@ func (m ExtMultiBid) String() string {
 	}
 	return fmt.Sprintf("{Bidder:%s, Bidders:%v, MaxBids:%s, TargetBidderCodePrefix:%s}", m.Bidder, m.Bidders, maxBid, m.TargetBidderCodePrefix)
 }
+
+func (erp *ExtRequestPrebid) Clone() *ExtRequestPrebid {
+	if erp == nil {
+		return nil
+	}
+
+	clone := *erp
+	clone.Aliases = maputil.Clone(erp.Aliases)
+	clone.AliasGVLIDs = maputil.Clone(erp.AliasGVLIDs)
+	clone.BidAdjustmentFactors = maputil.Clone(erp.BidAdjustmentFactors)
+
+	if erp.BidderConfigs != nil {
+		clone.BidderConfigs = make([]BidderConfig, len(erp.BidderConfigs))
+		for i, bc := range erp.BidderConfigs {
+			clonedBidderConfig := BidderConfig{Bidders: sliceutil.Clone(bc.Bidders)}
+			if bc.Config != nil {
+				config := &Config{}
+				if bc.Config.ORTB2 != nil {
+					config.ORTB2 = &ORTB2{
+						Site: maputil.Clone(bc.Config.ORTB2.Site),
+						App:  maputil.Clone(bc.Config.ORTB2.App),
+						User: maputil.Clone(bc.Config.ORTB2.User),
+					}
+				}
+				clonedBidderConfig.Config = config
+			}
+			clone.BidderConfigs[i] = clonedBidderConfig
+		}
+	}
+
+	if erp.Cache != nil {
+		clone.Cache = &ExtRequestPrebidCache{}
+		if erp.Cache.Bids != nil {
+			clone.Cache.Bids = &ExtRequestPrebidCacheBids{}
+			clone.Cache.Bids.ReturnCreative = ptrutil.Clone(erp.Cache.Bids.ReturnCreative)
+		}
+		if erp.Cache.VastXML != nil {
+			clone.Cache.VastXML = &ExtRequestPrebidCacheVAST{}
+			clone.Cache.VastXML.ReturnCreative = ptrutil.Clone(erp.Cache.VastXML.ReturnCreative)
+		}
+	}
+
+	if erp.Channel != nil {
+		channel := *erp.Channel
+		clone.Channel = &channel
+	}
+
+	if erp.CurrencyConversions != nil {
+		newConvRates := make(map[string]map[string]float64, len(erp.CurrencyConversions.ConversionRates))
+		for key, val := range erp.CurrencyConversions.ConversionRates {
+			newConvRates[key] = maputil.Clone(val)
+		}
+		clone.CurrencyConversions = &ExtRequestCurrency{ConversionRates: newConvRates}
+		if erp.CurrencyConversions.UsePBSRates != nil {
+			clone.CurrencyConversions.UsePBSRates = ptrutil.ToPtr(*erp.CurrencyConversions.UsePBSRates)
+		}
+	}
+
+	if erp.Data != nil {
+		clone.Data = &ExtRequestPrebidData{Bidders: sliceutil.Clone(erp.Data.Bidders)}
+		if erp.Data.EidPermissions != nil {
+			newEidPermissions := make([]ExtRequestPrebidDataEidPermission, len(erp.Data.EidPermissions))
+			for i, eidp := range erp.Data.EidPermissions {
+				newEidPermissions[i] = ExtRequestPrebidDataEidPermission{
+					Source:  eidp.Source,
+					Bidders: sliceutil.Clone(eidp.Bidders),
+				}
+			}
+			clone.Data.EidPermissions = newEidPermissions
+		}
+	}
+
+	if erp.Experiment != nil {
+		clone.Experiment = &Experiment{}
+		if erp.Experiment.AdsCert != nil {
+			clone.Experiment.AdsCert = ptrutil.ToPtr(*erp.Experiment.AdsCert)
+		}
+	}
+
+	if erp.MultiBid != nil {
+		clone.MultiBid = make([]*ExtMultiBid, len(erp.MultiBid))
+		for i, mulBid := range erp.MultiBid {
+			newMulBid := &ExtMultiBid{
+				Bidder:                 mulBid.Bidder,
+				Bidders:                sliceutil.Clone(mulBid.Bidders),
+				TargetBidderCodePrefix: mulBid.TargetBidderCodePrefix,
+			}
+			if mulBid.MaxBids != nil {
+				newMulBid.MaxBids = ptrutil.ToPtr(*mulBid.MaxBids)
+			}
+			clone.MultiBid[i] = newMulBid
+		}
+	}
+
+	if erp.SChains != nil {
+		clone.SChains = make([]*ExtRequestPrebidSChain, len(erp.SChains))
+		for i, schain := range erp.SChains {
+			newChain := *schain
+			newNodes := sliceutil.Clone(schain.SChain.Nodes)
+			for j, node := range newNodes {
+				if node.HP != nil {
+					newNodes[j].HP = ptrutil.ToPtr(*newNodes[j].HP)
+				}
+			}
+			newChain.SChain.Nodes = newNodes
+			clone.SChains[i] = &newChain
+		}
+	}
+
+	clone.Server = ptrutil.Clone(erp.Server)
+
+	clone.StoredRequest = ptrutil.Clone(erp.StoredRequest)
+
+	if erp.Targeting != nil {
+		newTargeting := &ExtRequestTargeting{
+			IncludeFormat:     erp.Targeting.IncludeFormat,
+			DurationRangeSec:  sliceutil.Clone(erp.Targeting.DurationRangeSec),
+			PreferDeals:       erp.Targeting.PreferDeals,
+			AppendBidderNames: erp.Targeting.AppendBidderNames,
+		}
+		if erp.Targeting.PriceGranularity != nil {
+			newPriceGranularity := &PriceGranularity{
+				Ranges: sliceutil.Clone(erp.Targeting.PriceGranularity.Ranges),
+			}
+			newPriceGranularity.Precision = ptrutil.Clone(erp.Targeting.PriceGranularity.Precision)
+			newTargeting.PriceGranularity = newPriceGranularity
+		}
+		newTargeting.IncludeWinners = ptrutil.Clone(erp.Targeting.IncludeWinners)
+		newTargeting.IncludeBidderKeys = ptrutil.Clone(erp.Targeting.IncludeBidderKeys)
+		if erp.Targeting.IncludeBrandCategory != nil {
+			newIncludeBrandCategory := *erp.Targeting.IncludeBrandCategory
+			newIncludeBrandCategory.TranslateCategories = ptrutil.Clone(erp.Targeting.IncludeBrandCategory.TranslateCategories)
+			newTargeting.IncludeBrandCategory = &newIncludeBrandCategory
+		}
+		clone.Targeting = newTargeting
+	}
+
+	clone.NoSale = sliceutil.Clone(erp.NoSale)
+
+	if erp.AlternateBidderCodes != nil {
+		newAlternateBidderCodes := ExtAlternateBidderCodes{Enabled: erp.AlternateBidderCodes.Enabled}
+		if erp.AlternateBidderCodes.Bidders != nil {
+			newBidders := make(map[string]ExtAdapterAlternateBidderCodes, len(erp.AlternateBidderCodes.Bidders))
+			for key, val := range erp.AlternateBidderCodes.Bidders {
+				newBidders[key] = ExtAdapterAlternateBidderCodes{
+					Enabled:            val.Enabled,
+					AllowedBidderCodes: sliceutil.Clone(val.AllowedBidderCodes),
+				}
+			}
+			newAlternateBidderCodes.Bidders = newBidders
+		}
+		clone.AlternateBidderCodes = &newAlternateBidderCodes
+	}
+
+	if erp.Floors != nil {
+		clonedFloors := *erp.Floors
+		clonedFloors.Location = ptrutil.Clone(erp.Floors.Location)
+		if erp.Floors.Data != nil {
+			clonedData := *erp.Floors.Data
+			if erp.Floors.Data.ModelGroups != nil {
+				clonedData.ModelGroups = make([]PriceFloorModelGroup, len(erp.Floors.Data.ModelGroups))
+				for i, pfmg := range erp.Floors.Data.ModelGroups {
+					clonedData.ModelGroups[i] = pfmg
+					clonedData.ModelGroups[i].ModelWeight = ptrutil.Clone(pfmg.ModelWeight)
+					clonedData.ModelGroups[i].Schema.Fields = sliceutil.Clone(pfmg.Schema.Fields)
+					clonedData.ModelGroups[i].Values = maputil.Clone(pfmg.Values)
+				}
+			}
+			clonedFloors.Data = &clonedData
+		}
+		if erp.Floors.Enforcement != nil {
+			clonedFloors.Enforcement = &PriceFloorEnforcement{
+				EnforceJS:     ptrutil.Clone(erp.Floors.Enforcement.EnforceJS),
+				EnforcePBS:    ptrutil.Clone(erp.Floors.Enforcement.EnforcePBS),
+				FloorDeals:    ptrutil.Clone(erp.Floors.Enforcement.FloorDeals),
+				BidAdjustment: ptrutil.Clone(erp.Floors.Enforcement.BidAdjustment),
+				EnforceRate:   erp.Floors.Enforcement.EnforceRate,
+			}
+		}
+		clonedFloors.Enabled = ptrutil.Clone(erp.Floors.Enabled)
+		clonedFloors.Skipped = ptrutil.Clone(erp.Floors.Skipped)
+		clone.Floors = &clonedFloors
+	}
+	if erp.MultiBidMap != nil {
+		clone.MultiBidMap = make(map[string]ExtMultiBid, len(erp.MultiBidMap))
+		for k, v := range erp.MultiBidMap {
+			// Make v a deep copy of the ExtMultiBid struct
+			v.Bidders = sliceutil.Clone(v.Bidders)
+			v.MaxBids = ptrutil.Clone(v.MaxBids)
+			clone.MultiBidMap[k] = v
+		}
+	}
+	clone.AdServerTargeting = sliceutil.Clone(erp.AdServerTargeting)
+
+	return &clone
+}
diff --git a/openrtb_ext/request_wrapper.go b/openrtb_ext/request_wrapper.go
index 8fe33fff..5f5636d6 100644
--- a/openrtb_ext/request_wrapper.go
+++ b/openrtb_ext/request_wrapper.go
@@ -5,6 +5,9 @@ import (
 	"errors"
 
 	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/util/maputil"
+	"github.com/prebid/prebid-server/util/ptrutil"
+	"github.com/prebid/prebid-server/util/sliceutil"
 )
 
 // RequestWrapper wraps the OpenRTB request to provide a storage location for unmarshalled ext fields, so they
@@ -351,6 +354,27 @@ func (rw *RequestWrapper) rebuildSourceExt() error {
 	return nil
 }
 
+func (rw *RequestWrapper) Clone() *RequestWrapper {
+	if rw == nil {
+		return nil
+	}
+	clone := *rw
+	newImpWrappers := make([]*ImpWrapper, len(rw.impWrappers))
+	for i, iw := range rw.impWrappers {
+		newImpWrappers[i] = iw.Clone()
+	}
+	clone.impWrappers = newImpWrappers
+	clone.userExt = rw.userExt.Clone()
+	clone.deviceExt = rw.deviceExt.Clone()
+	clone.requestExt = rw.requestExt.Clone()
+	clone.appExt = rw.appExt.Clone()
+	clone.regExt = rw.regExt.Clone()
+	clone.siteExt = rw.siteExt.Clone()
+	clone.sourceExt = rw.sourceExt.Clone()
+
+	return &clone
+}
+
 // ---------------------------------------------------------------
 // UserExt provides an interface for request.user.ext
 // ---------------------------------------------------------------
@@ -606,6 +630,43 @@ func (ue *UserExt) SetEid(eid *[]openrtb2.EID) {
 	ue.eidsDirty = true
 }
 
+func (ue *UserExt) Clone() *UserExt {
+	if ue == nil {
+		return nil
+	}
+	clone := *ue
+	clone.ext = maputil.Clone(ue.ext)
+
+	if ue.consent != nil {
+		clonedConsent := *ue.consent
+		clone.consent = &clonedConsent
+	}
+
+	if ue.prebid != nil {
+		clone.prebid = &ExtUserPrebid{}
+		clone.prebid.BuyerUIDs = maputil.Clone(ue.prebid.BuyerUIDs)
+	}
+
+	if ue.eids != nil {
+		clonedEids := make([]openrtb2.EID, len(*ue.eids))
+		for i, eid := range *ue.eids {
+			newEid := eid
+			newEid.UIDs = sliceutil.Clone(eid.UIDs)
+			clonedEids[i] = newEid
+		}
+		clone.eids = &clonedEids
+	}
+
+	if ue.consentedProvidersSettingsIn != nil {
+		clone.consentedProvidersSettingsIn = &ConsentedProvidersSettingsIn{ConsentedProvidersString: ue.consentedProvidersSettingsIn.ConsentedProvidersString}
+	}
+	if ue.consentedProvidersSettingsOut != nil {
+		clone.consentedProvidersSettingsOut = &ConsentedProvidersSettingsOut{ConsentedProvidersList: sliceutil.Clone(ue.consentedProvidersSettingsOut.ConsentedProvidersList)}
+	}
+
+	return &clone
+}
+
 // ---------------------------------------------------------------
 // RequestExt provides an interface for request.ext
 // ---------------------------------------------------------------
@@ -741,6 +802,23 @@ func (re *RequestExt) SetSChain(schain *openrtb2.SupplyChain) {
 	re.schainDirty = true
 }
 
+func (re *RequestExt) Clone() *RequestExt {
+	if re == nil {
+		return nil
+	}
+
+	clone := *re
+	clone.ext = maputil.Clone(re.ext)
+
+	if re.prebid != nil {
+		clone.prebid = re.prebid.Clone()
+	}
+
+	clone.schain = cloneSupplyChain(re.schain)
+
+	return &clone
+}
+
 // ---------------------------------------------------------------
 // DeviceExt provides an interface for request.device.ext
 // ---------------------------------------------------------------
@@ -837,6 +915,26 @@ func (de *DeviceExt) SetPrebid(prebid *ExtDevicePrebid) {
 	de.prebidDirty = true
 }
 
+func (de *DeviceExt) Clone() *DeviceExt {
+	if de == nil {
+		return nil
+	}
+
+	clone := *de
+	clone.ext = maputil.Clone(de.ext)
+
+	if de.prebid != nil {
+		clonedPrebid := *de.prebid
+		if clonedPrebid.Interstitial != nil {
+			clonedInterstitial := *de.prebid.Interstitial
+			clonedPrebid.Interstitial = &clonedInterstitial
+		}
+		clone.prebid = &clonedPrebid
+	}
+
+	return &clone
+}
+
 // ---------------------------------------------------------------
 // AppExt provides an interface for request.app.ext
 // ---------------------------------------------------------------
@@ -929,6 +1027,19 @@ func (ae *AppExt) SetPrebid(prebid *ExtAppPrebid) {
 	ae.prebidDirty = true
 }
 
+func (ae *AppExt) Clone() *AppExt {
+	if ae == nil {
+		return nil
+	}
+
+	clone := *ae
+	clone.ext = maputil.Clone(ae.ext)
+
+	clone.prebid = ptrutil.Clone(ae.prebid)
+
+	return &clone
+}
+
 // ---------------------------------------------------------------
 // RegExt provides an interface for request.regs.ext
 // ---------------------------------------------------------------
@@ -1045,6 +1156,19 @@ func (re *RegExt) SetUSPrivacy(usPrivacy string) {
 	re.usPrivacyDirty = true
 }
 
+func (re *RegExt) Clone() *RegExt {
+	if re == nil {
+		return nil
+	}
+
+	clone := *re
+	clone.ext = maputil.Clone(re.ext)
+
+	clone.gdpr = ptrutil.Clone(re.gdpr)
+
+	return &clone
+}
+
 // ---------------------------------------------------------------
 // SiteExt provides an interface for request.site.ext
 // ---------------------------------------------------------------
@@ -1128,6 +1252,18 @@ func (se *SiteExt) SetAmp(amp *int8) {
 	se.ampDirty = true
 }
 
+func (se *SiteExt) Clone() *SiteExt {
+	if se == nil {
+		return nil
+	}
+
+	clone := *se
+	clone.ext = maputil.Clone(se.ext)
+	clone.amp = ptrutil.Clone(se.amp)
+
+	return &clone
+}
+
 // ---------------------------------------------------------------
 // SourceExt provides an interface for request.source.ext
 // ---------------------------------------------------------------
@@ -1219,6 +1355,19 @@ func (se *SourceExt) SetSChain(schain *openrtb2.SupplyChain) {
 	se.schainDirty = true
 }
 
+func (se *SourceExt) Clone() *SourceExt {
+	if se == nil {
+		return nil
+	}
+
+	clone := *se
+	clone.ext = maputil.Clone(se.ext)
+
+	clone.schain = cloneSupplyChain(se.schain)
+
+	return &clone
+}
+
 // ImpWrapper wraps an OpenRTB impression object to provide storage for unmarshalled ext fields, so they
 // will not need to be unmarshalled multiple times. It is intended to use the ImpWrapper via the RequestWrapper
 // and follow the same usage conventions.
@@ -1265,6 +1414,17 @@ func (w *ImpWrapper) rebuildImpExt() error {
 	return nil
 }
 
+func (w *ImpWrapper) Clone() *ImpWrapper {
+	if w == nil {
+		return nil
+	}
+
+	clone := *w
+	clone.impExt = w.impExt.Clone()
+
+	return &clone
+}
+
 // ---------------------------------------------------------------
 // ImpExt provides an interface for imp.ext
 // ---------------------------------------------------------------
@@ -1429,3 +1589,32 @@ func (e *ImpExt) GetGpId() string {
 func CreateImpExtForTesting(ext map[string]json.RawMessage, prebid *ExtImpPrebid) ImpExt {
 	return ImpExt{ext: ext, prebid: prebid}
 }
+
+func (e *ImpExt) Clone() *ImpExt {
+	if e == nil {
+		return nil
+	}
+
+	clone := *e
+	clone.ext = maputil.Clone(e.ext)
+
+	if e.prebid != nil {
+		clonedPrebid := *e.prebid
+		clonedPrebid.StoredRequest = ptrutil.Clone(e.prebid.StoredRequest)
+		clonedPrebid.StoredAuctionResponse = ptrutil.Clone(e.prebid.StoredAuctionResponse)
+		if e.prebid.StoredBidResponse != nil {
+			clonedPrebid.StoredBidResponse = make([]ExtStoredBidResponse, len(e.prebid.StoredBidResponse))
+			for i, sbr := range e.prebid.StoredBidResponse {
+				clonedPrebid.StoredBidResponse[i] = sbr
+				clonedPrebid.StoredBidResponse[i].ReplaceImpId = ptrutil.Clone(sbr.ReplaceImpId)
+			}
+		}
+		clonedPrebid.IsRewardedInventory = ptrutil.Clone(e.prebid.IsRewardedInventory)
+		clonedPrebid.Bidder = maputil.Clone(e.prebid.Bidder)
+		clonedPrebid.Options = ptrutil.Clone(e.prebid.Options)
+		clonedPrebid.Floors = ptrutil.Clone(e.prebid.Floors)
+		clone.prebid = &clonedPrebid
+	}
+
+	return &clone
+}
diff --git a/openrtb_ext/supplyChain.go b/openrtb_ext/supplyChain.go
new file mode 100644
index 00000000..6f023542
--- /dev/null
+++ b/openrtb_ext/supplyChain.go
@@ -0,0 +1,21 @@
+package openrtb_ext
+
+import (
+	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/util/ptrutil"
+)
+
+func cloneSupplyChain(schain *openrtb2.SupplyChain) *openrtb2.SupplyChain {
+	if schain == nil {
+		return nil
+	}
+	clone := *schain
+	clone.Nodes = make([]openrtb2.SupplyChainNode, len(schain.Nodes))
+	for i, node := range schain.Nodes {
+		clone.Nodes[i] = node
+		clone.Nodes[i].HP = ptrutil.Clone(schain.Nodes[i].HP)
+	}
+
+	return &clone
+
+}
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/adview.yaml b/static/bidder-info/adview.yaml
index 53158cf4..2871653f 100644
--- a/static/bidder-info/adview.yaml
+++ b/static/bidder-info/adview.yaml
@@ -2,6 +2,8 @@ endpoint: "https://bid.adview.com/agent/thirdAdxService/{{.AccountID}}"
 maintainer:
   email: "partner@adview.com"
 gvlVendorID: 1022
+endpointCompression: gzip
+modifyingVastXmlAllowed: true
 capabilities:
   app:
     mediaTypes:
diff --git a/static/bidder-info/frvradn.yaml b/static/bidder-info/frvradn.yaml
new file mode 100644
index 00000000..b5b2e7f2
--- /dev/null
+++ b/static/bidder-info/frvradn.yaml
@@ -0,0 +1,18 @@
+endpoint: "https://fran.frvr.com/api/v1/openrtb"
+maintainer:
+  email: "info@frvr.com"
+gvlVendorID: 1107
+modifyingVastXmlAllowed: false
+capabilities:
+  app:
+    mediaTypes:
+      - banner
+      - video
+  site:
+    mediaTypes:
+      - banner
+      - video
+userSync:
+  iframe:
+    url: "https://fran.frvr.com/api/v1/sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect_uri={{.RedirectURL}}"
+    userMacro: "{{UID}}"
\ No newline at end of file
diff --git a/static/bidder-params/frvradn.json b/static/bidder-params/frvradn.json
new file mode 100644
index 00000000..09edaba1
--- /dev/null
+++ b/static/bidder-params/frvradn.json
@@ -0,0 +1,17 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "FRVR Ad Network Adapter Params",
+  "description": "A schema which validates params accepted by the FRVR Ad Network adapter",
+  "type": "object",
+  "properties": {
+    "publisher_id": {
+      "type": ["string"],
+      "description": "Publisher ID"
+    },
+    "ad_unit_id": {
+      "type": ["string"],
+      "description": "Ad Unit ID"
+    }
+  },
+  "required": ["publisher_id", "ad_unit_id"]
+}
diff --git a/util/maputil/maputil.go b/util/maputil/maputil.go
index 49df0772..c33740d4 100644
--- a/util/maputil/maputil.go
+++ b/util/maputil/maputil.go
@@ -48,3 +48,16 @@ func HasElement(m map[string]interface{}, k ...string) bool {
 
 	return exists
 }
+
+// Clone creates an indepent copy of a map,
+func Clone[K comparable, V any](m map[K]V) map[K]V {
+	if m == nil {
+		return nil
+	}
+	clone := make(map[K]V, len(m))
+	for key, value := range m {
+		clone[key] = value
+	}
+
+	return clone
+}


Test patch:
diff --git a/adapters/frvradn/frvradn_test.go b/adapters/frvradn/frvradn_test.go
new file mode 100644
index 00000000..f50f319b
--- /dev/null
+++ b/adapters/frvradn/frvradn_test.go
@@ -0,0 +1,28 @@
+package frvradn
+
+import (
+	"github.com/stretchr/testify/assert"
+	"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.BidderFRVRAdNetwork, config.Adapter{
+		Endpoint: "https://fran.frvr.com/api/v1/openrtb",
+	}, config.Server{ExternalUrl: "https://host.com", GvlID: 1, DataCenter: "2"})
+
+	if buildErr != nil {
+		t.Fatalf("Builder returned unexpected error %v", buildErr)
+	}
+
+	adapterstest.RunJSONBidderTest(t, "frvradntest", bidder)
+}
+
+func TestInvalidEndpoint(t *testing.T) {
+	_, buildErr := Builder(openrtb_ext.BidderFRVRAdNetwork, config.Adapter{Endpoint: ""}, config.Server{})
+
+	assert.Error(t, buildErr)
+}
diff --git a/adapters/frvradn/frvradntest/exemplary/banner.json b/adapters/frvradn/frvradntest/exemplary/banner.json
new file mode 100644
index 00000000..1c9866aa
--- /dev/null
+++ b/adapters/frvradn/frvradntest/exemplary/banner.json
@@ -0,0 +1,105 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "ad_unit_id": "4fd8fadb-b218-41cd-a99f-8ae932b8c529",
+            "publisher_id": "7de498f7-f955-4696-a337-348b026c5e42"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://fran.frvr.com/api/v1/openrtb",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-banner-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              },
+              "ext": {
+                "ad_unit_id": "4fd8fadb-b218-41cd-a99f-8ae932b8c529",
+                "publisher_id": "7de498f7-f955-4696-a337-348b026c5e42"
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "test-seat",
+              "bid": [
+                {
+                  "id": "d4916d88-c0f4-4340-87c0-bc9054696d3a",
+                  "impid": "test-banner-id",
+                  "price": 0.55,
+                  "adm": "a creative",
+                  "adid": "123123",
+                  "adomain": ["advertiser.com"],
+                  "crid": "9999",
+                  "w": 300,
+                  "h": 600,
+                  "ext": {
+                    "prebid": {
+                      "type": "banner"
+                    }
+                  }
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "bids": [
+        {
+          "bid": {
+            "id": "d4916d88-c0f4-4340-87c0-bc9054696d3a",
+            "impid": "test-banner-id",
+            "price": 0.55,
+            "adm": "a creative",
+            "adid": "123123",
+            "adomain": ["advertiser.com"],
+            "crid": "9999",
+            "w": 300,
+            "h": 600,
+            "ext": {
+              "prebid": {
+                "type": "banner"
+              }
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/frvradn/frvradntest/exemplary/native.json b/adapters/frvradn/frvradntest/exemplary/native.json
new file mode 100644
index 00000000..9b350547
--- /dev/null
+++ b/adapters/frvradn/frvradntest/exemplary/native.json
@@ -0,0 +1,95 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-native-id",
+        "native": {
+          "request": "test-native-request"
+        },
+        "ext": {
+          "bidder": {
+            "ad_unit_id": "38cab549-569e-4362-a90f-f7c1d09d3e4c",
+            "publisher_id": "90f55919-525e-4e2f-99cb-fd4a59fb75a0"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://fran.frvr.com/api/v1/openrtb",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-native-id",
+              "native": {
+                "request": "test-native-request"
+              },
+              "ext": {
+                "ad_unit_id": "38cab549-569e-4362-a90f-f7c1d09d3e4c",
+                "publisher_id": "90f55919-525e-4e2f-99cb-fd4a59fb75a0"
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "test-seat",
+              "bid": [
+                {
+                  "id": "9244e776-e0c7-4d76-8c20-88971909114b",
+                  "impid": "test-native-id",
+                  "price": 2.50,
+                  "adm": "a creative",
+                  "adid": "9999",
+                  "adomain": ["advertiser.com"],
+                  "crid": "9999",
+                  "w": 300,
+                  "h": 600,
+                  "ext": {
+                    "prebid": {
+                      "type": "native"
+                    }
+                  }
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "bids": [
+        {
+          "bid": {
+            "id": "9244e776-e0c7-4d76-8c20-88971909114b",
+            "impid": "test-native-id",
+            "price": 2.50,
+            "adm": "a creative",
+            "adid": "9999",
+            "adomain": ["advertiser.com"],
+            "crid": "9999",
+            "w": 300,
+            "h": 600,
+            "ext": {
+              "prebid": {
+                "type": "native"
+              }
+            }
+          },
+          "type": "native"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/frvradn/frvradntest/exemplary/video.json b/adapters/frvradn/frvradntest/exemplary/video.json
new file mode 100644
index 00000000..9ff1bb31
--- /dev/null
+++ b/adapters/frvradn/frvradntest/exemplary/video.json
@@ -0,0 +1,101 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-video-id",
+        "video": {
+          "mimes": ["video/mp4"],
+          "w": 300,
+          "h": 600
+        },
+        "ext": {
+          "bidder": {
+            "ad_unit_id": "5da02f4f-96de-4a89-824a-6e8e1dfc495c",
+            "publisher_id": "9d66baf8-a80f-4378-9bd2-06ec09eaa86c"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://fran.frvr.com/api/v1/openrtb",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-video-id",
+              "video": {
+                "mimes": ["video/mp4"],
+                "w": 300,
+                "h": 600
+              },
+              "ext": {
+                "ad_unit_id": "5da02f4f-96de-4a89-824a-6e8e1dfc495c",
+                "publisher_id": "9d66baf8-a80f-4378-9bd2-06ec09eaa86c"
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "test-seat",
+              "bid": [
+                {
+                  "id": "42d72858-e071-40b2-ab74-27848e4d3336",
+                  "impid": "test-video-id",
+                  "price": 6.50,
+                  "nurl": "notification",
+                  "adm": "a creative",
+                  "adid": "9999",
+                  "adomain": ["advertiser.com"],
+                  "crid": "9999",
+                  "w": 300,
+                  "h": 600,
+                  "ext": {
+                    "prebid": {
+                      "type": "video"
+                    }
+                  }
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "bids": [
+        {
+          "bid": {
+            "id": "42d72858-e071-40b2-ab74-27848e4d3336",
+            "impid": "test-video-id",
+            "price": 6.50,
+            "nurl": "notification",
+            "adm": "a creative",
+            "adid": "9999",
+            "adomain": ["advertiser.com"],
+            "crid": "9999",
+            "w": 300,
+            "h": 600,
+            "ext": {
+              "prebid": {
+                "type": "video"
+              }
+            }
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/frvradn/frvradntest/supplemental/204.json b/adapters/frvradn/frvradntest/supplemental/204.json
new file mode 100644
index 00000000..48fe8a03
--- /dev/null
+++ b/adapters/frvradn/frvradntest/supplemental/204.json
@@ -0,0 +1,55 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "ad_unit_id": "aae06006-2c41-499c-a432-7b74bc8d9b28",
+            "publisher_id": "0af78b6e-45ad-4bad-8332-4b5e6a87b222"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://fran.frvr.com/api/v1/openrtb",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-banner-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              },
+              "ext": {
+                "ad_unit_id": "aae06006-2c41-499c-a432-7b74bc8d9b28",
+                "publisher_id": "0af78b6e-45ad-4bad-8332-4b5e6a87b222"
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 204
+      }
+    }
+  ],
+  "expectedBidResponses": []
+}
diff --git a/adapters/frvradn/frvradntest/supplemental/400.json b/adapters/frvradn/frvradntest/supplemental/400.json
new file mode 100644
index 00000000..a7e5f40d
--- /dev/null
+++ b/adapters/frvradn/frvradntest/supplemental/400.json
@@ -0,0 +1,60 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "ad_unit_id": "7fc326d4-243d-41ef-bbb4-4c76c4ad69cd",
+            "publisher_id": "4552e5bd-5ece-4f7d-a256-9069a33260a6"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://fran.frvr.com/api/v1/openrtb",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-banner-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              },
+              "ext": {
+                "ad_unit_id": "7fc326d4-243d-41ef-bbb4-4c76c4ad69cd",
+                "publisher_id": "4552e5bd-5ece-4f7d-a256-9069a33260a6"
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 400
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Unexpected status code: 400. Run with request.debug = 1 for more info",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/frvradn/frvradntest/supplemental/503.json b/adapters/frvradn/frvradntest/supplemental/503.json
new file mode 100644
index 00000000..11b9ce2d
--- /dev/null
+++ b/adapters/frvradn/frvradntest/supplemental/503.json
@@ -0,0 +1,60 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "ad_unit_id": "7fc326d4-243d-41ef-bbb4-4c76c4ad69cd",
+            "publisher_id": "4552e5bd-5ece-4f7d-a256-9069a33260a6"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://fran.frvr.com/api/v1/openrtb",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-banner-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              },
+              "ext": {
+                "ad_unit_id": "7fc326d4-243d-41ef-bbb4-4c76c4ad69cd",
+                "publisher_id": "4552e5bd-5ece-4f7d-a256-9069a33260a6"
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 503
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Unexpected status code: 503. Run with request.debug = 1 for more info",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/frvradn/frvradntest/supplemental/currency_converter.json b/adapters/frvradn/frvradntest/supplemental/currency_converter.json
new file mode 100644
index 00000000..45561d0b
--- /dev/null
+++ b/adapters/frvradn/frvradntest/supplemental/currency_converter.json
@@ -0,0 +1,133 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "bidfloor": 5,
+        "bidfloorcur": "EUR",
+        "ext": {
+          "bidder": {
+            "ad_unit_id": "33c76a5f-d9ef-4630-9e1e-7705673f1693",
+            "publisher_id": "0c0ac25e-e0b5-46d7-94d1-e1bf68034dd9"
+          }
+        }
+      }
+    ],
+    "ext": {
+      "prebid": {
+        "currency": {
+          "rates": {
+            "EUR": {
+              "USD": 1.5
+            }
+          },
+          "usepbsrates": false
+        }
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://fran.frvr.com/api/v1/openrtb",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-banner-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              },
+              "bidfloor": 7.5,
+              "bidfloorcur": "USD",
+              "ext": {
+                "ad_unit_id": "33c76a5f-d9ef-4630-9e1e-7705673f1693",
+                "publisher_id": "0c0ac25e-e0b5-46d7-94d1-e1bf68034dd9"
+              }
+            }
+          ],
+          "ext": {
+            "prebid": {
+              "currency": {
+                "rates": {
+                  "EUR": {
+                    "USD": 1.5
+                  }
+                },
+                "usepbsrates": false
+              }
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "test-seat",
+              "bid": [
+                {
+                  "id": "ed03a73b-3c9d-4883-a672-7919cb607c15",
+                  "impid": "test-banner-id",
+                  "price": 7.5,
+                  "adm": "a creative",
+                  "adid": "9999",
+                  "adomain": ["advertiser.com"],
+                  "crid": "9999",
+                  "w": 300,
+                  "h": 600,
+                  "ext": {
+                    "prebid": {
+                      "type": "banner"
+                    }
+                  }
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "bids": [
+        {
+          "bid": {
+            "id": "ed03a73b-3c9d-4883-a672-7919cb607c15",
+            "impid": "test-banner-id",
+            "price": 7.5,
+            "adm": "a creative",
+            "adid": "9999",
+            "adomain": ["advertiser.com"],
+            "crid": "9999",
+            "w": 300,
+            "h": 600,
+            "ext": {
+              "prebid": {
+                "type": "banner"
+              }
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/frvradn/frvradntest/supplemental/empty_object_response.json b/adapters/frvradn/frvradntest/supplemental/empty_object_response.json
new file mode 100644
index 00000000..e0e8959e
--- /dev/null
+++ b/adapters/frvradn/frvradntest/supplemental/empty_object_response.json
@@ -0,0 +1,56 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "ad_unit_id": "33c76a5f-d9ef-4630-9e1e-7705673f1693",
+            "publisher_id": "0c0ac25e-e0b5-46d7-94d1-e1bf68034dd9"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://fran.frvr.com/api/v1/openrtb",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-banner-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              },
+              "ext": {
+                "ad_unit_id": "33c76a5f-d9ef-4630-9e1e-7705673f1693",
+                "publisher_id": "0c0ac25e-e0b5-46d7-94d1-e1bf68034dd9"
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {}
+      }
+    }
+  ],
+  "expectedBidResponses": []
+}
diff --git a/adapters/frvradn/frvradntest/supplemental/empty_reponse.json b/adapters/frvradn/frvradntest/supplemental/empty_reponse.json
new file mode 100644
index 00000000..d6e2b059
--- /dev/null
+++ b/adapters/frvradn/frvradntest/supplemental/empty_reponse.json
@@ -0,0 +1,55 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "ad_unit_id": "33c76a5f-d9ef-4630-9e1e-7705673f1693",
+            "publisher_id": "0c0ac25e-e0b5-46d7-94d1-e1bf68034dd9"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://fran.frvr.com/api/v1/openrtb",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-banner-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              },
+              "ext": {
+                "ad_unit_id": "33c76a5f-d9ef-4630-9e1e-7705673f1693",
+                "publisher_id": "0c0ac25e-e0b5-46d7-94d1-e1bf68034dd9"
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200
+      }
+    }
+  ],
+  "expectedBidResponses": []
+}
diff --git a/adapters/frvradn/frvradntest/supplemental/missing_bid_ext.json b/adapters/frvradn/frvradntest/supplemental/missing_bid_ext.json
new file mode 100644
index 00000000..32b54f20
--- /dev/null
+++ b/adapters/frvradn/frvradntest/supplemental/missing_bid_ext.json
@@ -0,0 +1,81 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "ad_unit_id": "56f94390-e632-46e3-8e0f-e399d7d6e10d",
+            "publisher_id": "96ce87c5-1c9d-4456-a093-9d8162e54b44"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://fran.frvr.com/api/v1/openrtb",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-banner-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              },
+              "ext": {
+                "ad_unit_id": "56f94390-e632-46e3-8e0f-e399d7d6e10d",
+                "publisher_id": "96ce87c5-1c9d-4456-a093-9d8162e54b44"
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "test-seat",
+              "bid": [
+                {
+                  "id": "ec174d08-4c15-4ea0-95cc-e55bb5bfa0f9\n\n",
+                  "impid": "random-imp",
+                  "price": 0.50,
+                  "adm": "a creative",
+                  "adid": "9999",
+                  "adomain": ["advertiser.com"],
+                  "crid": "9999",
+                  "w": 300,
+                  "h": 600
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "unable to deserialize imp random-imp bid.ext",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/frvradn/frvradntest/supplemental/missing_ext.json b/adapters/frvradn/frvradntest/supplemental/missing_ext.json
new file mode 100644
index 00000000..4c22142b
--- /dev/null
+++ b/adapters/frvradn/frvradntest/supplemental/missing_ext.json
@@ -0,0 +1,24 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        }
+      }
+    ]
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "missing ext",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/frvradn/frvradntest/supplemental/missing_ext_bidder.json b/adapters/frvradn/frvradntest/supplemental/missing_ext_bidder.json
new file mode 100644
index 00000000..b88f989a
--- /dev/null
+++ b/adapters/frvradn/frvradntest/supplemental/missing_ext_bidder.json
@@ -0,0 +1,25 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {}
+      }
+    ]
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "missing ext.bidder",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/frvradn/frvradntest/supplemental/missing_params.json b/adapters/frvradn/frvradntest/supplemental/missing_params.json
new file mode 100644
index 00000000..4aba47d2
--- /dev/null
+++ b/adapters/frvradn/frvradntest/supplemental/missing_params.json
@@ -0,0 +1,27 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {}
+        }
+      }
+    ]
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "publisher_id and ad_unit_id are required",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/frvradn/frvradntest/supplemental/unknown_currency.json b/adapters/frvradn/frvradntest/supplemental/unknown_currency.json
new file mode 100644
index 00000000..74332ad6
--- /dev/null
+++ b/adapters/frvradn/frvradntest/supplemental/unknown_currency.json
@@ -0,0 +1,44 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-banner-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "bidfloor": 1,
+        "bidfloorcur": "EUR",
+        "ext": {
+          "bidder": {
+            "ad_unit_id": "33c76a5f-d9ef-4630-9e1e-7705673f1693",
+            "publisher_id": "0c0ac25e-e0b5-46d7-94d1-e1bf68034dd9"
+          }
+        }
+      }
+    ],
+    "ext": {
+      "prebid": {
+        "currency": {
+          "rates": {
+            "GBP": {
+              "USD": 1.5
+            }
+          },
+          "usepbsrates": false
+        }
+      }
+    }
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "Currency conversion rate not found: 'EUR' => 'USD'",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/frvradn/frvradntest/supplemental/unknown_imp_media_type.json b/adapters/frvradn/frvradntest/supplemental/unknown_imp_media_type.json
new file mode 100644
index 00000000..7ed869d7
--- /dev/null
+++ b/adapters/frvradn/frvradntest/supplemental/unknown_imp_media_type.json
@@ -0,0 +1,66 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-banner-id",
+        "ext": {
+          "bidder": {
+            "ad_unit_id": "cdb3a029-ce0a-4b4b-b404-1d5f9ff86d7d",
+            "publisher_id": "dc4c0f5e-8637-4017-8aa8-af59d4bc03af"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://fran.frvr.com/api/v1/openrtb",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-banner-id",
+              "ext": {
+                "ad_unit_id": "cdb3a029-ce0a-4b4b-b404-1d5f9ff86d7d",
+                "publisher_id": "dc4c0f5e-8637-4017-8aa8-af59d4bc03af"
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "test-seat",
+              "bid": [
+                {
+                  "id": "e2710817-83dd-421f-9cd6-55bed1ca8a41",
+                  "impid": "test-banner-id",
+                  "price": 5.50,
+                  "adm": "test-banner",
+                  "adid": "9999",
+                  "adomain": ["advertiser.com"],
+                  "crid": "9999",
+                  "w": 300,
+                  "h": 600,
+                  "ext": {}
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "imp test-banner-id with unknown media type",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/frvradn/params_test.go b/adapters/frvradn/params_test.go
new file mode 100644
index 00000000..2d4836c1
--- /dev/null
+++ b/adapters/frvradn/params_test.go
@@ -0,0 +1,48 @@
+package frvradn
+
+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.BidderFRVRAdNetwork, 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.BidderFRVRAdNetwork, json.RawMessage(p)); err == nil {
+			t.Errorf("Schema allowed invalid params: %s", p)
+		}
+	}
+}
+
+var validParams = []string{
+	`{"publisher_id": "247f36ed-bda5-4159-86f1-e383849e7810", "ad_unit_id": "63c36c82-3246-4931-97f9-4f16a9639ba9"}`,
+}
+
+var invalidParams = []string{
+	`null`,
+	`nil`,
+	``,
+	`[]`,
+	`{}`,
+	`{"publisher_id": "247f36ed-bda5-4159-86f1-e383849e7810"}`,
+	`{"ad_unit_id": "247f36ed-bda5-4159-86f1-e383849e7810"}`,
+}
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/auction_test.go b/endpoints/openrtb2/auction_test.go
index 00c6d7f7..f291d2e8 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -3590,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",
diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go
index 2aa03573..4e367ebe 100644
--- a/exchange/bidder_test.go
+++ b/exchange/bidder_test.go
@@ -2026,6 +2026,7 @@ func TestCallRecordAdapterConnections(t *testing.T) {
 
 	mockMetricEngine.On("RecordAdapterConnections", expectedAdapterName, false, mock.MatchedBy(compareConnWaitTime)).Once()
 	mockMetricEngine.On("RecordOverheadTime", metrics.PreBidder, mock.Anything).Once()
+	mockMetricEngine.On("RecordBidderServerResponseTime", mock.Anything).Once()
 
 	// Run requestBid using an http.Client with a mock handler
 	bidder := AdaptBidder(bidderImpl, server.Client(), &config.Configuration{}, mockMetricEngine, openrtb_ext.BidderAppnexus, nil, "")
@@ -2087,6 +2088,7 @@ func TestCallRecordDNSTime(t *testing.T) {
 	metricsMock := &metrics.MetricsEngineMock{}
 	metricsMock.Mock.On("RecordDNSTime", mock.Anything).Return()
 	metricsMock.On("RecordOverheadTime", metrics.PreBidder, mock.Anything).Once()
+	metricsMock.On("RecordBidderServerResponseTime", 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{})
@@ -2109,6 +2111,7 @@ func TestCallRecordTLSHandshakeTime(t *testing.T) {
 	metricsMock := &metrics.MetricsEngineMock{}
 	metricsMock.Mock.On("RecordTLSHandshakeTime", mock.Anything).Return()
 	metricsMock.On("RecordOverheadTime", metrics.PreBidder, mock.Anything).Once()
+	metricsMock.On("RecordBidderServerResponseTime", 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{})
diff --git a/metrics/go_metrics_test.go b/metrics/go_metrics_test.go
index 1b0b20e4..1ab22ac1 100644
--- a/metrics/go_metrics_test.go
+++ b/metrics/go_metrics_test.go
@@ -83,6 +83,7 @@ func TestNewMetrics(t *testing.T) {
 	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])
+	ensureContains(t, registry, "bidder_server_response_time_seconds", m.BidderServerResponseTimer)
 
 	for module, stages := range moduleStageNames {
 		for _, stage := range stages {
@@ -420,6 +421,36 @@ func TestRecordTLSHandshakeTime(t *testing.T) {
 	}
 }
 
+func TestRecordBidderServerResponseTime(t *testing.T) {
+	testCases := []struct {
+		name          string
+		time          time.Duration
+		expectedCount int64
+		expectedSum   int64
+	}{
+		{
+			name:          "record-bidder-server-response-time-1",
+			time:          time.Duration(500),
+			expectedCount: 1,
+			expectedSum:   500,
+		},
+		{
+			name:          "record-bidder-server-response-time-2",
+			time:          time.Duration(500),
+			expectedCount: 2,
+			expectedSum:   1000,
+		},
+	}
+	for _, test := range testCases {
+		registry := metrics.NewRegistry()
+		m := NewMetrics(registry, []openrtb_ext.BidderName{openrtb_ext.BidderAppnexus}, config.DisabledMetrics{AccountAdapterDetails: true}, nil, nil)
+
+		m.RecordBidderServerResponseTime(test.time)
+
+		assert.Equal(t, test.time.Nanoseconds(), m.BidderServerResponseTimer.Sum(), test.name)
+	}
+}
+
 func TestRecordAdapterConnections(t *testing.T) {
 	var fakeBidder openrtb_ext.BidderName = "fooAdvertising"
 
diff --git a/metrics/prometheus/prometheus_test.go b/metrics/prometheus/prometheus_test.go
index d2ede844..32f7848c 100644
--- a/metrics/prometheus/prometheus_test.go
+++ b/metrics/prometheus/prometheus_test.go
@@ -1475,6 +1475,39 @@ func TestRecordTLSHandshakeTime(t *testing.T) {
 	}
 }
 
+func TestRecordBidderServerResponseTime(t *testing.T) {
+	testCases := []struct {
+		description   string
+		timeInMs      float64
+		expectedCount uint64
+		expectedSum   float64
+	}{
+		{
+			description:   "record-bidder-server-response-time-1",
+			timeInMs:      500,
+			expectedCount: 1,
+			expectedSum:   0.5,
+		},
+		{
+			description:   "record-bidder-server-response-time-2",
+			timeInMs:      400,
+			expectedCount: 1,
+			expectedSum:   0.4,
+		},
+	}
+	for _, test := range testCases {
+		pm := createMetricsForTesting()
+		pm.RecordBidderServerResponseTime(time.Duration(test.timeInMs) * time.Millisecond)
+
+		m := dto.Metric{}
+		pm.bidderServerResponseTimer.Write(&m)
+		histogram := *m.GetHistogram()
+
+		assert.Equal(t, test.expectedCount, histogram.GetSampleCount())
+		assert.Equal(t, test.expectedSum, histogram.GetSampleSum())
+	}
+}
+
 func TestRecordAdapterConnections(t *testing.T) {
 
 	type testIn struct {
diff --git a/openrtb_ext/request_test.go b/openrtb_ext/request_test.go
index 8d1ecee2..94c042db 100644
--- a/openrtb_ext/request_test.go
+++ b/openrtb_ext/request_test.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"testing"
 
+	"github.com/prebid/openrtb/v19/openrtb2"
 	"github.com/prebid/prebid-server/util/ptrutil"
 	"github.com/stretchr/testify/assert"
 )
@@ -141,3 +142,628 @@ var validGranularityTests []granularityTestData = []granularityTestData{
 		},
 	},
 }
+
+func TestCloneExtRequestPrebid(t *testing.T) {
+	testCases := []struct {
+		name       string
+		prebid     *ExtRequestPrebid
+		prebidCopy *ExtRequestPrebid                            // manual copy of above prebid object to verify against
+		mutator    func(t *testing.T, prebid *ExtRequestPrebid) // function to modify the prebid object
+	}{
+		{
+			name:       "Nil", // Verify the nil case
+			prebid:     nil,
+			prebidCopy: nil,
+			mutator:    func(t *testing.T, prebid *ExtRequestPrebid) {},
+		},
+		{
+			name: "NoMutateTest",
+			prebid: &ExtRequestPrebid{
+				Aliases:              map[string]string{"alias1": "bidder1"},
+				BidAdjustmentFactors: map[string]float64{"bidder5": 1.2},
+				BidderParams:         json.RawMessage(`{}`),
+				Channel: &ExtRequestPrebidChannel{
+					Name:    "ABC",
+					Version: "1.0",
+				},
+				Debug: true,
+				Experiment: &Experiment{
+					AdsCert: &AdsCert{
+						Enabled: false,
+					},
+				},
+				Server: &ExtRequestPrebidServer{
+					ExternalUrl: "http://www.example.com",
+					GvlID:       5,
+					DataCenter:  "Universe Central",
+				},
+				SupportDeals: true,
+			},
+			prebidCopy: &ExtRequestPrebid{
+				Aliases:              map[string]string{"alias1": "bidder1"},
+				BidAdjustmentFactors: map[string]float64{"bidder5": 1.2},
+				BidderParams:         json.RawMessage(`{}`),
+				Channel: &ExtRequestPrebidChannel{
+					Name:    "ABC",
+					Version: "1.0",
+				},
+				Debug: true,
+				Experiment: &Experiment{
+					AdsCert: &AdsCert{
+						Enabled: false,
+					},
+				},
+				Server: &ExtRequestPrebidServer{
+					ExternalUrl: "http://www.example.com",
+					GvlID:       5,
+					DataCenter:  "Universe Central",
+				},
+				SupportDeals: true,
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {},
+		},
+		{
+			name: "GeneralTest",
+			prebid: &ExtRequestPrebid{
+				Aliases:              map[string]string{"alias1": "bidder1"},
+				BidAdjustmentFactors: map[string]float64{"bidder5": 1.2},
+				BidderParams:         json.RawMessage(`{}`),
+				Channel: &ExtRequestPrebidChannel{
+					Name:    "ABC",
+					Version: "1.0",
+				},
+				Debug: true,
+				Experiment: &Experiment{
+					AdsCert: &AdsCert{
+						Enabled: false,
+					},
+				},
+				Server: &ExtRequestPrebidServer{
+					ExternalUrl: "http://www.example.com",
+					GvlID:       5,
+					DataCenter:  "Universe Central",
+				},
+				SupportDeals: true,
+			},
+			prebidCopy: &ExtRequestPrebid{
+				Aliases:              map[string]string{"alias1": "bidder1"},
+				BidAdjustmentFactors: map[string]float64{"bidder5": 1.2},
+				BidderParams:         json.RawMessage(`{}`),
+				Channel: &ExtRequestPrebidChannel{
+					Name:    "ABC",
+					Version: "1.0",
+				},
+				Debug: true,
+				Experiment: &Experiment{
+					AdsCert: &AdsCert{
+						Enabled: false,
+					},
+				},
+				Server: &ExtRequestPrebidServer{
+					ExternalUrl: "http://www.example.com",
+					GvlID:       5,
+					DataCenter:  "Universe Central",
+				},
+				SupportDeals: true,
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				prebid.Aliases["alias2"] = "bidder52"
+				prebid.Aliases["alias1"] = "some other"
+				prebid.AliasGVLIDs = map[string]uint16{"alias2": 42}
+				prebid.BidAdjustmentFactors["alias2"] = 1.1
+				delete(prebid.BidAdjustmentFactors, "bidder5")
+				prebid.BidderParams = json.RawMessage(`{"someJSON": true}`)
+				prebid.Channel = nil
+				prebid.Data = &ExtRequestPrebidData{
+					EidPermissions: []ExtRequestPrebidDataEidPermission{{Source: "mySource", Bidders: []string{"sauceBidder"}}},
+				}
+				prebid.Events = json.RawMessage(`{}`)
+				prebid.Server.GvlID = 7
+				prebid.SupportDeals = false
+			},
+		},
+		{
+			name: "BidderConfig",
+			prebid: &ExtRequestPrebid{
+				BidderConfigs: []BidderConfig{
+					{
+						Bidders: []string{"Bidder1", "bidder2"},
+						Config:  &Config{&ORTB2{Site: map[string]json.RawMessage{"test": json.RawMessage(`{}`)}}},
+					},
+					{
+						Bidders: []string{"Bidder5", "bidder17"},
+						Config:  &Config{&ORTB2{Site: map[string]json.RawMessage{"test": json.RawMessage(`{"foo":"bar"}`), "more": json.RawMessage(`{}`)}}},
+					},
+					{
+						Bidders: []string{"foo"},
+						Config:  &Config{&ORTB2{User: map[string]json.RawMessage{"abc": json.RawMessage(`123`)}}},
+					},
+				},
+			},
+			prebidCopy: &ExtRequestPrebid{
+				BidderConfigs: []BidderConfig{
+					{
+						Bidders: []string{"Bidder1", "bidder2"},
+						Config:  &Config{&ORTB2{Site: map[string]json.RawMessage{"test": json.RawMessage(`{}`)}}},
+					},
+					{
+						Bidders: []string{"Bidder5", "bidder17"},
+						Config:  &Config{&ORTB2{Site: map[string]json.RawMessage{"test": json.RawMessage(`{"foo":"bar"}`), "more": json.RawMessage(`{}`)}}},
+					},
+					{
+						Bidders: []string{"foo"},
+						Config:  &Config{&ORTB2{User: map[string]json.RawMessage{"abc": json.RawMessage(`123`)}}},
+					},
+				},
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				prebid.BidderConfigs[0].Bidders = append(prebid.BidderConfigs[0].Bidders, "bidder4")
+				prebid.BidderConfigs[1] = BidderConfig{
+					Bidders: []string{"george"},
+					Config:  &Config{nil},
+				}
+				prebid.BidderConfigs[2].Config.ORTB2.User["abc"] = json.RawMessage(`{"id": 345}`)
+				prebid.BidderConfigs = append(prebid.BidderConfigs, BidderConfig{
+					Bidders: []string{"bidder2"},
+					Config:  &Config{&ORTB2{}},
+				})
+			},
+		},
+		{
+			name: "Cache",
+			prebid: &ExtRequestPrebid{
+				Cache: &ExtRequestPrebidCache{
+					Bids: &ExtRequestPrebidCacheBids{
+						ReturnCreative: ptrutil.ToPtr(true),
+					},
+					VastXML: &ExtRequestPrebidCacheVAST{
+						ReturnCreative: ptrutil.ToPtr(false),
+					},
+				},
+			},
+			prebidCopy: &ExtRequestPrebid{
+				Cache: &ExtRequestPrebidCache{
+					Bids: &ExtRequestPrebidCacheBids{
+						ReturnCreative: ptrutil.ToPtr(true),
+					},
+					VastXML: &ExtRequestPrebidCacheVAST{
+						ReturnCreative: ptrutil.ToPtr(false),
+					},
+				},
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				prebid.Cache.Bids.ReturnCreative = ptrutil.ToPtr(false)
+				prebid.Cache.Bids = nil
+				prebid.Cache.VastXML = &ExtRequestPrebidCacheVAST{
+					ReturnCreative: ptrutil.ToPtr(true),
+				}
+			},
+		},
+		{
+			name: "Currency",
+			prebid: &ExtRequestPrebid{
+				CurrencyConversions: &ExtRequestCurrency{
+					ConversionRates: map[string]map[string]float64{"A": {"X": 5.4}},
+					UsePBSRates:     ptrutil.ToPtr(false),
+				},
+			},
+			prebidCopy: &ExtRequestPrebid{
+				CurrencyConversions: &ExtRequestCurrency{
+					ConversionRates: map[string]map[string]float64{"A": {"X": 5.4}},
+					UsePBSRates:     ptrutil.ToPtr(false),
+				},
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				prebid.CurrencyConversions.ConversionRates["A"]["X"] = 3.4
+				prebid.CurrencyConversions.ConversionRates["B"] = make(map[string]float64)
+				prebid.CurrencyConversions.ConversionRates["B"]["Y"] = 0.76
+				prebid.CurrencyConversions.UsePBSRates = ptrutil.ToPtr(true)
+			},
+		},
+		{
+			name: "Data",
+			prebid: &ExtRequestPrebid{
+				Data: &ExtRequestPrebidData{
+					EidPermissions: []ExtRequestPrebidDataEidPermission{
+						{
+							Source:  "Sauce",
+							Bidders: []string{"G", "H"},
+						},
+						{
+							Source:  "Black Hole",
+							Bidders: []string{"Q", "P"},
+						},
+					},
+					Bidders: []string{"A", "B", "C"},
+				},
+			},
+			prebidCopy: &ExtRequestPrebid{
+				Data: &ExtRequestPrebidData{
+					EidPermissions: []ExtRequestPrebidDataEidPermission{
+						{
+							Source:  "Sauce",
+							Bidders: []string{"G", "H"},
+						},
+						{
+							Source:  "Black Hole",
+							Bidders: []string{"Q", "P"},
+						},
+					},
+					Bidders: []string{"A", "B", "C"},
+				},
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				prebid.Data.EidPermissions[0].Source = "Fuzzy Bunnies"
+				prebid.Data.EidPermissions[1].Bidders[0] = "X"
+				prebid.Data.EidPermissions[0].Bidders = append(prebid.Data.EidPermissions[0].Bidders, "R")
+				prebid.Data.EidPermissions = append(prebid.Data.EidPermissions, ExtRequestPrebidDataEidPermission{Source: "Harry"})
+				prebid.Data.Bidders[1] = "D"
+				prebid.Data.Bidders = append(prebid.Data.Bidders, "E")
+			},
+		},
+		{
+			name: "Multibid",
+			prebid: &ExtRequestPrebid{
+				MultiBid: []*ExtMultiBid{
+					{Bidder: "somebidder", MaxBids: ptrutil.ToPtr(3), TargetBidderCodePrefix: "SB"},
+					{Bidders: []string{"A", "B", "C"}, MaxBids: ptrutil.ToPtr(4)},
+				},
+			},
+			prebidCopy: &ExtRequestPrebid{
+				MultiBid: []*ExtMultiBid{
+					{Bidder: "somebidder", MaxBids: ptrutil.ToPtr(3), TargetBidderCodePrefix: "SB"},
+					{Bidders: []string{"A", "B", "C"}, MaxBids: ptrutil.ToPtr(4)},
+				},
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				prebid.MultiBid[0].MaxBids = ptrutil.ToPtr(2)
+				prebid.MultiBid[1].Bidders = []string{"C", "D", "E", "F"}
+				prebid.MultiBid = []*ExtMultiBid{
+					{Bidder: "otherbid"},
+				}
+			},
+		},
+		{
+			name: "PassthroughSChains",
+			prebid: &ExtRequestPrebid{
+				SChains: []*ExtRequestPrebidSChain{
+					{
+						Bidders: []string{"A", "B", "C"},
+						SChain: openrtb2.SupplyChain{
+							Complete: 1,
+							Ver:      "2.2",
+							Ext:      json.RawMessage(`{"foo": "bar"}`),
+							Nodes: []openrtb2.SupplyChainNode{
+								{
+									ASI:    "something",
+									Domain: "example.com",
+									HP:     openrtb2.Int8Ptr(1),
+								},
+							},
+						},
+					},
+				},
+			},
+			prebidCopy: &ExtRequestPrebid{
+				SChains: []*ExtRequestPrebidSChain{
+					{
+						Bidders: []string{"A", "B", "C"},
+						SChain: openrtb2.SupplyChain{
+							Complete: 1,
+							Ver:      "2.2",
+							Ext:      json.RawMessage(`{"foo": "bar"}`),
+							Nodes: []openrtb2.SupplyChainNode{
+								{
+									ASI:    "something",
+									Domain: "example.com",
+									HP:     openrtb2.Int8Ptr(1),
+								},
+							},
+						},
+					},
+				},
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				prebid.Passthrough = json.RawMessage(`{"bar": "foo"}`)
+				prebid.SChains[0].Bidders = append(prebid.SChains[0].Bidders, "D")
+				prebid.SChains[0].SChain.Ver = "2.3"
+				prebid.SChains[0].SChain.Complete = 0
+				prebid.SChains[0].SChain.Nodes[0].Name = "Alice"
+				prebid.SChains[0].SChain.Nodes[0].ASI = "New ASI"
+				prebid.SChains[0].SChain.Nodes[0].HP = openrtb2.Int8Ptr(0)
+				prebid.SChains[0].SChain.Nodes = append(prebid.SChains[0].SChain.Nodes, prebid.SChains[0].SChain.Nodes[0])
+				prebid.SChains = append(prebid.SChains, prebid.SChains[0])
+			},
+		},
+		{
+			name: "StoredRequest",
+			prebid: &ExtRequestPrebid{
+				StoredRequest: &ExtStoredRequest{
+					ID: "abc123",
+				},
+			},
+			prebidCopy: &ExtRequestPrebid{
+				StoredRequest: &ExtStoredRequest{
+					ID: "abc123",
+				},
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				prebid.StoredRequest.ID = "nada"
+				prebid.StoredRequest = &ExtStoredRequest{ID: "ID"}
+			},
+		},
+		{
+			name: "Targeting",
+			prebid: &ExtRequestPrebid{
+				Targeting: &ExtRequestTargeting{
+					PriceGranularity: &PriceGranularity{
+						Precision: ptrutil.ToPtr(2),
+						Ranges: []GranularityRange{
+							{Max: 2.0, Increment: 0.1},
+							{Max: 10.0, Increment: 0.5},
+							{Max: 20.0, Increment: 1.0},
+						},
+					},
+					IncludeWinners: ptrutil.ToPtr(true),
+					IncludeBrandCategory: &ExtIncludeBrandCategory{
+						PrimaryAdServer:     1,
+						Publisher:           "Bob",
+						TranslateCategories: ptrutil.ToPtr(true),
+					},
+					DurationRangeSec: []int{1, 2, 3},
+				},
+			},
+			prebidCopy: &ExtRequestPrebid{
+				Targeting: &ExtRequestTargeting{
+					PriceGranularity: &PriceGranularity{
+						Precision: ptrutil.ToPtr(2),
+						Ranges: []GranularityRange{
+							{Max: 2.0, Increment: 0.1},
+							{Max: 10.0, Increment: 0.5},
+							{Max: 20.0, Increment: 1.0},
+						},
+					},
+					IncludeWinners: ptrutil.ToPtr(true),
+					IncludeBrandCategory: &ExtIncludeBrandCategory{
+						PrimaryAdServer:     1,
+						Publisher:           "Bob",
+						TranslateCategories: ptrutil.ToPtr(true),
+					},
+					DurationRangeSec: []int{1, 2, 3},
+				},
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				prebid.Targeting.PriceGranularity.Ranges[1].Max = 12
+				prebid.Targeting.PriceGranularity.Ranges[1].Min = 2.0
+				prebid.Targeting.PriceGranularity.Ranges = append(prebid.Targeting.PriceGranularity.Ranges, GranularityRange{Max: 50, Increment: 2.0})
+				prebid.Targeting.IncludeWinners = nil
+				prebid.Targeting.IncludeBidderKeys = ptrutil.ToPtr(true)
+				prebid.Targeting.IncludeBrandCategory.TranslateCategories = ptrutil.ToPtr(false)
+				prebid.Targeting.IncludeBrandCategory = nil
+				prebid.Targeting.DurationRangeSec[1] = 5
+				prebid.Targeting.DurationRangeSec = append(prebid.Targeting.DurationRangeSec, 1)
+				prebid.Targeting.AppendBidderNames = true
+			},
+		},
+		{
+			name: "NoSale",
+			prebid: &ExtRequestPrebid{
+				NoSale: []string{"A", "B", "C"},
+			},
+			prebidCopy: &ExtRequestPrebid{
+				NoSale: []string{"A", "B", "C"},
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				prebid.NoSale[1] = "G"
+				prebid.NoSale = append(prebid.NoSale, "D")
+			},
+		},
+		{
+			name: "AlternateBidderCodes",
+			prebid: &ExtRequestPrebid{
+				AlternateBidderCodes: &ExtAlternateBidderCodes{
+					Enabled: true,
+					Bidders: map[string]ExtAdapterAlternateBidderCodes{
+						"X": {Enabled: true, AllowedBidderCodes: []string{"A", "B", "C"}},
+						"Y": {Enabled: false, AllowedBidderCodes: []string{"C", "B", "G"}},
+					},
+				},
+			},
+			prebidCopy: &ExtRequestPrebid{
+				AlternateBidderCodes: &ExtAlternateBidderCodes{
+					Enabled: true,
+					Bidders: map[string]ExtAdapterAlternateBidderCodes{
+						"X": {Enabled: true, AllowedBidderCodes: []string{"A", "B", "C"}},
+						"Y": {Enabled: false, AllowedBidderCodes: []string{"C", "B", "G"}},
+					},
+				},
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				newAABC := prebid.AlternateBidderCodes.Bidders["X"]
+				newAABC.Enabled = false
+				newAABC.AllowedBidderCodes[1] = "F"
+				newAABC.AllowedBidderCodes = append(newAABC.AllowedBidderCodes, "Z")
+				prebid.AlternateBidderCodes.Bidders["X"] = newAABC
+				prebid.AlternateBidderCodes.Bidders["Z"] = ExtAdapterAlternateBidderCodes{Enabled: true, AllowedBidderCodes: []string{"G", "Z"}}
+				prebid.AlternateBidderCodes.Enabled = false
+				prebid.AlternateBidderCodes = nil
+			},
+		},
+		{
+			name: "Floors",
+			prebid: &ExtRequestPrebid{
+				Floors: &PriceFloorRules{
+					FloorMin:    0.25,
+					FloorMinCur: "EUR",
+					Location: &PriceFloorEndpoint{
+						URL: "http://www.example.com",
+					},
+					Data: &PriceFloorData{
+						Currency: "USD",
+						SkipRate: 3,
+						ModelGroups: []PriceFloorModelGroup{
+							{
+								Currency:    "USD",
+								ModelWeight: ptrutil.ToPtr(0),
+								SkipRate:    2,
+								Schema: PriceFloorSchema{
+									Fields:    []string{"A", "B"},
+									Delimiter: "^",
+								},
+								Values: map[string]float64{"A": 2, "B": 1},
+							},
+						},
+					},
+					Enforcement: &PriceFloorEnforcement{
+						EnforceJS:   ptrutil.ToPtr(true),
+						FloorDeals:  ptrutil.ToPtr(false),
+						EnforceRate: 5,
+					},
+					Enabled:       ptrutil.ToPtr(true),
+					FloorProvider: "Someone",
+				},
+			},
+			prebidCopy: &ExtRequestPrebid{
+				Floors: &PriceFloorRules{
+					FloorMin:    0.25,
+					FloorMinCur: "EUR",
+					Location: &PriceFloorEndpoint{
+						URL: "http://www.example.com",
+					},
+					Data: &PriceFloorData{
+						Currency: "USD",
+						SkipRate: 3,
+						ModelGroups: []PriceFloorModelGroup{
+							{
+								Currency:    "USD",
+								ModelWeight: ptrutil.ToPtr(0),
+								SkipRate:    2,
+								Schema: PriceFloorSchema{
+									Fields:    []string{"A", "B"},
+									Delimiter: "^",
+								},
+								Values: map[string]float64{"A": 2, "B": 1},
+							},
+						},
+					},
+					Enforcement: &PriceFloorEnforcement{
+						EnforceJS:   ptrutil.ToPtr(true),
+						FloorDeals:  ptrutil.ToPtr(false),
+						EnforceRate: 5,
+					},
+					Enabled:       ptrutil.ToPtr(true),
+					FloorProvider: "Someone",
+				},
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				prebid.Floors.Data.ModelGroups[0].Schema.Fields[1] = "C"
+				prebid.Floors.Data.ModelGroups[0].Schema.Fields = append(prebid.Floors.Data.ModelGroups[0].Schema.Fields, "D")
+				prebid.Floors.Data.ModelGroups[0].Schema.Delimiter = ","
+				prebid.Floors.Data.ModelGroups[0].Currency = "CRO"
+				prebid.Floors.Data.ModelGroups[0].ModelWeight = ptrutil.ToPtr(8)
+				prebid.Floors.Data.ModelGroups[0].Values["A"] = 0
+				prebid.Floors.Data.ModelGroups[0].Values["C"] = 7
+				prebid.Floors.Data.ModelGroups = append(prebid.Floors.Data.ModelGroups, PriceFloorModelGroup{})
+				prebid.Floors.FloorMin = 0.3
+				prebid.Floors.FetchStatus = "arf"
+				prebid.Floors.Location.URL = "www.nothere.com"
+				prebid.Floors.Location = nil
+				prebid.Floors.Enabled = nil
+				prebid.Floors.Skipped = ptrutil.ToPtr(true)
+				prebid.Floors.Enforcement.BidAdjustment = ptrutil.ToPtr(true)
+				prebid.Floors.Enforcement.EnforceJS = ptrutil.ToPtr(false)
+				prebid.Floors.Enforcement.FloorDeals = nil
+				prebid.Floors.FloorProvider = ""
+			},
+		},
+		{
+			name: "MultiBidMap",
+			prebid: &ExtRequestPrebid{
+				MultiBidMap: map[string]ExtMultiBid{
+					"A": {
+						Bidder:                 "J",
+						Bidders:                []string{"X", "Y", "Z"},
+						MaxBids:                ptrutil.ToPtr(5),
+						TargetBidderCodePrefix: ">>",
+					},
+					"B": {
+						Bidder:  "J",
+						Bidders: []string{"One", "Two", "Three"},
+					},
+				},
+			},
+			prebidCopy: &ExtRequestPrebid{
+				MultiBidMap: map[string]ExtMultiBid{
+					"A": {
+						Bidder:                 "J",
+						Bidders:                []string{"X", "Y", "Z"},
+						MaxBids:                ptrutil.ToPtr(5),
+						TargetBidderCodePrefix: ">>",
+					},
+					"B": {
+						Bidder:  "J",
+						Bidders: []string{"One", "Two", "Three"},
+					},
+				},
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				mulbidB := prebid.MultiBidMap["B"]
+				mulbidB.TargetBidderCodePrefix = "|"
+				mulbidB.Bidders[1] = "Five"
+				mulbidB.Bidders = append(mulbidB.Bidders, "Six")
+				mulbidB.MaxBids = ptrutil.ToPtr(2)
+				prebid.MultiBidMap["B"] = mulbidB
+				prebid.MultiBidMap["C"] = ExtMultiBid{Bidder: "alpha", MaxBids: ptrutil.ToPtr(3)}
+			},
+		},
+		{
+			name: "MultiBidMap",
+			prebid: &ExtRequestPrebid{
+				AdServerTargeting: []AdServerTarget{
+					{
+						Key:    "A",
+						Source: "Sauce",
+						Value:  "Gold",
+					},
+					{
+						Key:    "B",
+						Source: "Omega",
+						Value:  "Dirt",
+					},
+				},
+			},
+			prebidCopy: &ExtRequestPrebid{
+				AdServerTargeting: []AdServerTarget{
+					{
+						Key:    "A",
+						Source: "Sauce",
+						Value:  "Gold",
+					},
+					{
+						Key:    "B",
+						Source: "Omega",
+						Value:  "Dirt",
+					},
+				},
+			},
+			mutator: func(t *testing.T, prebid *ExtRequestPrebid) {
+				prebid.AdServerTargeting[0].Key = "Five"
+				prebid.AdServerTargeting[1].Value = "Dust"
+				prebid.AdServerTargeting = append(prebid.AdServerTargeting, AdServerTarget{Key: "Val"})
+				prebid.AdServerTargeting = nil
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			clone := test.prebid.Clone()
+			if test.prebid != nil {
+				assert.NotSame(t, test.prebid, clone)
+			}
+			test.mutator(t, test.prebid)
+			assert.Equal(t, test.prebidCopy, clone)
+		})
+	}
+
+}
diff --git a/openrtb_ext/request_wrapper_test.go b/openrtb_ext/request_wrapper_test.go
index 2986840b..e02dd741 100644
--- a/openrtb_ext/request_wrapper_test.go
+++ b/openrtb_ext/request_wrapper_test.go
@@ -5,9 +5,130 @@ import (
 	"testing"
 
 	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/util/ptrutil"
 	"github.com/stretchr/testify/assert"
 )
 
+func TestCloneRequestWrapper(t *testing.T) {
+	testCases := []struct {
+		name        string
+		reqWrap     *RequestWrapper
+		reqWrapCopy *RequestWrapper                             // manual copy of above ext object to verify against
+		mutator     func(t *testing.T, reqWrap *RequestWrapper) // function to modify the Ext object
+	}{
+		{
+			name:        "Nil", // Verify the nil case
+			reqWrap:     nil,
+			reqWrapCopy: nil,
+			mutator:     func(t *testing.T, reqWrap *RequestWrapper) {},
+		},
+		{
+			name: "NoMutate",
+			reqWrap: &RequestWrapper{
+				impWrappers: []*ImpWrapper{
+					{
+						impExt: &ImpExt{prebid: &ExtImpPrebid{Options: &Options{EchoVideoAttrs: true}}, prebidDirty: true, tid: "fun"},
+					},
+					{
+						impExt: &ImpExt{tid: "star"},
+					},
+				},
+				userExt:   &UserExt{consentDirty: true},
+				deviceExt: &DeviceExt{extDirty: true},
+				requestExt: &RequestExt{
+					prebid: &ExtRequestPrebid{Integration: "derivative"},
+				},
+				appExt:    &AppExt{prebidDirty: true},
+				regExt:    &RegExt{usPrivacy: "foo"},
+				siteExt:   &SiteExt{amp: ptrutil.ToPtr[int8](1)},
+				sourceExt: &SourceExt{schainDirty: true},
+			},
+			reqWrapCopy: &RequestWrapper{
+				impWrappers: []*ImpWrapper{
+					{
+						impExt: &ImpExt{prebid: &ExtImpPrebid{Options: &Options{EchoVideoAttrs: true}}, prebidDirty: true, tid: "fun"},
+					},
+					{
+						impExt: &ImpExt{tid: "star"},
+					},
+				},
+				userExt:   &UserExt{consentDirty: true},
+				deviceExt: &DeviceExt{extDirty: true},
+				requestExt: &RequestExt{
+					prebid: &ExtRequestPrebid{Integration: "derivative"},
+				},
+				appExt:    &AppExt{prebidDirty: true},
+				regExt:    &RegExt{usPrivacy: "foo"},
+				siteExt:   &SiteExt{amp: ptrutil.ToPtr[int8](1)},
+				sourceExt: &SourceExt{schainDirty: true},
+			},
+			mutator: func(t *testing.T, reqWrap *RequestWrapper) {},
+		},
+		{
+			name: "General",
+			reqWrap: &RequestWrapper{
+				impWrappers: []*ImpWrapper{
+					{
+						impExt: &ImpExt{prebid: &ExtImpPrebid{Options: &Options{EchoVideoAttrs: true}}, prebidDirty: true, tid: "fun"},
+					},
+					{
+						impExt: &ImpExt{tid: "star"},
+					},
+				},
+				userExt:   &UserExt{consentDirty: true},
+				deviceExt: &DeviceExt{extDirty: true},
+				requestExt: &RequestExt{
+					prebid: &ExtRequestPrebid{Integration: "derivative"},
+				},
+				appExt:    &AppExt{prebidDirty: true},
+				regExt:    &RegExt{usPrivacy: "foo"},
+				siteExt:   &SiteExt{amp: ptrutil.ToPtr[int8](1)},
+				sourceExt: &SourceExt{schainDirty: true},
+			},
+			reqWrapCopy: &RequestWrapper{
+				impWrappers: []*ImpWrapper{
+					{
+						impExt: &ImpExt{prebid: &ExtImpPrebid{Options: &Options{EchoVideoAttrs: true}}, prebidDirty: true, tid: "fun"},
+					},
+					{
+						impExt: &ImpExt{tid: "star"},
+					},
+				},
+				userExt:   &UserExt{consentDirty: true},
+				deviceExt: &DeviceExt{extDirty: true},
+				requestExt: &RequestExt{
+					prebid: &ExtRequestPrebid{Integration: "derivative"},
+				},
+				appExt:    &AppExt{prebidDirty: true},
+				regExt:    &RegExt{usPrivacy: "foo"},
+				siteExt:   &SiteExt{amp: ptrutil.ToPtr[int8](1)},
+				sourceExt: &SourceExt{schainDirty: true},
+			},
+			mutator: func(t *testing.T, reqWrap *RequestWrapper) {
+				reqWrap.impWrappers[1].impExt.prebidDirty = true
+				reqWrap.impWrappers[0] = nil
+				reqWrap.impWrappers = append(reqWrap.impWrappers, &ImpWrapper{impExt: &ImpExt{tid: "star"}})
+				reqWrap.impWrappers = nil
+				reqWrap.userExt = nil
+				reqWrap.deviceExt = nil
+				reqWrap.requestExt = nil
+				reqWrap.appExt = nil
+				reqWrap.regExt = nil
+				reqWrap.siteExt = nil
+				reqWrap.sourceExt = nil
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			clone := test.reqWrap.Clone()
+			test.mutator(t, test.reqWrap)
+			assert.Equal(t, test.reqWrapCopy, clone)
+		})
+	}
+}
+
 func TestUserExt(t *testing.T) {
 	userExt := &UserExt{}
 
@@ -465,6 +586,162 @@ func TestUserExtUnmarshal(t *testing.T) {
 	}
 }
 
+func TestCloneUserExt(t *testing.T) {
+	testCases := []struct {
+		name        string
+		userExt     *UserExt
+		userExtCopy *UserExt                             // manual copy of above ext object to verify against
+		mutator     func(t *testing.T, userExt *UserExt) // function to modify the Ext object
+	}{
+		{
+			name:        "Nil", // Verify the nil case
+			userExt:     nil,
+			userExtCopy: nil,
+			mutator:     func(t *testing.T, user *UserExt) {},
+		},
+		{
+			name: "NoMutate",
+			userExt: &UserExt{
+				ext:          map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				consent:      ptrutil.ToPtr("Myconsent"),
+				consentDirty: true,
+				prebid: &ExtUserPrebid{
+					BuyerUIDs: map[string]string{"A": "X", "B": "Y"},
+				},
+				prebidDirty: true,
+				eids:        &[]openrtb2.EID{},
+			},
+			userExtCopy: &UserExt{
+				ext:          map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				consent:      ptrutil.ToPtr("Myconsent"),
+				consentDirty: true,
+				prebid: &ExtUserPrebid{
+					BuyerUIDs: map[string]string{"A": "X", "B": "Y"},
+				},
+				prebidDirty: true,
+				eids:        &[]openrtb2.EID{},
+			},
+			mutator: func(t *testing.T, user *UserExt) {},
+		},
+		{
+			name: "General",
+			userExt: &UserExt{
+				ext:          map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				consent:      ptrutil.ToPtr("Myconsent"),
+				consentDirty: true,
+				prebid: &ExtUserPrebid{
+					BuyerUIDs: map[string]string{"A": "X", "B": "Y"},
+				},
+				prebidDirty: true,
+				eids:        &[]openrtb2.EID{},
+			},
+			userExtCopy: &UserExt{
+				ext:          map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				consent:      ptrutil.ToPtr("Myconsent"),
+				consentDirty: true,
+				prebid: &ExtUserPrebid{
+					BuyerUIDs: map[string]string{"A": "X", "B": "Y"},
+				},
+				prebidDirty: true,
+				eids:        &[]openrtb2.EID{},
+			},
+			mutator: func(t *testing.T, user *UserExt) {
+				user.ext["A"] = json.RawMessage(`G`)
+				user.ext["C"] = json.RawMessage(`L`)
+				user.extDirty = true
+				user.consent = nil
+				user.consentDirty = false
+				user.prebid.BuyerUIDs["A"] = "C"
+				user.prebid.BuyerUIDs["C"] = "A"
+				user.prebid = nil
+			},
+		},
+		{
+			name: "EIDs",
+			userExt: &UserExt{
+				eids: &[]openrtb2.EID{
+					{
+						Source: "Sauce",
+						UIDs: []openrtb2.UID{
+							{ID: "A", AType: 5, Ext: json.RawMessage(`{}`)},
+							{ID: "B", AType: 1, Ext: json.RawMessage(`{"extra": "stuff"}`)},
+						},
+					},
+					{
+						Source: "Moon",
+						UIDs: []openrtb2.UID{
+							{ID: "G", AType: 3, Ext: json.RawMessage(`{}`)},
+							{ID: "D", AType: 1},
+						},
+					},
+				},
+			},
+			userExtCopy: &UserExt{
+				eids: &[]openrtb2.EID{
+					{
+						Source: "Sauce",
+						UIDs: []openrtb2.UID{
+							{ID: "A", AType: 5, Ext: json.RawMessage(`{}`)},
+							{ID: "B", AType: 1, Ext: json.RawMessage(`{"extra": "stuff"}`)},
+						},
+					},
+					{
+						Source: "Moon",
+						UIDs: []openrtb2.UID{
+							{ID: "G", AType: 3, Ext: json.RawMessage(`{}`)},
+							{ID: "D", AType: 1},
+						},
+					},
+				},
+			},
+			mutator: func(t *testing.T, userExt *UserExt) {
+				eids := *userExt.eids
+				eids[0].UIDs[1].ID = "G2"
+				eids[1].UIDs[0].AType = 0
+				eids[0].UIDs = append(eids[0].UIDs, openrtb2.UID{ID: "Z", AType: 2})
+				eids = append(eids, openrtb2.EID{Source: "Blank"})
+				userExt.eids = nil
+			},
+		},
+		{
+			name: "ConsentedProviders",
+			userExt: &UserExt{
+				consentedProvidersSettingsIn: &ConsentedProvidersSettingsIn{
+					ConsentedProvidersString: "A,B,C",
+				},
+				consentedProvidersSettingsOut: &ConsentedProvidersSettingsOut{
+					ConsentedProvidersList: []int{1, 2, 3, 4},
+				},
+			},
+			userExtCopy: &UserExt{
+				consentedProvidersSettingsIn: &ConsentedProvidersSettingsIn{
+					ConsentedProvidersString: "A,B,C",
+				},
+				consentedProvidersSettingsOut: &ConsentedProvidersSettingsOut{
+					ConsentedProvidersList: []int{1, 2, 3, 4},
+				},
+			},
+			mutator: func(t *testing.T, userExt *UserExt) {
+				userExt.consentedProvidersSettingsIn.ConsentedProvidersString = "B,C,D"
+				userExt.consentedProvidersSettingsIn = &ConsentedProvidersSettingsIn{
+					ConsentedProvidersString: "G,H,I",
+				}
+				userExt.consentedProvidersSettingsOut.ConsentedProvidersList[1] = 5
+				userExt.consentedProvidersSettingsOut.ConsentedProvidersList = append(userExt.consentedProvidersSettingsOut.ConsentedProvidersList, 7)
+				userExt.consentedProvidersSettingsOut = nil
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			clone := test.userExt.Clone()
+			test.mutator(t, test.userExt)
+			assert.Equal(t, test.userExtCopy, clone)
+		})
+	}
+}
+
 func TestRebuildDeviceExt(t *testing.T) {
 	prebidContent1 := ExtDevicePrebid{Interstitial: &ExtDeviceInt{MinWidthPerc: 1}}
 	prebidContent2 := ExtDevicePrebid{Interstitial: &ExtDeviceInt{MinWidthPerc: 2}}
@@ -611,6 +888,172 @@ func TestRebuildRequestExt(t *testing.T) {
 	}
 }
 
+func TestCloneRequestExt(t *testing.T) {
+	testCases := []struct {
+		name       string
+		reqExt     *RequestExt
+		reqExtCopy *RequestExt                            // manual copy of above ext object to verify against
+		mutator    func(t *testing.T, reqExt *RequestExt) // function to modify the Ext object
+	}{
+		{
+			name:       "Nil", // Verify the nil case
+			reqExt:     nil,
+			reqExtCopy: nil,
+			mutator:    func(t *testing.T, reqExt *RequestExt) {},
+		},
+		{
+			name: "NoMutate", // Verify the nil case
+			reqExt: &RequestExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)},
+				extDirty: true,
+				prebid: &ExtRequestPrebid{
+					BidderParams: json.RawMessage(`{}`),
+				},
+			},
+			reqExtCopy: &RequestExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)},
+				extDirty: true,
+				prebid: &ExtRequestPrebid{
+					BidderParams: json.RawMessage(`{}`),
+				},
+			},
+			mutator: func(t *testing.T, reqExt *RequestExt) {},
+		},
+		{
+			name: "General", // Verify the nil case
+			reqExt: &RequestExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)},
+				extDirty: true,
+				prebid: &ExtRequestPrebid{
+					BidderParams: json.RawMessage(`{}`),
+				},
+			},
+			reqExtCopy: &RequestExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)},
+				extDirty: true,
+				prebid: &ExtRequestPrebid{
+					BidderParams: json.RawMessage(`{}`),
+				},
+			},
+			mutator: func(t *testing.T, reqExt *RequestExt) {
+				reqExt.ext["A"] = json.RawMessage(`"string"`)
+				reqExt.ext["C"] = json.RawMessage(`{}`)
+				reqExt.extDirty = false
+				reqExt.prebid.Channel = &ExtRequestPrebidChannel{Name: "Bob"}
+				reqExt.prebid.BidderParams = nil
+				reqExt.prebid = nil
+			},
+		},
+		{
+			name: "SChain", // Verify the nil case
+			reqExt: &RequestExt{
+				schain: &openrtb2.SupplyChain{
+					Complete: 1,
+					Ver:      "1.1",
+					Nodes: []openrtb2.SupplyChainNode{
+						{ASI: "Is a", RID: "off", HP: ptrutil.ToPtr[int8](1)},
+						{ASI: "other", RID: "drift", HP: ptrutil.ToPtr[int8](0)},
+					},
+				},
+			},
+			reqExtCopy: &RequestExt{
+				schain: &openrtb2.SupplyChain{
+					Complete: 1,
+					Ver:      "1.1",
+					Nodes: []openrtb2.SupplyChainNode{
+						{ASI: "Is a", RID: "off", HP: ptrutil.ToPtr[int8](1)},
+						{ASI: "other", RID: "drift", HP: ptrutil.ToPtr[int8](0)},
+					},
+				},
+			},
+			mutator: func(t *testing.T, reqExt *RequestExt) {
+				reqExt.schain.Complete = 0
+				reqExt.schain.Ver = "1.2"
+				reqExt.schain.Nodes[0].ASI = "some"
+				reqExt.schain.Nodes[1].HP = nil
+				reqExt.schain.Nodes = append(reqExt.schain.Nodes, openrtb2.SupplyChainNode{ASI: "added"})
+				reqExt.schain = nil
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			clone := test.reqExt.Clone()
+			test.mutator(t, test.reqExt)
+			assert.Equal(t, test.reqExtCopy, clone)
+		})
+	}
+
+}
+
+func TestCloneDeviceExt(t *testing.T) {
+	testCases := []struct {
+		name       string
+		devExt     *DeviceExt
+		devExtCopy *DeviceExt                            // manual copy of above ext object to verify against
+		mutator    func(t *testing.T, devExt *DeviceExt) // function to modify the Ext object
+	}{
+		{
+			name:       "Nil", // Verify the nil case
+			devExt:     nil,
+			devExtCopy: nil,
+			mutator:    func(t *testing.T, devExt *DeviceExt) {},
+		},
+		{
+			name: "NoMutate",
+			devExt: &DeviceExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)},
+				extDirty: true,
+				prebid: &ExtDevicePrebid{
+					Interstitial: &ExtDeviceInt{MinWidthPerc: 65.0, MinHeightPerc: 75.0},
+				},
+			},
+			devExtCopy: &DeviceExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)},
+				extDirty: true,
+				prebid: &ExtDevicePrebid{
+					Interstitial: &ExtDeviceInt{MinWidthPerc: 65.0, MinHeightPerc: 75.0},
+				},
+			},
+			mutator: func(t *testing.T, devExt *DeviceExt) {},
+		},
+		{
+			name: "General",
+			devExt: &DeviceExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)},
+				extDirty: true,
+				prebid: &ExtDevicePrebid{
+					Interstitial: &ExtDeviceInt{MinWidthPerc: 65.0, MinHeightPerc: 75.0},
+				},
+			},
+			devExtCopy: &DeviceExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)},
+				extDirty: true,
+				prebid: &ExtDevicePrebid{
+					Interstitial: &ExtDeviceInt{MinWidthPerc: 65, MinHeightPerc: 75},
+				},
+			},
+			mutator: func(t *testing.T, devExt *DeviceExt) {
+				devExt.ext["A"] = json.RawMessage(`"string"`)
+				devExt.ext["C"] = json.RawMessage(`{}`)
+				devExt.extDirty = false
+				devExt.prebid.Interstitial.MinHeightPerc = 55
+				devExt.prebid.Interstitial = &ExtDeviceInt{MinWidthPerc: 80}
+				devExt.prebid = nil
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			clone := test.devExt.Clone()
+			test.mutator(t, test.devExt)
+			assert.Equal(t, test.devExtCopy, clone)
+		})
+	}
+}
+
 func TestRebuildAppExt(t *testing.T) {
 	prebidContent1 := ExtAppPrebid{Source: "1"}
 	prebidContent2 := ExtAppPrebid{Source: "2"}
@@ -693,6 +1136,143 @@ func TestRebuildAppExt(t *testing.T) {
 	}
 }
 
+func TestCloneAppExt(t *testing.T) {
+	testCases := []struct {
+		name       string
+		appExt     *AppExt
+		appExtCopy *AppExt                            // manual copy of above ext object to verify against
+		mutator    func(t *testing.T, appExt *AppExt) // function to modify the Ext object
+	}{
+		{
+			name:       "Nil", // Verify the nil case
+			appExt:     nil,
+			appExtCopy: nil,
+			mutator:    func(t *testing.T, appExt *AppExt) {},
+		},
+		{
+			name: "NoMutate",
+			appExt: &AppExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				prebid: &ExtAppPrebid{
+					Source:  "Sauce",
+					Version: "2.2",
+				},
+			},
+			appExtCopy: &AppExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				prebid: &ExtAppPrebid{
+					Source:  "Sauce",
+					Version: "2.2",
+				},
+			},
+			mutator: func(t *testing.T, appExt *AppExt) {},
+		},
+		{
+			name: "General",
+			appExt: &AppExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				prebid: &ExtAppPrebid{
+					Source:  "Sauce",
+					Version: "2.2",
+				},
+			},
+			appExtCopy: &AppExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				prebid: &ExtAppPrebid{
+					Source:  "Sauce",
+					Version: "2.2",
+				},
+			},
+			mutator: func(t *testing.T, appExt *AppExt) {
+				appExt.ext["A"] = json.RawMessage(`"string"`)
+				appExt.ext["C"] = json.RawMessage(`{}`)
+				appExt.extDirty = false
+				appExt.prebid.Source = "foobar"
+				appExt.prebid = nil
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			clone := test.appExt.Clone()
+			test.mutator(t, test.appExt)
+			assert.Equal(t, test.appExtCopy, clone)
+		})
+	}
+}
+
+func TestCloneRegExt(t *testing.T) {
+	testCases := []struct {
+		name       string
+		regExt     *RegExt
+		regExtCopy *RegExt                            // manual copy of above ext object to verify against
+		mutator    func(t *testing.T, regExt *RegExt) // function to modify the Ext object
+	}{
+		{
+			name:       "Nil", // Verify the nil case
+			regExt:     nil,
+			regExtCopy: nil,
+			mutator:    func(t *testing.T, appExt *RegExt) {},
+		},
+		{
+			name: "NoMutate",
+			regExt: &RegExt{
+				ext:            map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty:       true,
+				gdpr:           ptrutil.ToPtr[int8](1),
+				usPrivacy:      "priv",
+				usPrivacyDirty: true,
+			},
+			regExtCopy: &RegExt{
+				ext:            map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty:       true,
+				gdpr:           ptrutil.ToPtr[int8](1),
+				usPrivacy:      "priv",
+				usPrivacyDirty: true,
+			},
+			mutator: func(t *testing.T, appExt *RegExt) {},
+		},
+		{
+			name: "General",
+			regExt: &RegExt{
+				ext:            map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty:       true,
+				gdpr:           ptrutil.ToPtr[int8](1),
+				usPrivacy:      "priv",
+				usPrivacyDirty: true,
+			},
+			regExtCopy: &RegExt{
+				ext:            map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty:       true,
+				gdpr:           ptrutil.ToPtr[int8](1),
+				usPrivacy:      "priv",
+				usPrivacyDirty: true,
+			},
+			mutator: func(t *testing.T, appExt *RegExt) {
+				appExt.ext["A"] = json.RawMessage(`"string"`)
+				appExt.ext["C"] = json.RawMessage(`{}`)
+				appExt.extDirty = false
+				appExt.gdpr = nil
+				appExt.gdprDirty = true
+				appExt.usPrivacy = "Other"
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			clone := test.regExt.Clone()
+			test.mutator(t, test.regExt)
+			assert.Equal(t, test.regExtCopy, clone)
+		})
+	}
+}
+
 func TestRebuildSiteExt(t *testing.T) {
 	int1 := int8(1)
 	int2 := int8(2)
@@ -775,6 +1355,64 @@ func TestRebuildSiteExt(t *testing.T) {
 	}
 }
 
+func TestCloneSiteExt(t *testing.T) {
+	testCases := []struct {
+		name        string
+		siteExt     *SiteExt
+		siteExtCopy *SiteExt                             // manual copy of above ext object to verify against
+		mutator     func(t *testing.T, siteExt *SiteExt) // function to modify the Ext object
+	}{
+		{
+			name:        "Nil", // Verify the nil case
+			siteExt:     nil,
+			siteExtCopy: nil,
+			mutator:     func(t *testing.T, siteExt *SiteExt) {},
+		},
+		{
+			name: "NoMutate",
+			siteExt: &SiteExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				amp:      ptrutil.ToPtr[int8](1),
+			},
+			siteExtCopy: &SiteExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				amp:      ptrutil.ToPtr[int8](1),
+			},
+			mutator: func(t *testing.T, siteExt *SiteExt) {},
+		},
+		{
+			name: "General",
+			siteExt: &SiteExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				amp:      ptrutil.ToPtr[int8](1),
+			},
+			siteExtCopy: &SiteExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				amp:      ptrutil.ToPtr[int8](1),
+			},
+			mutator: func(t *testing.T, siteExt *SiteExt) {
+				siteExt.ext["A"] = json.RawMessage(`"string"`)
+				siteExt.ext["C"] = json.RawMessage(`{}`)
+				siteExt.extDirty = false
+				siteExt.amp = nil
+				siteExt.ampDirty = true
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			clone := test.siteExt.Clone()
+			test.mutator(t, test.siteExt)
+			assert.Equal(t, test.siteExtCopy, clone)
+		})
+	}
+}
+
 func TestRebuildSourceExt(t *testing.T) {
 	schainContent1 := openrtb2.SupplyChain{Ver: "1"}
 	schainContent2 := openrtb2.SupplyChain{Ver: "2"}
@@ -857,6 +1495,69 @@ func TestRebuildSourceExt(t *testing.T) {
 	}
 }
 
+func TestCloneSourceExt(t *testing.T) {
+	testCases := []struct {
+		name          string
+		sourceExt     *SourceExt
+		sourceExtCopy *SourceExt                               // manual copy of above ext object to verify against
+		mutator       func(t *testing.T, sourceExt *SourceExt) // function to modify the Ext object
+	}{
+		{
+			name:          "Nil", // Verify the nil case
+			sourceExt:     nil,
+			sourceExtCopy: nil,
+			mutator:       func(t *testing.T, sourceExt *SourceExt) {},
+		},
+		{
+			name: "NoMutate",
+			sourceExt: &SourceExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				schain: &openrtb2.SupplyChain{
+					Complete: 1,
+					Ver:      "1.1",
+					Nodes: []openrtb2.SupplyChainNode{
+						{ASI: "Is a", RID: "off", HP: ptrutil.ToPtr[int8](1)},
+						{ASI: "other", RID: "drift", HP: ptrutil.ToPtr[int8](0)},
+					},
+				},
+			},
+			sourceExtCopy: &SourceExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				schain: &openrtb2.SupplyChain{
+					Complete: 1,
+					Ver:      "1.1",
+					Nodes: []openrtb2.SupplyChainNode{
+						{ASI: "Is a", RID: "off", HP: ptrutil.ToPtr[int8](1)},
+						{ASI: "other", RID: "drift", HP: ptrutil.ToPtr[int8](0)},
+					},
+				},
+			},
+			mutator: func(t *testing.T, sourceExt *SourceExt) {
+				sourceExt.ext["A"] = json.RawMessage(`"string"`)
+				sourceExt.ext["C"] = json.RawMessage(`{}`)
+				sourceExt.extDirty = false
+				sourceExt.schain.Complete = 0
+				sourceExt.schain.Ver = "1.2"
+				sourceExt.schain.Nodes[0].ASI = "some"
+				sourceExt.schain.Nodes[1].HP = nil
+				sourceExt.schain.Nodes = append(sourceExt.schain.Nodes, openrtb2.SupplyChainNode{ASI: "added"})
+				sourceExt.schain = nil
+
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			clone := test.sourceExt.Clone()
+			test.mutator(t, test.sourceExt)
+			assert.Equal(t, test.sourceExtCopy, clone)
+		})
+	}
+}
+
 func TestImpWrapperRebuildImp(t *testing.T) {
 	var (
 		isRewardedInventoryOne int8 = 1
@@ -1026,3 +1727,184 @@ func TestImpExtTid(t *testing.T) {
 	assert.Equal(t, "tid", impExt.GetTid(), "ImpExt tid is incorrect")
 	assert.Equal(t, true, impExt.Dirty(), "New impext should be dirty.")
 }
+
+func TestCloneImpWrapper(t *testing.T) {
+	testCases := []struct {
+		name           string
+		impWrapper     *ImpWrapper
+		impWrapperCopy *ImpWrapper                                // manual copy of above ext object to verify against
+		mutator        func(t *testing.T, impWrapper *ImpWrapper) // function to modify the Ext object
+	}{
+		{
+			name:           "Nil", // Verify the nil case
+			impWrapper:     nil,
+			impWrapperCopy: nil,
+			mutator:        func(t *testing.T, impWrapper *ImpWrapper) {},
+		},
+		{
+			name: "NoMutate",
+			impWrapper: &ImpWrapper{
+				impExt: &ImpExt{
+					tid: "occupied",
+				},
+			},
+			impWrapperCopy: &ImpWrapper{
+				impExt: &ImpExt{
+					tid: "occupied",
+				},
+			},
+			mutator: func(t *testing.T, impWrapper *ImpWrapper) {},
+		},
+		{
+			name: "General",
+			impWrapper: &ImpWrapper{
+				impExt: &ImpExt{
+					tid: "occupied",
+				},
+			},
+			impWrapperCopy: &ImpWrapper{
+				impExt: &ImpExt{
+					tid: "occupied",
+				},
+			},
+			mutator: func(t *testing.T, impWrapper *ImpWrapper) {
+				impWrapper.impExt.extDirty = true
+				impWrapper.impExt.tid = "Something"
+				impWrapper.impExt = nil
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			clone := test.impWrapper.Clone()
+			test.mutator(t, test.impWrapper)
+			assert.Equal(t, test.impWrapperCopy, clone)
+		})
+	}
+}
+
+func TestCloneImpExt(t *testing.T) {
+	testCases := []struct {
+		name       string
+		impExt     *ImpExt
+		impExtCopy *ImpExt                            // manual copy of above ext object to verify against
+		mutator    func(t *testing.T, impExt *ImpExt) // function to modify the Ext object
+	}{
+		{
+			name:       "Nil", // Verify the nil case
+			impExt:     nil,
+			impExtCopy: nil,
+			mutator:    func(t *testing.T, impExt *ImpExt) {},
+		},
+		{
+			name: "NoMutate",
+			impExt: &ImpExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				tid:      "TID",
+			},
+			impExtCopy: &ImpExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				tid:      "TID",
+			},
+			mutator: func(t *testing.T, impExt *ImpExt) {},
+		},
+		{
+			name: "General",
+			impExt: &ImpExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				tid:      "TID",
+			},
+			impExtCopy: &ImpExt{
+				ext:      map[string]json.RawMessage{"A": json.RawMessage(`X`), "B": json.RawMessage(`Y`)},
+				extDirty: true,
+				tid:      "TID",
+			},
+			mutator: func(t *testing.T, impExt *ImpExt) {
+				impExt.ext["A"] = json.RawMessage(`"string"`)
+				impExt.ext["C"] = json.RawMessage(`{}`)
+				impExt.extDirty = false
+				impExt.tid = "other"
+				impExt.tidDirty = true
+			},
+		},
+		{
+			name: "Prebid",
+			impExt: &ImpExt{
+				prebid: &ExtImpPrebid{
+					StoredRequest:         &ExtStoredRequest{ID: "abc123"},
+					StoredAuctionResponse: &ExtStoredAuctionResponse{ID: "123abc"},
+					StoredBidResponse: []ExtStoredBidResponse{
+						{ID: "foo", Bidder: "bar", ReplaceImpId: ptrutil.ToPtr(true)},
+						{ID: "def", Bidder: "xyz", ReplaceImpId: ptrutil.ToPtr(false)},
+					},
+					IsRewardedInventory: ptrutil.ToPtr[int8](1),
+					Bidder: map[string]json.RawMessage{
+						"abc": json.RawMessage(`{}`),
+						"def": json.RawMessage(`{"alpha":"beta"}`),
+					},
+					Options:     &Options{EchoVideoAttrs: true},
+					Passthrough: json.RawMessage(`{"foo":"bar"}`),
+					Floors: &ExtImpPrebidFloors{
+						FloorRule:      "Rule 16",
+						FloorRuleValue: 16.17,
+						FloorValue:     6.7,
+					},
+				},
+			},
+			impExtCopy: &ImpExt{
+				prebid: &ExtImpPrebid{
+					StoredRequest:         &ExtStoredRequest{ID: "abc123"},
+					StoredAuctionResponse: &ExtStoredAuctionResponse{ID: "123abc"},
+					StoredBidResponse: []ExtStoredBidResponse{
+						{ID: "foo", Bidder: "bar", ReplaceImpId: ptrutil.ToPtr(true)},
+						{ID: "def", Bidder: "xyz", ReplaceImpId: ptrutil.ToPtr(false)},
+					},
+					IsRewardedInventory: ptrutil.ToPtr[int8](1),
+					Bidder: map[string]json.RawMessage{
+						"abc": json.RawMessage(`{}`),
+						"def": json.RawMessage(`{"alpha":"beta"}`),
+					},
+					Options:     &Options{EchoVideoAttrs: true},
+					Passthrough: json.RawMessage(`{"foo":"bar"}`),
+					Floors: &ExtImpPrebidFloors{
+						FloorRule:      "Rule 16",
+						FloorRuleValue: 16.17,
+						FloorValue:     6.7,
+					},
+				},
+			},
+			mutator: func(t *testing.T, impExt *ImpExt) {
+				impExt.prebid.StoredRequest.ID = "seventy"
+				impExt.prebid.StoredRequest = nil
+				impExt.prebid.StoredAuctionResponse.ID = "xyz"
+				impExt.prebid.StoredAuctionResponse = nil
+				impExt.prebid.StoredBidResponse[0].ID = "alpha"
+				impExt.prebid.StoredBidResponse[1].ReplaceImpId = nil
+				impExt.prebid.StoredBidResponse[0] = ExtStoredBidResponse{ID: "o", Bidder: "k", ReplaceImpId: ptrutil.ToPtr(false)}
+				impExt.prebid.StoredBidResponse = append(impExt.prebid.StoredBidResponse, ExtStoredBidResponse{ID: "jay", Bidder: "walk"})
+				impExt.prebid.IsRewardedInventory = nil
+				impExt.prebid.Bidder["def"] = json.RawMessage(``)
+				delete(impExt.prebid.Bidder, "abc")
+				impExt.prebid.Bidder["xyz"] = json.RawMessage(`{"jar":5}`)
+				impExt.prebid.Options.EchoVideoAttrs = false
+				impExt.prebid.Options = nil
+				impExt.prebid.Passthrough = json.RawMessage(`{}`)
+				impExt.prebid.Floors.FloorRule = "Friday"
+				impExt.prebid.Floors.FloorMinCur = "EUR"
+				impExt.prebid.Floors = nil
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			clone := test.impExt.Clone()
+			test.mutator(t, test.impExt)
+			assert.Equal(t, test.impExtCopy, clone)
+		})
+	}
+}
diff --git a/openrtb_ext/supplyChain_test.go b/openrtb_ext/supplyChain_test.go
new file mode 100644
index 00000000..12fd5c33
--- /dev/null
+++ b/openrtb_ext/supplyChain_test.go
@@ -0,0 +1,83 @@
+package openrtb_ext
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/util/ptrutil"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCloneSupplyChain(t *testing.T) {
+	testCases := []struct {
+		name       string
+		schain     *openrtb2.SupplyChain
+		schainCopy *openrtb2.SupplyChain                            // manual copy of above prebid object to verify against
+		mutator    func(t *testing.T, schain *openrtb2.SupplyChain) // function to modify the prebid object
+	}{
+		{
+			name:       "Nil", // Verify the nil case
+			schain:     nil,
+			schainCopy: nil,
+			mutator:    func(t *testing.T, schain *openrtb2.SupplyChain) {},
+		},
+		{
+			name: "General",
+			schain: &openrtb2.SupplyChain{
+				Complete: 2,
+				Nodes: []openrtb2.SupplyChainNode{
+					{
+						SID:  "alpha",
+						Name: "Johnny",
+						HP:   ptrutil.ToPtr[int8](5),
+						Ext:  json.RawMessage(`{}`),
+					},
+					{
+						ASI:  "Oh my",
+						Name: "Johnny",
+						HP:   ptrutil.ToPtr[int8](5),
+						Ext:  json.RawMessage(`{"samson"}`),
+					},
+				},
+				Ver: "v2.5",
+				Ext: json.RawMessage(`{"foo": "bar"}`),
+			},
+			schainCopy: &openrtb2.SupplyChain{
+				Complete: 2,
+				Nodes: []openrtb2.SupplyChainNode{
+					{
+						SID:  "alpha",
+						Name: "Johnny",
+						HP:   ptrutil.ToPtr[int8](5),
+						Ext:  json.RawMessage(`{}`),
+					},
+					{
+						ASI:  "Oh my",
+						Name: "Johnny",
+						HP:   ptrutil.ToPtr[int8](5),
+						Ext:  json.RawMessage(`{"samson"}`),
+					},
+				},
+				Ver: "v2.5",
+				Ext: json.RawMessage(`{"foo": "bar"}`),
+			},
+			mutator: func(t *testing.T, schain *openrtb2.SupplyChain) {
+				schain.Nodes[0].SID = "beta"
+				schain.Nodes[1].HP = nil
+				schain.Nodes[0].Ext = nil
+				schain.Nodes = append(schain.Nodes, openrtb2.SupplyChainNode{SID: "Gamma"})
+				schain.Complete = 0
+				schain.Ext = json.RawMessage(`{}`)
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			clone := cloneSupplyChain(test.schain)
+			test.mutator(t, test.schain)
+			assert.Equal(t, test.schainCopy, clone)
+		})
+	}
+}
diff --git a/util/maputil/maputil_test.go b/util/maputil/maputil_test.go
index 114c2332..e7c06722 100644
--- a/util/maputil/maputil_test.go
+++ b/util/maputil/maputil_test.go
@@ -245,3 +245,29 @@ func TestHasElement(t *testing.T) {
 		assert.Equal(t, test.expected, result, test.description)
 	}
 }
+
+func TestCloneMap(t *testing.T) {
+	// Test we handle nils properly
+	t.Run("NilMap", func(t *testing.T) {
+		var testMap, copyMap map[string]string = nil, nil // copyMap is a manual copy of testMap
+		clone := Clone(testMap)
+		testMap = map[string]string{"foo": "bar"}
+		assert.Equal(t, copyMap, clone)
+	})
+	// Test a simple string map
+	t.Run("StringMap", func(t *testing.T) {
+		var testMap, copyMap map[string]string = map[string]string{"foo": "bar", "first": "one"}, map[string]string{"foo": "bar", "first": "one"}
+		clone := Clone(testMap)
+		testMap["foo"] = "baz"
+		testMap["bozo"] = "the clown"
+		assert.Equal(t, copyMap, clone)
+	})
+	// Test a simple map[string]int
+	t.Run("StringInt", func(t *testing.T) {
+		var testMap, copyMap map[string]int = map[string]int{"foo": 1, "first": 2}, map[string]int{"foo": 1, "first": 2}
+		clone := Clone(testMap)
+		testMap["foo"] = 7
+		testMap["bozo"] = 13
+		assert.Equal(t, copyMap, clone)
+	})
+}