Repository: prebid/prebid-server
Pull request #1364 (2020-06-22 03:49:00)

Fall back to constant rates when the currency rates endpoint i…
…s not accessible and rates are stale


Patch:
diff --git a/adapters/adman/adman.go b/adapters/adman/adman.go
new file mode 100644
index 00000000..aa8d0dc6
--- /dev/null
+++ b/adapters/adman/adman.go
@@ -0,0 +1,140 @@
+package adman
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+
+	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+// AdmanAdapter struct
+type AdmanAdapter struct {
+	URI string
+}
+
+// NewAdmanBidder Initializes the Bidder
+func NewAdmanBidder(endpoint string) *AdmanAdapter {
+	return &AdmanAdapter{
+		URI: endpoint,
+	}
+}
+
+type admanParams struct {
+	TagID string `json:"TagID"`
+}
+
+// MakeRequests create bid request for adman demand
+func (a *AdmanAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	var errs []error
+	var admanExt openrtb_ext.ExtImpAdman
+	var err error
+
+	var adapterRequests []*adapters.RequestData
+
+	reqCopy := *request
+	for _, imp := range request.Imp {
+		reqCopy.Imp = []openrtb.Imp{imp}
+
+		var bidderExt adapters.ExtImpBidder
+		if err = json.Unmarshal(reqCopy.Imp[0].Ext, &bidderExt); err != nil {
+			errs = append(errs, err)
+			continue
+		}
+
+		if err = json.Unmarshal(bidderExt.Bidder, &admanExt); err != nil {
+			errs = append(errs, err)
+			continue
+		}
+
+		reqCopy.Imp[0].TagID = admanExt.TagID
+
+		adapterReq, errors := a.makeRequest(&reqCopy)
+		if adapterReq != nil {
+			adapterRequests = append(adapterRequests, adapterReq)
+		}
+		errs = append(errs, errors...)
+	}
+	return adapterRequests, errs
+}
+
+func (a *AdmanAdapter) makeRequest(request *openrtb.BidRequest) (*adapters.RequestData, []error) {
+
+	var errs []error
+
+	reqJSON, err := json.Marshal(request)
+
+	if err != nil {
+		errs = append(errs, err)
+		return nil, errs
+	}
+
+	headers := http.Header{}
+	headers.Add("Content-Type", "application/json;charset=utf-8")
+	headers.Add("Accept", "application/json")
+	return &adapters.RequestData{
+		Method:  "POST",
+		Uri:     a.URI,
+		Body:    reqJSON,
+		Headers: headers,
+	}, errs
+}
+
+// MakeBids makes the bids
+func (a *AdmanAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	var errs []error
+
+	if response.StatusCode == http.StatusNoContent {
+		return nil, nil
+	}
+
+	if response.StatusCode == http.StatusNotFound {
+		return nil, []error{&errortypes.BadServerResponse{
+			Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode),
+		}}
+	}
+
+	var bidResp openrtb.BidResponse
+
+	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
+		return nil, []error{err}
+	}
+
+	bidResponse := adapters.NewBidderResponseWithBidsCapacity(1)
+
+	for _, sb := range bidResp.SeatBid {
+		for i := range sb.Bid {
+			bidType, err := getMediaTypeForImp(sb.Bid[i].ImpID, internalRequest.Imp)
+			if err != nil {
+				errs = append(errs, err)
+			} else {
+				b := &adapters.TypedBid{
+					Bid:     &sb.Bid[i],
+					BidType: bidType,
+				}
+				bidResponse.Bids = append(bidResponse.Bids, b)
+			}
+		}
+	}
+	return bidResponse, errs
+}
+
+func getMediaTypeForImp(impID string, imps []openrtb.Imp) (openrtb_ext.BidType, error) {
+	mediaType := openrtb_ext.BidTypeBanner
+	for _, imp := range imps {
+		if imp.ID == impID {
+			if imp.Banner == nil && imp.Video != nil {
+				mediaType = openrtb_ext.BidTypeVideo
+			}
+			return mediaType, nil
+		}
+	}
+
+	// This shouldnt happen. Lets handle it just incase by returning an error.
+	return "", &errortypes.BadInput{
+		Message: fmt.Sprintf("Failed to find impression \"%s\" ", impID),
+	}
+}
diff --git a/adapters/adman/usersync.go b/adapters/adman/usersync.go
new file mode 100644
index 00000000..aae6afcd
--- /dev/null
+++ b/adapters/adman/usersync.go
@@ -0,0 +1,13 @@
+package adman
+
+import (
+	"text/template"
+
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/usersync"
+)
+
+// NewAdmanSyncer returns adman syncer
+func NewAdmanSyncer(temp *template.Template) usersync.Usersyncer {
+	return adapters.NewSyncer("adman", 149, temp, adapters.SyncTypeRedirect)
+}
diff --git a/adapters/audienceNetwork/facebook.go b/adapters/audienceNetwork/facebook.go
index 9edb9a7d..f4091e4e 100644
--- a/adapters/audienceNetwork/facebook.go
+++ b/adapters/audienceNetwork/facebook.go
@@ -10,16 +10,17 @@ import (
 	"net/http"
 	"strings"
 
-	"github.com/buger/jsonparser"
-	"github.com/golang/glog"
-	"github.com/mxmCherry/openrtb"
 	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/prebid/prebid-server/util/maputil"
+
+	"github.com/buger/jsonparser"
+	"github.com/golang/glog"
+	"github.com/mxmCherry/openrtb"
 )
 
 type FacebookAdapter struct {
-	http         *adapters.HTTPAdapter
 	URI          string
 	nonSecureUri string
 	platformID   string
@@ -35,15 +36,6 @@ var supportedBannerHeights = map[uint64]bool{
 	250: true,
 }
 
-// used for cookies and such
-func (a *FacebookAdapter) Name() string {
-	return "audienceNetwork"
-}
-
-func (a *FacebookAdapter) SkipNoCookies() bool {
-	return false
-}
-
 type facebookReqExt struct {
 	PlatformID string `json:"platformid"`
 	AuthID     string `json:"authentication_id"`
@@ -178,8 +170,10 @@ func (this *FacebookAdapter) modifyImp(out *openrtb.Imp) error {
 		}
 	}
 
-	switch impType {
-	case openrtb_ext.BidTypeBanner:
+	if impType == openrtb_ext.BidTypeBanner {
+		bannerCopy := *out.Banner
+		out.Banner = &bannerCopy
+
 		if out.Instl == 1 {
 			out.Banner.W = openrtb.Uint64Ptr(0)
 			out.Banner.H = openrtb.Uint64Ptr(0)
@@ -212,7 +206,6 @@ func (this *FacebookAdapter) modifyImp(out *openrtb.Imp) error {
 		/* This will get overwritten post-serialization */
 		out.Banner.W = openrtb.Uint64Ptr(0)
 		out.Banner.Format = nil
-		break
 	}
 
 	return nil
@@ -239,102 +232,106 @@ func (this *FacebookAdapter) extractPlacementAndPublisher(out *openrtb.Imp) (str
 		}
 	}
 
-	placementId := fbExt.PlacementId
-	publisherId := fbExt.PublisherId
+	placementID := fbExt.PlacementId
+	publisherID := fbExt.PublisherId
 
 	// Support the legacy path with the caller was expected to pass in just placementId
 	// which was an underscore concantenated string with the publisherId and placementId.
 	// The new path for callers is to pass in the placementId and publisherId independently
 	// and the below code will prefix the placementId that we pass to FAN with the publsiherId
 	// so that we can abstract the implementation details from the caller
-	toks := strings.Split(placementId, "_")
+	toks := strings.Split(placementID, "_")
 	if len(toks) == 1 {
-		if publisherId == "" {
+		if publisherID == "" {
 			return "", "", &errortypes.BadInput{
 				Message: "Missing publisherId param",
 			}
 		}
 
-		return placementId, publisherId, nil
+		return placementID, publisherID, nil
 	} else if len(toks) == 2 {
-		publisherId = toks[0]
-		placementId = toks[1]
+		publisherID = toks[0]
+		placementID = toks[1]
 	} else {
 		return "", "", &errortypes.BadInput{
-			Message: fmt.Sprintf("Invalid placementId param '%s' and publisherId param '%s'", placementId, publisherId),
+			Message: fmt.Sprintf("Invalid placementId param '%s' and publisherId param '%s'", placementID, publisherID),
 		}
 	}
 
-	return placementId, publisherId, nil
+	return placementID, publisherID, nil
 }
 
 // XXX: This entire function is just a hack to get around mxmCherry 11.0.0 limitations, without
 // having to fork the library and maintain our own branch
-func modifyImpCustom(json []byte, imp *openrtb.Imp) ([]byte, error) {
+func modifyImpCustom(jsonData []byte, imp *openrtb.Imp) ([]byte, error) {
 	impType, ok := resolveImpType(imp)
 	if ok == false {
 		panic("processing an invalid impression")
 	}
 
-	var err error
+	var jsonMap map[string]interface{}
+	err := json.Unmarshal(jsonData, &jsonMap)
+	if err != nil {
+		return jsonData, err
+	}
+
+	var impMap map[string]interface{}
+	if impSlice, ok := maputil.ReadEmbeddedSlice(jsonMap, "imp"); !ok {
+		return jsonData, errors.New("unable to find imp in json data")
+	} else if len(impSlice) == 0 {
+		return jsonData, errors.New("unable to find imp[0] in json data")
+	} else if impMap, ok = impSlice[0].(map[string]interface{}); !ok {
+		return jsonData, errors.New("unexpected type for imp[0] found in json data")
+	}
 
 	switch impType {
 	case openrtb_ext.BidTypeBanner:
-		// The current version of mxmCherry (11.0.0) repesents banner.w as unsigned
-		// integers, so setting a value of -1 is not possible which is why we have to do it
+		// The current version of mxmCherry (11.0.0) represents banner.w as an unsigned
+		// integer, so setting a value of -1 is not possible which is why we have to do it
 		// post-serialization
-
-		// The above does not apply to interstitial impressions
-		if imp.Instl == 1 {
-			break
-		}
-
-		json, err = jsonparser.Set(json, []byte("-1"), "imp", "[0]", "banner", "w")
-		if err != nil {
-			return json, err
+		isInterstitial := imp.Instl == 1
+		if !isInterstitial {
+			if bannerMap, ok := maputil.ReadEmbeddedMap(impMap, "banner"); ok {
+				bannerMap["w"] = json.RawMessage("-1")
+			} else {
+				return jsonData, errors.New("unable to find imp[0].banner in json data")
+			}
 		}
 
-		break
-
 	case openrtb_ext.BidTypeVideo:
 		// mxmCherry omits video.w/h if set to zero, so we need to force set those
 		// fields to zero post-serialization for the time being
-		json, err = jsonparser.Set(json, []byte("0"), "imp", "[0]", "video", "w")
-		if err != nil {
-			return json, err
+		if videoMap, ok := maputil.ReadEmbeddedMap(impMap, "video"); ok {
+			videoMap["w"] = json.RawMessage("0")
+			videoMap["h"] = json.RawMessage("0")
+		} else {
+			return jsonData, errors.New("unable to find imp[0].video in json data")
 		}
 
-		json, err = jsonparser.Set(json, []byte("0"), "imp", "[0]", "video", "h")
-		if err != nil {
-			return json, err
+	case openrtb_ext.BidTypeNative:
+		nativeMap, ok := maputil.ReadEmbeddedMap(impMap, "native")
+		if !ok {
+			return jsonData, errors.New("unable to find imp[0].video in json data")
 		}
 
-		break
-
-	case openrtb_ext.BidTypeNative:
 		// Set w/h to -1 for native impressions based on the facebook native spec.
 		// We have to set this post-serialization since the OpenRTB protocol doesn't
-		// actaully support w/h in the native object
-		json, err = jsonparser.Set(json, []byte("-1"), "imp", "[0]", "native", "w")
-		if err != nil {
-			return json, err
-		}
-
-		json, err = jsonparser.Set(json, []byte("-1"), "imp", "[0]", "native", "h")
-		if err != nil {
-			return json, err
-		}
+		// actually support w/h in the native object
+		nativeMap["w"] = json.RawMessage("-1")
+		nativeMap["h"] = json.RawMessage("-1")
 
 		// The FAN adserver does not expect the native request payload, all that information
 		// is derived server side based on the placement ID. We need to remove these pieces of
 		// information manually since OpenRTB (and thus mxmCherry) never omit native.request
-		json = jsonparser.Delete(json, "imp", "[0]", "native", "ver")
-		json = jsonparser.Delete(json, "imp", "[0]", "native", "request")
-
-		break
+		delete(nativeMap, "ver")
+		delete(nativeMap, "request")
 	}
 
-	return json, nil
+	if jsonReEncoded, err := json.Marshal(jsonMap); err == nil {
+		return jsonReEncoded, nil
+	} else {
+		return nil, fmt.Errorf("unable to encode json data (%v)", err)
+	}
 }
 
 func (this *FacebookAdapter) MakeBids(request *openrtb.BidRequest, adapterRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
@@ -430,7 +427,7 @@ func resolveImpType(imp *openrtb.Imp) (openrtb_ext.BidType, bool) {
 	return openrtb_ext.BidTypeBanner, false
 }
 
-func NewFacebookBidder(client *http.Client, platformID string, appSecret string) adapters.Bidder {
+func NewFacebookBidder(platformID string, appSecret string) adapters.Bidder {
 	if platformID == "" {
 		glog.Errorf("No facebook partnerID specified. Calls to the Audience Network will fail. Did you set adapters.facebook.platform_id in the app config?")
 		return &adapters.MisconfiguredBidder{
@@ -447,11 +444,8 @@ func NewFacebookBidder(client *http.Client, platformID string, appSecret string)
 		}
 	}
 
-	a := &adapters.HTTPAdapter{Client: client}
-
 	return &FacebookAdapter{
-		http: a,
-		URI:  "https://an.facebook.com/placementbid.ortb",
+		URI: "https://an.facebook.com/placementbid.ortb",
 		//for AB test
 		nonSecureUri: "http://an.facebook.com/placementbid.ortb",
 		platformID:   platformID,
diff --git a/adapters/kubient/kubient.go b/adapters/kubient/kubient.go
index cb1fe93f..acfaa44b 100644
--- a/adapters/kubient/kubient.go
+++ b/adapters/kubient/kubient.go
@@ -24,10 +24,24 @@ type KubientAdapter struct {
 func (adapter *KubientAdapter) MakeRequests(
 	openRTBRequest *openrtb.BidRequest,
 	reqInfo *adapters.ExtraRequestInfo,
-) (
-	requestsToBidder []*adapters.RequestData,
-	errs []error,
-) {
+) ([]*adapters.RequestData, []error) {
+	if len(openRTBRequest.Imp) == 0 {
+		return nil, []error{&errortypes.BadInput{
+			Message: "No impression in the bid request",
+		}}
+	}
+	errs := make([]error, 0, len(openRTBRequest.Imp))
+	hasErrors := false
+	for _, impObj := range openRTBRequest.Imp {
+		err := checkImpExt(impObj)
+		if err != nil {
+			errs = append(errs, err)
+			hasErrors = true
+		}
+	}
+	if hasErrors {
+		return nil, errs
+	}
 	openRTBRequestJSON, err := json.Marshal(openRTBRequest)
 	if err != nil {
 		errs = append(errs, err)
@@ -36,17 +50,36 @@ func (adapter *KubientAdapter) MakeRequests(
 
 	headers := http.Header{}
 	headers.Add("Content-Type", "application/json;charset=utf-8")
-	requestToBidder := &adapters.RequestData{
+	requestsToBidder := []*adapters.RequestData{{
 		Method:  "POST",
 		Uri:     adapter.endpoint,
 		Body:    openRTBRequestJSON,
 		Headers: headers,
-	}
-	requestsToBidder = append(requestsToBidder, requestToBidder)
-
+	}}
 	return requestsToBidder, errs
 }
 
+func checkImpExt(impObj openrtb.Imp) error {
+	var bidderExt adapters.ExtImpBidder
+	if err := json.Unmarshal(impObj.Ext, &bidderExt); err != nil {
+		return &errortypes.BadInput{
+			Message: "ext.bidder not provided",
+		}
+	}
+	var kubientExt openrtb_ext.ExtImpKubient
+	if err := json.Unmarshal(bidderExt.Bidder, &kubientExt); err != nil {
+		return &errortypes.BadInput{
+			Message: "ext.bidder.zoneid is not provided",
+		}
+	}
+	if kubientExt.ZoneID == "" {
+		return &errortypes.BadInput{
+			Message: "zoneid is empty",
+		}
+	}
+	return nil
+}
+
 // MakeBids makes the bids
 func (adapter *KubientAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
 	var errs []error
diff --git a/adapters/openx/openx.go b/adapters/openx/openx.go
index 63297d0a..ca88b18b 100644
--- a/adapters/openx/openx.go
+++ b/adapters/openx/openx.go
@@ -143,11 +143,13 @@ func preprocess(imp *openrtb.Imp, reqExt *openxReqExt) error {
 	}
 
 	if imp.Video != nil {
+		videoCopy := *imp.Video
 		if bidderExt.Prebid != nil && bidderExt.Prebid.IsRewardedInventory == 1 {
-			imp.Video.Ext = json.RawMessage(`{"rewarded":1}`)
+			videoCopy.Ext = json.RawMessage(`{"rewarded":1}`)
 		} else {
-			imp.Video.Ext = nil
+			videoCopy.Ext = nil
 		}
+		imp.Video = &videoCopy
 	}
 
 	return nil
diff --git a/adapters/pubnative/pubnative.go b/adapters/pubnative/pubnative.go
index 4dc92920..777ac4a0 100644
--- a/adapters/pubnative/pubnative.go
+++ b/adapters/pubnative/pubnative.go
@@ -83,7 +83,6 @@ func checkRequest(request *openrtb.BidRequest) error {
 		}
 	}
 
-	request.Test = 0 // don't forward test flag to PN adserver
 	return nil
 }
 
diff --git a/config/config.go b/config/config.go
index c50118e2..cc1d4a0a 100755
--- a/config/config.go
+++ b/config/config.go
@@ -17,7 +17,7 @@ import (
 	validator "github.com/asaskevich/govalidator"
 )
 
-// Configuration
+// Configuration specifies the static application config.
 type Configuration struct {
 	ExternalURL string     `mapstructure:"external_url"`
 	Host        string     `mapstructure:"host"`
@@ -69,11 +69,14 @@ type Configuration struct {
 	RequestTimeoutHeaders RequestTimeoutHeaders `mapstructure:"request_timeout_headers"`
 	// Debug/logging flags go here
 	Debug Debug `mapstructure:"debug"`
+	// RequestValidation specifies the request validation options.
+	RequestValidation RequestValidation `mapstructure:"request_validation"`
 }
 
 const MIN_COOKIE_SIZE_BYTES = 500
 
 type HTTPClient struct {
+	MaxConnsPerHost     int `mapstructure:"max_connections_per_host"`
 	MaxIdleConns        int `mapstructure:"max_idle_connections"`
 	MaxIdleConnsPerHost int `mapstructure:"max_idle_connections_per_host"`
 	IdleConnTimeout     int `mapstructure:"idle_connection_timeout_seconds"`
@@ -212,6 +215,7 @@ type Analytics struct {
 type CurrencyConverter struct {
 	FetchURL             string `mapstructure:"fetch_url"`
 	FetchIntervalSeconds int    `mapstructure:"fetch_interval_seconds"`
+	StaleRatesSeconds    int    `mapstructure:"stale_rates_seconds"`
 }
 
 func (cfg *CurrencyConverter) validate(errs configErrors) configErrors {
@@ -238,15 +242,15 @@ type HostCookie struct {
 	TTL int64 `mapstructure:"ttl_days"`
 }
 
+func (cfg *HostCookie) TTLDuration() time.Duration {
+	return time.Duration(cfg.TTL) * time.Hour * 24
+}
+
 type RequestTimeoutHeaders struct {
 	RequestTimeInQueue    string `mapstructure:"request_time_in_queue"`
 	RequestTimeoutInQueue string `mapstructure:"request_timeout_in_queue"`
 }
 
-func (cfg *HostCookie) TTLDuration() time.Duration {
-	return time.Duration(cfg.TTL) * time.Hour * 24
-}
-
 const (
 	dummyHost        string = "dummyhost.com"
 	dummyPublisherID string = "12"
@@ -497,6 +501,15 @@ func New(v *viper.Viper) (*Configuration, error) {
 	}
 	c.setDerivedDefaults()
 
+	if err := c.RequestValidation.Parse(); err != nil {
+		return nil, err
+	}
+
+	if err := isValidCookieSize(c.HostCookie.MaxCookieSizeBytes); err != nil {
+		glog.Fatal(fmt.Printf("Max cookie size %d cannot be less than %d \n", c.HostCookie.MaxCookieSizeBytes, MIN_COOKIE_SIZE_BYTES))
+		return nil, err
+	}
+
 	// To look for a request's publisher_id in the NonStandardPublishers list in
 	// O(1) time, we fill this hash table located in the NonStandardPublisherMap field of GDPR
 	c.GDPR.NonStandardPublisherMap = make(map[string]int)
@@ -518,11 +531,6 @@ func New(v *viper.Viper) (*Configuration, error) {
 		c.BlacklistedAcctMap[c.BlacklistedAccts[i]] = true
 	}
 
-	if err := isValidCookieSize(c.HostCookie.MaxCookieSizeBytes); err != nil {
-		glog.Fatal(fmt.Printf("Max cookie size %d cannot be less than %d \n", c.HostCookie.MaxCookieSizeBytes, MIN_COOKIE_SIZE_BYTES))
-		return nil, err
-	}
-
 	glog.Info("Logging the resolved configuration:")
 	logGeneral(reflect.ValueOf(c), "  \t")
 	if errs := c.validate(); len(errs) > 0 {
@@ -563,6 +571,7 @@ func (cfg *Configuration) setDerivedDefaults() {
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdtarget, "https://sync.console.adtarget.com.tr/csync?t=p&ep=0&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadtarget%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Buid%7D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdtelligent, "https://sync.adtelligent.com/csync?t=p&ep=0&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadtelligent%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Buid%7D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdmixer, "https://inv-nets.admixer.net/adxcm.aspx?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir=1&rurl="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadmixer%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%24visitor_cookie%24%24")
+	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdman, "https://sync.admanmedia.com/pbs.gif?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadman%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5BUID%5D")
 	// openrtb_ext.BidderAdOcean doesn't have a good default.
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdvangelists, "https://nep.advangelists.com/xp/user-sync?acctid={aid}&&redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadvangelists%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAJA, "https://ad.as.amanad.adtdp.com/v1/sync/ssp?ssp=4&gdpr={{.GDPR}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Daja%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%25s")
@@ -668,9 +677,11 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("host_cookie.value", "")
 	v.SetDefault("host_cookie.ttl_days", 90)
 	v.SetDefault("host_cookie.max_cookie_size_bytes", 0)
+	v.SetDefault("http_client.max_connections_per_host", 0) // unlimited
 	v.SetDefault("http_client.max_idle_connections", 400)
 	v.SetDefault("http_client.max_idle_connections_per_host", 10)
 	v.SetDefault("http_client.idle_connection_timeout_seconds", 60)
+	v.SetDefault("http_client_cache.max_connections_per_host", 0) // unlimited
 	v.SetDefault("http_client_cache.max_idle_connections", 10)
 	v.SetDefault("http_client_cache.max_idle_connections_per_host", 2)
 	v.SetDefault("http_client_cache.idle_connection_timeout_seconds", 60)
@@ -763,6 +774,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.adhese.endpoint", "https://ads-{{.AccountID}}.adhese.com/json")
 	v.SetDefault("adapters.adkernel.endpoint", "http://{{.Host}}/hb?zone={{.ZoneID}}")
 	v.SetDefault("adapters.adkerneladn.endpoint", "http://{{.Host}}/rtbpub?account={{.PublisherID}}")
+	v.SetDefault("adapters.adman.endpoint", "http://pub.admanmedia.com/?c=o&m=ortb")
 	v.SetDefault("adapters.admixer.endpoint", "http://inv-nets.admixer.net/pbs.aspx")
 	v.SetDefault("adapters.adocean.endpoint", "https://{{.Host}}")
 	v.SetDefault("adapters.adoppler.endpoint", "http://app.trustedmarketplace.io/ads")
@@ -780,7 +792,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.beintoo.endpoint", "https://ib.beintoo.com/um")
 	v.SetDefault("adapters.brightroll.endpoint", "http://east-bid.ybp.yahoo.com/bid/appnexuspbs")
 	v.SetDefault("adapters.consumable.endpoint", "https://e.serverbid.com/api/v2")
-	v.SetDefault("adapters.conversant.endpoint", "http://api.hb.ad.cpe.dotomi.com/s2s/header/24")
+	v.SetDefault("adapters.conversant.endpoint", "http://api.hb.ad.cpe.dotomi.com/cvx/server/hb/ortb/25")
 	v.SetDefault("adapters.cpmstar.endpoint", "https://server.cpmstar.com/openrtbbidrq.aspx")
 	v.SetDefault("adapters.datablocks.endpoint", "http://{{.Host}}/openrtb2?sid={{.SourceId}}")
 	v.SetDefault("adapters.emx_digital.endpoint", "https://hb.emxdgt.com")
@@ -855,6 +867,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("lmt.enforce", true)
 	v.SetDefault("currency_converter.fetch_url", "https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json")
 	v.SetDefault("currency_converter.fetch_interval_seconds", 1800) // fetch currency rates every 30 minutes
+	v.SetDefault("currency_converter.stale_rates_seconds", 0)
 	v.SetDefault("default_request.type", "")
 	v.SetDefault("default_request.file.name", "")
 	v.SetDefault("default_request.alias_info", false)
@@ -870,8 +883,23 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("debug.timeout_notification.sampling_rate", 0.0)
 	v.SetDefault("debug.timeout_notification.fail_only", false)
 
+	/* IPv4
+	/*  Site Local: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
+	/*  Link Local: 169.254.0.0/16
+	/*  Loopback:   127.0.0.0/8
+	/*
+	/* IPv6
+	/*  Loopback:     ::1/128
+	/*  Unique Local: fc00::/7
+	/*  Link Local:   fe80::/10
+	/*  Multicast:    ff00::/8
+	*/
+	v.SetDefault("request_validation.ipv4_private_networks", []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16", "127.0.0.0/8"})
+	v.SetDefault("request_validation.ipv6_private_networks", []string{"::1/128", "fc00::/7", "fe80::/10", "ff00::/8"})
+
 	// Set environment variable support:
 	v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
+	v.SetTypeByDefaultValue(true)
 	v.SetEnvPrefix("PBS")
 	v.AutomaticEnv()
 	v.ReadInConfig()
diff --git a/config/requestvalidation.go b/config/requestvalidation.go
new file mode 100644
index 00000000..0824f4da
--- /dev/null
+++ b/config/requestvalidation.go
@@ -0,0 +1,55 @@
+package config
+
+import (
+	"errors"
+	"fmt"
+	"net"
+	"strings"
+)
+
+// RequestValidation specifies the request validation options.
+type RequestValidation struct {
+	IPv4PrivateNetworks       []string `mapstructure:"ipv4_private_networks,flow"`
+	IPv4PrivateNetworksParsed []net.IPNet
+
+	IPv6PrivateNetworks       []string `mapstructure:"ipv6_private_networks,flow"`
+	IPv6PrivateNetworksParsed []net.IPNet
+}
+
+// Parse converts the CIDR representation of the IPv4 and IPv6 private networks as net.IPNet structs, or returns an error if at least one is invalid.
+func (r *RequestValidation) Parse() error {
+	ipv4Nets, err := parseNetworks(r.IPv4PrivateNetworks, net.IPv4len)
+	if err != nil {
+		return errors.New("Invalid private IPv4 network: " + err.Error())
+	}
+
+	ipv6Nets, err := parseNetworks(r.IPv6PrivateNetworks, net.IPv6len)
+	if err != nil {
+		return errors.New("Invalid private IPv6 network: " + err.Error())
+	}
+
+	r.IPv4PrivateNetworksParsed = ipv4Nets
+	r.IPv6PrivateNetworksParsed = ipv6Nets
+	return nil
+}
+
+func parseNetworks(networks []string, networksLen int) ([]net.IPNet, error) {
+	ipNetworks := make([]net.IPNet, 0, len(networks))
+	errMsg := strings.Builder{}
+
+	for _, v := range networks {
+		v := strings.TrimSpace(v)
+
+		if _, ipNet, err := net.ParseCIDR(v); err != nil || len(ipNet.IP) != networksLen {
+			fmt.Fprintf(&errMsg, "'%s',", v)
+		} else {
+			ipNetworks = append(ipNetworks, *ipNet)
+		}
+	}
+
+	if errMsg.Len() > 0 {
+		return nil, errors.New(errMsg.String()[:errMsg.Len()-1])
+	}
+
+	return ipNetworks, nil
+}
diff --git a/currencies/rate_converter.go b/currencies/rate_converter.go
index 6c6ed172..d22f347b 100644
--- a/currencies/rate_converter.go
+++ b/currencies/rate_converter.go
@@ -12,14 +12,15 @@ import (
 
 // RateConverter holds the currencies conversion rates dictionary
 type RateConverter struct {
-	httpClient       httpClient
-	done             chan bool
-	updateNotifier   chan<- int
-	fetchingInterval time.Duration
-	syncSourceURL    string
-	rates            atomic.Value // Should only hold Rates struct
-	lastUpdated      atomic.Value // Should only hold time.Time
-	constantRates    Conversions
+	httpClient          httpClient
+	done                chan bool
+	updateNotifier      chan<- int
+	fetchingInterval    time.Duration
+	staleRatesThreshold time.Duration
+	syncSourceURL       string
+	rates               atomic.Value // Should only hold Rates struct
+	lastUpdated         atomic.Value // Should only hold time.Time
+	constantRates       Conversions
 }
 
 // NewRateConverter returns a new RateConverter
@@ -27,11 +28,13 @@ func NewRateConverter(
 	httpClient httpClient,
 	syncSourceURL string,
 	fetchingInterval time.Duration,
+	staleRatesThreshold time.Duration,
 ) *RateConverter {
 	return NewRateConverterWithNotifier(
 		httpClient,
 		syncSourceURL,
 		fetchingInterval,
+		staleRatesThreshold,
 		nil, // no notifier channel specified, won't send any notifications
 	)
 }
@@ -40,7 +43,7 @@ func NewRateConverter(
 // By default there will be no currencies conversions done.
 // `currencies.ConstantRate` will be used.
 func NewRateConverterDefault() *RateConverter {
-	return NewRateConverter(&http.Client{}, "", time.Duration(0))
+	return NewRateConverter(&http.Client{}, "", time.Duration(0), time.Duration(0))
 }
 
 // NewRateConverterWithNotifier returns a new RateConverter
@@ -51,22 +54,24 @@ func NewRateConverterWithNotifier(
 	httpClient httpClient,
 	syncSourceURL string,
 	fetchingInterval time.Duration,
+	staleRatesThreshold time.Duration,
 	updateNotifier chan<- int,
 ) *RateConverter {
 	rc := &RateConverter{
-		httpClient:       httpClient,
-		done:             make(chan bool),
-		updateNotifier:   updateNotifier,
-		fetchingInterval: fetchingInterval,
-		syncSourceURL:    syncSourceURL,
-		rates:            atomic.Value{},
-		lastUpdated:      atomic.Value{},
+		httpClient:          httpClient,
+		done:                make(chan bool),
+		updateNotifier:      updateNotifier,
+		fetchingInterval:    fetchingInterval,
+		staleRatesThreshold: staleRatesThreshold,
+		syncSourceURL:       syncSourceURL,
+		rates:               atomic.Value{},
+		lastUpdated:         atomic.Value{},
+		constantRates:       NewConstantRates(),
 	}
 
 	// In case host do not want to support currency lookup
 	// we just stop here and do nothing
 	if rc.fetchingInterval == time.Duration(0) {
-		rc.constantRates = NewConstantRates()
 		return rc
 	}
 
@@ -111,7 +116,12 @@ func (rc *RateConverter) Update() error {
 		rc.rates.Store(rates)
 		rc.lastUpdated.Store(time.Now())
 	} else {
-		glog.Errorf("Error updating conversion rates: %v", err)
+		if rc.CheckStaleRates() {
+			rc.ClearRates()
+			glog.Errorf("Error updating conversion rates, falling back to constant rates: %v", err)
+		} else {
+			glog.Errorf("Error updating conversion rates: %v", err)
+		}
 	}
 
 	return err
@@ -160,14 +170,33 @@ func (rc *RateConverter) LastUpdated() time.Time {
 
 // Rates returns current conversions rates
 func (rc *RateConverter) Rates() Conversions {
-	if rc.constantRates != nil {
-		// Converter is not active, returning the constant rates
-		return rc.constantRates
-	}
-	if rates := rc.rates.Load(); rates != nil {
+	// atomic.Value field rates is an empty interface and will be of type *Rates the first time rates are stored
+	// or nil if the rates have never been stored
+	if rates := rc.rates.Load(); rates != (*Rates)(nil) && rates != nil {
 		return rates.(*Rates)
 	}
-	return nil
+	return rc.constantRates
+}
+
+// ClearRates sets the rates to nil
+func (rc *RateConverter) ClearRates() {
+	// atomic.Value field rates must be of type *Rates so we cast nil to that type
+	rc.rates.Store((*Rates)(nil))
+}
+
+// CheckStaleRates checks if loaded third party conversion rates are stale
+func (rc *RateConverter) CheckStaleRates() bool {
+	if rc.staleRatesThreshold <= 0 {
+		return false
+	}
+	currentTime := time.Now().UTC()
+	if lastUpdated := rc.lastUpdated.Load(); lastUpdated != nil {
+		delta := currentTime.Sub(lastUpdated.(time.Time).UTC())
+		if delta.Seconds() > rc.staleRatesThreshold.Seconds() {
+			return true
+		}
+	}
+	return false
 }
 
 // GetInfo returns setup information about the converter
diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go
index 2dcd572c..e8b5d3ec 100644
--- a/endpoints/openrtb2/amp_auction.go
+++ b/endpoints/openrtb2/amp_auction.go
@@ -26,6 +26,7 @@ import (
 	"github.com/prebid/prebid-server/stored_requests"
 	"github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher"
 	"github.com/prebid/prebid-server/usersync"
+	"github.com/prebid/prebid-server/util/iputil"
 )
 
 const defaultAmpRequestTimeoutMillis = 900
@@ -58,6 +59,11 @@ func NewAmpEndpoint(
 
 	defRequest := defReqJSON != nil && len(defReqJSON) > 0
 
+	ipValidator := iputil.PublicNetworkIPValidator{
+		IPv4PrivateNetworks: cfg.RequestValidation.IPv4PrivateNetworksParsed,
+		IPv6PrivateNetworks: cfg.RequestValidation.IPv6PrivateNetworksParsed,
+	}
+
 	return httprouter.Handle((&endpointDeps{
 		ex,
 		validator,
@@ -72,7 +78,8 @@ func NewAmpEndpoint(
 		defReqJSON,
 		bidderMap,
 		nil,
-		nil}).AmpAuction), nil
+		nil,
+		ipValidator}).AmpAuction), nil
 
 }
 
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index bd50fca9..20acc2ae 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -27,12 +27,13 @@ import (
 	"github.com/prebid/prebid-server/exchange"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/pbsmetrics"
-	"github.com/prebid/prebid-server/prebid"
 	"github.com/prebid/prebid-server/prebid_cache_client"
 	"github.com/prebid/prebid-server/privacy/ccpa"
 	"github.com/prebid/prebid-server/stored_requests"
 	"github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher"
 	"github.com/prebid/prebid-server/usersync"
+	"github.com/prebid/prebid-server/util/httputil"
+	"github.com/prebid/prebid-server/util/iputil"
 	"golang.org/x/net/publicsuffix"
 )
 
@@ -43,8 +44,14 @@ func NewEndpoint(ex exchange.Exchange, validator openrtb_ext.BidderParamValidato
 	if ex == nil || validator == nil || requestsById == nil || cfg == nil || met == nil {
 		return nil, errors.New("NewEndpoint requires non-nil arguments.")
 	}
+
 	defRequest := defReqJSON != nil && len(defReqJSON) > 0
 
+	ipValidator := iputil.PublicNetworkIPValidator{
+		IPv4PrivateNetworks: cfg.RequestValidation.IPv4PrivateNetworksParsed,
+		IPv6PrivateNetworks: cfg.RequestValidation.IPv6PrivateNetworksParsed,
+	}
+
 	return httprouter.Handle((&endpointDeps{
 		ex,
 		validator,
@@ -59,24 +66,26 @@ func NewEndpoint(ex exchange.Exchange, validator openrtb_ext.BidderParamValidato
 		defReqJSON,
 		bidderMap,
 		nil,
-		nil}).Auction), nil
+		nil,
+		ipValidator}).Auction), nil
 }
 
 type endpointDeps struct {
-	ex               exchange.Exchange
-	paramsValidator  openrtb_ext.BidderParamValidator
-	storedReqFetcher stored_requests.Fetcher
-	videoFetcher     stored_requests.Fetcher
-	categories       stored_requests.CategoryFetcher
-	cfg              *config.Configuration
-	metricsEngine    pbsmetrics.MetricsEngine
-	analytics        analytics.PBSAnalyticsModule
-	disabledBidders  map[string]string
-	defaultRequest   bool
-	defReqJSON       []byte
-	bidderMap        map[string]openrtb_ext.BidderName
-	cache            prebid_cache_client.Client
-	debugLogRegexp   *regexp.Regexp
+	ex                        exchange.Exchange
+	paramsValidator           openrtb_ext.BidderParamValidator
+	storedReqFetcher          stored_requests.Fetcher
+	videoFetcher              stored_requests.Fetcher
+	categories                stored_requests.CategoryFetcher
+	cfg                       *config.Configuration
+	metricsEngine             pbsmetrics.MetricsEngine
+	analytics                 analytics.PBSAnalyticsModule
+	disabledBidders           map[string]string
+	defaultRequest            bool
+	defReqJSON                []byte
+	bidderMap                 map[string]openrtb_ext.BidderName
+	cache                     prebid_cache_client.Client
+	debugLogRegexp            *regexp.Regexp
+	privateNetworkIPValidator iputil.IPValidator
 }
 
 func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
@@ -308,17 +317,14 @@ func (deps *endpointDeps) validateRequest(req *openrtb.BidRequest) []error {
 		return errL
 	}
 
-	ccpaPolicy, ccpaPolicyErr := ccpa.ReadPolicy(req)
-	if ccpaPolicyErr != nil {
-		errL = append(errL, ccpaPolicyErr)
+	if policy, err := ccpa.ReadPolicy(req); err != nil {
+		errL = append(errL, errL...)
 		return errL
-	}
-
-	if err := ccpaPolicy.Validate(); err != nil {
+	} else if err := policy.Validate(); err != nil {
 		errL = append(errL, &errortypes.InvalidPrivacyConsent{Message: fmt.Sprintf("CCPA consent is invalid and will be ignored. (%v)", err)})
 
-		ccpaPolicy.Value = ""
-		if err := ccpaPolicy.Write(req); err != nil {
+		policy.Value = ""
+		if err := policy.Write(req); err != nil {
 			errL = append(errL, fmt.Errorf("Unable to remove invalid CCPA consent from the request. (%v)", err))
 		}
 	}
@@ -914,13 +920,27 @@ func validateRegs(regs *openrtb.Regs) error {
 	return nil
 }
 
+func sanitizeRequest(r *openrtb.BidRequest, ipValidator iputil.IPValidator) {
+	if r.Device != nil {
+		if ip, ver := iputil.ParseIP(r.Device.IP); ip == nil || ver != iputil.IPv4 || !ipValidator.IsValid(ip, ver) {
+			r.Device.IP = ""
+		}
+
+		if ip, ver := iputil.ParseIP(r.Device.IPv6); ip == nil || ver != iputil.IPv6 || !ipValidator.IsValid(ip, ver) {
+			r.Device.IPv6 = ""
+		}
+	}
+}
+
 // setFieldsImplicitly uses _implicit_ information from the httpReq to set values on bidReq.
 // This function does not consume the request body, which was set explicitly, but infers certain
 // OpenRTB properties from the headers and other implicit info.
 //
 // This function _should not_ override any fields which were defined explicitly by the caller in the request.
 func (deps *endpointDeps) setFieldsImplicitly(httpReq *http.Request, bidReq *openrtb.BidRequest) {
-	setDeviceImplicitly(httpReq, bidReq)
+	sanitizeRequest(bidReq, deps.privateNetworkIPValidator)
+
+	setDeviceImplicitly(httpReq, bidReq, deps.privateNetworkIPValidator)
 
 	// Per the OpenRTB spec: A bid request must not contain both a Site and an App object.
 	if bidReq.App == nil {
@@ -932,8 +952,8 @@ func (deps *endpointDeps) setFieldsImplicitly(httpReq *http.Request, bidReq *ope
 }
 
 // setDeviceImplicitly uses implicit info from httpReq to populate bidReq.Device
-func setDeviceImplicitly(httpReq *http.Request, bidReq *openrtb.BidRequest) {
-	setIPImplicitly(httpReq, bidReq) // Fixes #230
+func setDeviceImplicitly(httpReq *http.Request, bidReq *openrtb.BidRequest, ipValidtor iputil.IPValidator) {
+	setIPImplicitly(httpReq, bidReq, ipValidtor)
 	setUAImplicitly(httpReq, bidReq)
 }
 
@@ -975,7 +995,7 @@ func setSiteImplicitly(httpReq *http.Request, bidReq *openrtb.BidRequest) {
 func setImpsImplicitly(httpReq *http.Request, imps []openrtb.Imp) {
 	secure := int8(1)
 	for i := 0; i < len(imps); i++ {
-		if imps[i].Secure == nil && prebid.IsSecure(httpReq) {
+		if imps[i].Secure == nil && httputil.IsSecure(httpReq) {
 			imps[i].Secure = &secure
 		}
 	}
@@ -1132,13 +1152,21 @@ func getStoredRequestId(data []byte) (string, bool, error) {
 }
 
 // setIPImplicitly sets the IP address on bidReq, if it's not explicitly defined and we can figure it out.
-func setIPImplicitly(httpReq *http.Request, bidReq *openrtb.BidRequest) {
-	if bidReq.Device == nil || bidReq.Device.IP == "" {
-		if ip := prebid.GetIP(httpReq); ip != "" {
-			if bidReq.Device == nil {
-				bidReq.Device = &openrtb.Device{}
+func setIPImplicitly(httpReq *http.Request, bidReq *openrtb.BidRequest, ipValidator iputil.IPValidator) {
+	if bidReq.Device == nil || (bidReq.Device.IP == "" && bidReq.Device.IPv6 == "") {
+		if ip, ver := httputil.FindIP(httpReq, ipValidator); ip != nil {
+			switch ver {
+			case iputil.IPv4:
+				if bidReq.Device == nil {
+					bidReq.Device = &openrtb.Device{}
+				}
+				bidReq.Device.IP = ip.String()
+			case iputil.IPv6:
+				if bidReq.Device == nil {
+					bidReq.Device = &openrtb.Device{}
+				}
+				bidReq.Device.IPv6 = ip.String()
 			}
-			bidReq.Device.IP = ip
 		}
 	}
 }
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-ipv4.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-ipv4.json
new file mode 100644
index 00000000..feade898
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-ipv4.json
@@ -0,0 +1,38 @@
+{
+  "id": "some-request-id",
+  "site": {
+    "page": "test.somepage.com"
+  },
+  "imp": [{
+    "id": "my-imp-id",
+    "banner": {
+      "format": [{
+        "w": 300,
+        "h": 600
+      }]
+    },
+    "pmp": {
+      "deals": [{
+        "id": "some-deal-id"
+      }]
+    },
+    "ext": {
+      "appnexus": {
+        "placementId": 12883451
+      }
+    }
+  }],
+  "device": {
+    "ip": "8.8.8.8"
+  },
+  "ext": {
+    "prebid": {
+      "targeting": {
+        "pricegranularity": "low"
+      },
+      "cache": {
+        "bids": {}
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-ipv6.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-ipv6.json
new file mode 100644
index 00000000..42d8d37b
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/site-has-ipv6.json
@@ -0,0 +1,38 @@
+{
+    "id": "some-request-id",
+    "site": {
+      "page": "test.somepage.com"
+    },
+    "imp": [{
+      "id": "my-imp-id",
+      "banner": {
+        "format": [{
+          "w": 300,
+          "h": 600
+        }]
+      },
+      "pmp": {
+        "deals": [{
+          "id": "some-deal-id"
+        }]
+      },
+      "ext": {
+        "appnexus": {
+          "placementId": 12883451
+        }
+      }
+    }],
+    "device": {
+      "ipv6": "8888::"
+    },
+    "ext": {
+      "prebid": {
+        "targeting": {
+          "pricegranularity": "low"
+        },
+        "cache": {
+          "bids": {}
+        }
+      }
+    }
+  }
\ No newline at end of file
diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go
index 64c99fa5..18678be5 100644
--- a/endpoints/openrtb2/video_auction.go
+++ b/endpoints/openrtb2/video_auction.go
@@ -18,6 +18,7 @@ import (
 	jsonpatch "github.com/evanphx/json-patch"
 	"github.com/gofrs/uuid"
 	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/util/iputil"
 
 	"github.com/golang/glog"
 	"github.com/julienschmidt/httprouter"
@@ -39,11 +40,32 @@ func NewVideoEndpoint(ex exchange.Exchange, validator openrtb_ext.BidderParamVal
 	if ex == nil || validator == nil || requestsById == nil || cfg == nil || met == nil {
 		return nil, errors.New("NewVideoEndpoint requires non-nil arguments.")
 	}
+
 	defRequest := defReqJSON != nil && len(defReqJSON) > 0
 
+	ipValidator := iputil.PublicNetworkIPValidator{
+		IPv4PrivateNetworks: cfg.RequestValidation.IPv4PrivateNetworksParsed,
+		IPv6PrivateNetworks: cfg.RequestValidation.IPv6PrivateNetworksParsed,
+	}
+
 	videoEndpointRegexp := regexp.MustCompile(`[<>]`)
 
-	return httprouter.Handle((&endpointDeps{ex, validator, requestsById, videoFetcher, categories, cfg, met, pbsAnalytics, disabledBidders, defRequest, defReqJSON, bidderMap, cache, videoEndpointRegexp}).VideoAuctionEndpoint), nil
+	return httprouter.Handle((&endpointDeps{
+		ex,
+		validator,
+		requestsById,
+		videoFetcher,
+		categories,
+		cfg,
+		met,
+		pbsAnalytics,
+		disabledBidders,
+		defRequest,
+		defReqJSON,
+		bidderMap,
+		cache,
+		videoEndpointRegexp,
+		ipValidator}).VideoAuctionEndpoint), nil
 }
 
 /*
diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go
index 44054df0..1f62d232 100755
--- a/exchange/adapter_map.go
+++ b/exchange/adapter_map.go
@@ -14,6 +14,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/adhese"
 	"github.com/prebid/prebid-server/adapters/adkernel"
 	"github.com/prebid/prebid-server/adapters/adkernelAdn"
+	"github.com/prebid/prebid-server/adapters/adman"
 	"github.com/prebid/prebid-server/adapters/admixer"
 	"github.com/prebid/prebid-server/adapters/adocean"
 	"github.com/prebid/prebid-server/adapters/adoppler"
@@ -98,6 +99,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter
 		openrtb_ext.BidderAdhese:       adhese.NewAdheseBidder(cfg.Adapters[string(openrtb_ext.BidderAdhese)].Endpoint),
 		openrtb_ext.BidderAdkernel:     adkernel.NewAdkernelAdapter(cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderAdkernel))].Endpoint),
 		openrtb_ext.BidderAdkernelAdn:  adkernelAdn.NewAdkernelAdnAdapter(cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderAdkernelAdn))].Endpoint),
+		openrtb_ext.BidderAdman:        adman.NewAdmanBidder(cfg.Adapters[string(openrtb_ext.BidderAdman)].Endpoint),
 		openrtb_ext.BidderAdmixer:      admixer.NewAdmixerBidder(cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderAdmixer))].Endpoint),
 		openrtb_ext.BidderAdOcean:      adocean.NewAdOceanBidder(client, cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderAdOcean))].Endpoint),
 		openrtb_ext.BidderAdoppler:     adoppler.NewAdopplerBidder(cfg.Adapters[string(openrtb_ext.BidderAdoppler)].Endpoint),
@@ -120,7 +122,6 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter
 		openrtb_ext.BidderEngageBDR:    engagebdr.NewEngageBDRBidder(client, cfg.Adapters[string(openrtb_ext.BidderEngageBDR)].Endpoint),
 		openrtb_ext.BidderEPlanning:    eplanning.NewEPlanningBidder(client, cfg.Adapters[string(openrtb_ext.BidderEPlanning)].Endpoint),
 		openrtb_ext.BidderFacebook: audienceNetwork.NewFacebookBidder(
-			client,
 			cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderFacebook))].PlatformID,
 			cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderFacebook))].AppSecret),
 		openrtb_ext.BidderGamma:           gamma.NewGammaBidder(cfg.Adapters[string(openrtb_ext.BidderGamma)].Endpoint),
diff --git a/exchange/exchange.go b/exchange/exchange.go
index d7eab0f4..174a0b3e 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -104,8 +104,11 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque
 
 	// Slice of BidRequests, each a copy of the original cleaned to only contain bidder data for the named bidder
 	blabels := make(map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels)
-	cleanRequests, aliases, errs := cleanOpenRTBRequests(ctx, bidRequest, usersyncs, blabels, labels, e.gDPR, e.UsersyncIfAmbiguous, e.privacyConfig)
+	cleanRequests, aliases, cleanMetrics, errs := cleanOpenRTBRequests(ctx, bidRequest, usersyncs, blabels, labels, e.gDPR, e.UsersyncIfAmbiguous, e.privacyConfig)
 
+	if cleanMetrics.gdprEnforced {
+		e.me.RecordTCFReq(pbsmetrics.TCFVersionToValue(cleanMetrics.gdprTcfVersion))
+	}
 	// List of bidders we have requests for.
 	liveAdapters := listBiddersWithRequests(cleanRequests)
 
diff --git a/exchange/utils.go b/exchange/utils.go
index 54122d13..96c00ec0 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -6,6 +6,8 @@ import (
 	"fmt"
 	"math/rand"
 
+	"github.com/prebid/go-gdpr/vendorconsent"
+
 	"github.com/buger/jsonparser"
 	"github.com/mxmCherry/openrtb"
 	"github.com/prebid/prebid-server/config"
@@ -17,6 +19,15 @@ import (
 	"github.com/prebid/prebid-server/privacy/lmt"
 )
 
+// cleanMetrics is a struct to export any metrics data resulting from cleanOpenRTBRequests(). It starts with just
+// the TCF version, but made a struct to facilitate future expansion
+type cleanMetrics struct {
+	// A simple flag if GDPR is being enforced on this request.
+	gdprEnforced bool
+	// a zero value means a missing or invalid GDPR string
+	gdprTcfVersion int
+}
+
 // cleanOpenRTBRequests splits the input request into requests which are sanitized for each bidder. Intended behavior is:
 //
 //   1. BidRequest.Imp[].Ext will only contain the "prebid" field and a "bidder" field which has the params for the intended Bidder.
@@ -29,7 +40,7 @@ func cleanOpenRTBRequests(ctx context.Context,
 	labels pbsmetrics.Labels,
 	gDPR gdpr.Permissions,
 	usersyncIfAmbiguous bool,
-	privacyConfig config.Privacy) (requestsByBidder map[openrtb_ext.BidderName]*openrtb.BidRequest, aliases map[string]string, errs []error) {
+	privacyConfig config.Privacy) (requestsByBidder map[openrtb_ext.BidderName]*openrtb.BidRequest, aliases map[string]string, cleanMetrics cleanMetrics, errs []error) {
 
 	impsByBidder, errs := splitImps(orig.Imp)
 	if len(errs) > 0 {
@@ -64,6 +75,13 @@ func cleanOpenRTBRequests(ctx context.Context,
 		LMT:   lmtPolicy.ShouldEnforce(),
 	}
 
+	if gdpr == 1 {
+		cleanMetrics.gdprEnforced = true
+		parsedConsent, err := vendorconsent.ParseString(consent)
+		if err == nil {
+			cleanMetrics.gdprTcfVersion = int(parsedConsent.Version())
+		}
+	}
 	// bidder level privacy policies
 	for bidder, bidReq := range requestsByBidder {
 
diff --git a/go.sum b/go.sum
index 5d941b89..35b2b765 100644
--- a/go.sum
+++ b/go.sum
@@ -76,6 +76,8 @@ github.com/prebid/prebid-cache v0.0.0-20200218152159-6d6d678c1caf h1:CcE+KN1tCtW
 github.com/prebid/prebid-cache v0.0.0-20200218152159-6d6d678c1caf/go.mod h1:k5xrl5ZpnumN1S2x8w8cMiFYsgRuVyAeFJz+BkSi+98=
 github.com/prometheus/client_golang v0.0.0-20180623155954-77e8f2ddcfed h1:0dloFFFNNDG7c+8qtkYw2FdADrWy9s5cI8wHp6tK3Mg=
 github.com/prometheus/client_golang v0.0.0-20180623155954-77e8f2ddcfed/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.6.0 h1:YVPodQOcK15POxhgARIvnDRVpLcuK8mglnMrWfyrw6A=
+github.com/prometheus/client_golang v1.7.0 h1:wCi7urQOGBsYcQROHqpUUX4ct84xp40t9R9JX0FuA/U=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e h1:n/3MEhJQjQxrOUCzh1Y3Re6aJUUWRp2M9+Oc3eVn/54=
@@ -126,6 +128,7 @@ golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM=
 golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
diff --git a/main.go b/main.go
index d6ba430f..9a835f42 100644
--- a/main.go
+++ b/main.go
@@ -52,7 +52,9 @@ func loadConfig() (*config.Configuration, error) {
 
 func serve(revision string, cfg *config.Configuration) error {
 	fetchingInterval := time.Duration(cfg.CurrencyConverter.FetchIntervalSeconds) * time.Second
-	currencyConverter := currencies.NewRateConverter(&http.Client{}, cfg.CurrencyConverter.FetchURL, fetchingInterval)
+	staleRatesThreshold := time.Duration(cfg.CurrencyConverter.StaleRatesSeconds) * time.Second
+	currencyConverter := currencies.NewRateConverter(&http.Client{}, cfg.CurrencyConverter.FetchURL,
+		fetchingInterval, staleRatesThreshold)
 
 	r, err := router.New(cfg, currencyConverter)
 	if err != nil {
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 1f9cffb9..49d7b09d 100755
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -31,6 +31,7 @@ const (
 	BidderAdkernel         BidderName = "adkernel"
 	BidderAdkernelAdn      BidderName = "adkernelAdn"
 	BidderAdpone           BidderName = "adpone"
+	BidderAdman            BidderName = "adman"
 	BidderAdmixer          BidderName = "admixer"
 	BidderAdOcean          BidderName = "adocean"
 	BidderAdtarget         BidderName = "adtarget"
@@ -110,6 +111,7 @@ var BidderMap = map[string]BidderName{
 	"adhese":            BidderAdhese,
 	"adkernel":          BidderAdkernel,
 	"adkernelAdn":       BidderAdkernelAdn,
+	"adman":             BidderAdman,
 	"admixer":           BidderAdmixer,
 	"adocean":           BidderAdOcean,
 	"adpone":            BidderAdpone,
diff --git a/openrtb_ext/imp_adman.go b/openrtb_ext/imp_adman.go
new file mode 100644
index 00000000..bc794154
--- /dev/null
+++ b/openrtb_ext/imp_adman.go
@@ -0,0 +1,6 @@
+package openrtb_ext
+
+// ExtImpAdman defines adman specifiec param
+type ExtImpAdman struct {
+	TagID string `json:"TagID"`
+}
diff --git a/openrtb_ext/imp_kubient.go b/openrtb_ext/imp_kubient.go
new file mode 100644
index 00000000..fafd2a0e
--- /dev/null
+++ b/openrtb_ext/imp_kubient.go
@@ -0,0 +1,6 @@
+package openrtb_ext
+
+// ExtImpKubient defines the contract for bidrequest.imp[i].ext.kubient
+type ExtImpKubient struct {
+	ZoneID string `json:"zoneid"`
+}
diff --git a/pbs/pbsrequest.go b/pbs/pbsrequest.go
index 9e79b62b..30f8bd25 100644
--- a/pbs/pbsrequest.go
+++ b/pbs/pbsrequest.go
@@ -12,9 +12,10 @@ import (
 
 	"github.com/prebid/prebid-server/cache"
 	"github.com/prebid/prebid-server/config"
-	"github.com/prebid/prebid-server/prebid"
 	"github.com/prebid/prebid-server/stored_requests"
 	"github.com/prebid/prebid-server/usersync"
+	"github.com/prebid/prebid-server/util/httputil"
+	"github.com/prebid/prebid-server/util/iputil"
 
 	"github.com/blang/semver"
 	"github.com/buger/jsonparser"
@@ -216,6 +217,8 @@ func ParseMediaTypes(types []string) []MediaType {
 	return mtypes
 }
 
+var ipv4Validator iputil.IPValidator = iputil.VersionIPValidator{iputil.IPv4}
+
 func ParsePBSRequest(r *http.Request, cfg *config.AuctionTimeouts, cache cache.Cache, hostCookieConfig *config.HostCookie) (*PBSRequest, error) {
 	defer r.Body.Close()
 
@@ -235,7 +238,9 @@ func ParsePBSRequest(r *http.Request, cfg *config.AuctionTimeouts, cache cache.C
 	if pbsReq.Device == nil {
 		pbsReq.Device = &openrtb.Device{}
 	}
-	pbsReq.Device.IP = prebid.GetIP(r)
+	if ip, _ := httputil.FindIP(r, ipv4Validator); ip != nil {
+		pbsReq.Device.IP = ip.String()
+	}
 
 	if pbsReq.SDK == nil {
 		pbsReq.SDK = &SDK{}
@@ -291,7 +296,7 @@ func ParsePBSRequest(r *http.Request, cfg *config.AuctionTimeouts, cache cache.C
 		pbsReq.IsDebug = true
 	}
 
-	if prebid.IsSecure(r) {
+	if httputil.IsSecure(r) {
 		pbsReq.Secure = 1
 	}
 
diff --git a/pbsmetrics/config/metrics.go b/pbsmetrics/config/metrics.go
index 4e249785..3d105dea 100644
--- a/pbsmetrics/config/metrics.go
+++ b/pbsmetrics/config/metrics.go
@@ -195,6 +195,13 @@ func (me *MultiMetricsEngine) RecordTimeoutNotice(success bool) {
 	}
 }
 
+// RecordTCFReq across all engines
+func (me *MultiMetricsEngine) RecordTCFReq(version pbsmetrics.TCFVersionValue) {
+	for _, thisME := range *me {
+		thisME.RecordTCFReq(version)
+	}
+}
+
 // DummyMetricsEngine is a Noop metrics engine in case no metrics are configured. (may also be useful for tests)
 type DummyMetricsEngine struct{}
 
@@ -273,3 +280,7 @@ func (me *DummyMetricsEngine) RecordRequestQueueTime(success bool, requestType p
 // RecordTimeoutNotice as a noop
 func (me *DummyMetricsEngine) RecordTimeoutNotice(success bool) {
 }
+
+// RecordReq as a noop
+func (me *DummyMetricsEngine) RecordTCFReq(version pbsmetrics.TCFVersionValue) {
+}
diff --git a/pbsmetrics/go_metrics.go b/pbsmetrics/go_metrics.go
index 1ced4d57..73eb30a1 100644
--- a/pbsmetrics/go_metrics.go
+++ b/pbsmetrics/go_metrics.go
@@ -48,9 +48,13 @@ type Metrics struct {
 	ImpsTypeAudio  metrics.Meter
 	ImpsTypeNative metrics.Meter
 
+	// Notification timeout metrics
 	TimeoutNotificationSuccess metrics.Meter
 	TimeoutNotificationFailure metrics.Meter
 
+	// TCF adaption metrics
+	TCFReqVersion map[TCFVersionValue]metrics.Meter
+
 	AdapterMetrics map[openrtb_ext.BidderName]*AdapterMetrics
 	// Don't export accountMetrics because we need helper functions here to insure its properly populated dynamically
 	accountMetrics        map[string]*accountMetrics
@@ -137,6 +141,8 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa
 		TimeoutNotificationSuccess: blankMeter,
 		TimeoutNotificationFailure: blankMeter,
 
+		TCFReqVersion: make(map[TCFVersionValue]metrics.Meter, len(TCFVersions())),
+
 		AdapterMetrics:  make(map[openrtb_ext.BidderName]*AdapterMetrics, len(exchanges)),
 		accountMetrics:  make(map[string]*accountMetrics),
 		MetricsDisabled: disableMetrics,
@@ -154,6 +160,15 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa
 		}
 	}
 
+	for _, c := range CacheResults() {
+		newMetrics.StoredReqCacheMeter[c] = blankMeter
+		newMetrics.StoredImpCacheMeter[c] = blankMeter
+	}
+
+	for _, v := range TCFVersions() {
+		newMetrics.TCFReqVersion[v] = blankMeter
+	}
+
 	//to minimize memory usage, queuedTimeout metric is now supported for video endpoint only
 	//boolean value represents 2 general request statuses: accepted and rejected
 	newMetrics.RequestsQueueTimer["video"] = make(map[bool]metrics.Timer)
@@ -218,6 +233,11 @@ func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, d
 
 	newMetrics.TimeoutNotificationSuccess = metrics.GetOrRegisterMeter("timeout_notification.ok", registry)
 	newMetrics.TimeoutNotificationFailure = metrics.GetOrRegisterMeter("timeout_notification.failed", registry)
+
+	for _, version := range TCFVersions() {
+		newMetrics.TCFReqVersion[version] = metrics.GetOrRegisterMeter(fmt.Sprintf("privacy.request.tcf.%s", string(version)), registry)
+	}
+
 	return newMetrics
 }
 
@@ -562,6 +582,16 @@ func (me *Metrics) RecordTimeoutNotice(success bool) {
 	return
 }
 
+func (me *Metrics) RecordTCFReq(version TCFVersionValue) {
+	met, ok := me.TCFReqVersion[version]
+	if ok {
+		met.Mark(1)
+	} else {
+		me.TCFReqVersion[TCFVersionErr].Mark(1)
+	}
+	return
+}
+
 func doMark(bidder openrtb_ext.BidderName, meters map[openrtb_ext.BidderName]metrics.Meter) {
 	met, ok := meters[bidder]
 	if ok {
diff --git a/pbsmetrics/metrics.go b/pbsmetrics/metrics.go
index e65ba313..0e94fe71 100644
--- a/pbsmetrics/metrics.go
+++ b/pbsmetrics/metrics.go
@@ -248,6 +248,35 @@ func RequestActions() []RequestAction {
 	}
 }
 
+// TCFVersionValue : The possible values for TCF versions
+type TCFVersionValue string
+
+const (
+	TCFVersionErr TCFVersionValue = "err"
+	TCFVersionV1  TCFVersionValue = "v1"
+	TCFVersionV2  TCFVersionValue = "v2"
+)
+
+// TCFVersions rtuens the possible values for the TCF version
+func TCFVersions() []TCFVersionValue {
+	return []TCFVersionValue{
+		TCFVersionErr,
+		TCFVersionV1,
+		TCFVersionV2,
+	}
+}
+
+// TCFVersionToValue takes an integer TCF version and returns the corresponding TCFVersionValue
+func TCFVersionToValue(version int) TCFVersionValue {
+	switch {
+	case version == 1:
+		return TCFVersionV1
+	case version == 2:
+		return TCFVersionV2
+	}
+	return TCFVersionErr
+}
+
 // MetricsEngine is a generic interface to record PBS metrics into the desired backend
 // The first three metrics function fire off once per incoming request, so total metrics
 // will equal the total number of incoming requests. The remaining 5 fire off per outgoing
@@ -276,4 +305,5 @@ type MetricsEngine interface {
 	RecordPrebidCacheRequestTime(success bool, length time.Duration)
 	RecordRequestQueueTime(success bool, requestType RequestType, length time.Duration)
 	RecordTimeoutNotice(sucess bool)
+	RecordTCFReq(version TCFVersionValue)
 }
diff --git a/pbsmetrics/metrics_mock.go b/pbsmetrics/metrics_mock.go
index 482cbf24..a6d36a72 100644
--- a/pbsmetrics/metrics_mock.go
+++ b/pbsmetrics/metrics_mock.go
@@ -106,3 +106,8 @@ func (me *MetricsEngineMock) RecordRequestQueueTime(success bool, requestType Re
 func (me *MetricsEngineMock) RecordTimeoutNotice(success bool) {
 	me.Called(success)
 }
+
+// RecordTCFReq mock
+func (me *MetricsEngineMock) RecordTCFReq(version TCFVersionValue) {
+	me.Called(version)
+}
diff --git a/pbsmetrics/prometheus/preload.go b/pbsmetrics/prometheus/preload.go
index 11e6bdc1..19f4f225 100644
--- a/pbsmetrics/prometheus/preload.go
+++ b/pbsmetrics/prometheus/preload.go
@@ -99,6 +99,11 @@ func preloadLabelValues(m *Metrics) {
 		requestTypeLabel:   {string(pbsmetrics.ReqTypeVideo)},
 		requestStatusLabel: {requestSuccessLabel, requestRejectLabel},
 	})
+
+	preloadLabelValuesForCounter(m.tcfVersion, map[string][]string{
+		versionLabel: tcfVersionsAsString(),
+		sourceLabel:  {string(sourceRequest)},
+	})
 }
 
 func preloadLabelValuesForCounter(counter *prometheus.CounterVec, labelsWithValues map[string][]string) {
diff --git a/pbsmetrics/prometheus/prometheus.go b/pbsmetrics/prometheus/prometheus.go
index e385b044..bf854746 100644
--- a/pbsmetrics/prometheus/prometheus.go
+++ b/pbsmetrics/prometheus/prometheus.go
@@ -28,7 +28,8 @@ type Metrics struct {
 	requestsWithoutCookie        *prometheus.CounterVec
 	storedImpressionsCacheResult *prometheus.CounterVec
 	storedRequestCacheResult     *prometheus.CounterVec
-	timeout_notifications        *prometheus.CounterVec
+	timeoutNotifications         *prometheus.CounterVec
+	tcfVersion                   *prometheus.CounterVec
 
 	// Adapter Metrics
 	adapterBids          *prometheus.CounterVec
@@ -63,6 +64,7 @@ const (
 	requestStatusLabel   = "request_status"
 	requestTypeLabel     = "request_type"
 	successLabel         = "success"
+	versionLabel         = "version"
 )
 
 const (
@@ -85,6 +87,11 @@ const (
 	requestFailed     = "failed"
 )
 
+const (
+	sourceLabel   = "source"
+	sourceRequest = "request"
+)
+
 // NewMetrics initializes a new Prometheus metrics instance with preloaded label values.
 func NewMetrics(cfg config.PrometheusMetrics) *Metrics {
 	requestTimeBuckets := []float64{0.05, 0.1, 0.15, 0.20, 0.25, 0.3, 0.4, 0.5, 0.75, 1}
@@ -153,11 +160,16 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics {
 		"Count of stored request cache requests attempts by hits or miss.",
 		[]string{cacheResultLabel})
 
-	metrics.timeout_notifications = newCounter(cfg, metrics.Registry,
+	metrics.timeoutNotifications = newCounter(cfg, metrics.Registry,
 		"timeout_notification",
 		"Count of timeout notifications triggered, and if they were successfully sent.",
 		[]string{successLabel})
 
+	metrics.tcfVersion = newCounter(cfg, metrics.Registry,
+		"privacy_tcf",
+		"Count of TCF versions for requests where GDPR was enforced.",
+		[]string{versionLabel, sourceLabel})
+
 	metrics.adapterBids = newCounter(cfg, metrics.Registry,
 		"adapter_bids",
 		"Count of bids labeled by adapter and markup delivery type (adm or nurl).",
@@ -412,12 +424,19 @@ func (m *Metrics) RecordRequestQueueTime(success bool, requestType pbsmetrics.Re
 
 func (m *Metrics) RecordTimeoutNotice(success bool) {
 	if success {
-		m.timeout_notifications.With(prometheus.Labels{
+		m.timeoutNotifications.With(prometheus.Labels{
 			successLabel: requestSuccessful,
 		}).Inc()
 	} else {
-		m.timeout_notifications.With(prometheus.Labels{
+		m.timeoutNotifications.With(prometheus.Labels{
 			successLabel: requestFailed,
 		}).Inc()
 	}
 }
+
+func (m *Metrics) RecordTCFReq(version pbsmetrics.TCFVersionValue) {
+	m.tcfVersion.With(prometheus.Labels{
+		versionLabel: string(version),
+		sourceLabel:  sourceRequest,
+	}).Inc()
+}
diff --git a/pbsmetrics/prometheus/type_conversion.go b/pbsmetrics/prometheus/type_conversion.go
index 8294ede0..55a7092e 100644
--- a/pbsmetrics/prometheus/type_conversion.go
+++ b/pbsmetrics/prometheus/type_conversion.go
@@ -76,3 +76,12 @@ func requestTypesAsString() []string {
 	}
 	return valuesAsString
 }
+
+func tcfVersionsAsString() []string {
+	values := pbsmetrics.TCFVersions()
+	valuesAsString := make([]string, len(values))
+	for i, v := range values {
+		valuesAsString[i] = string(v)
+	}
+	return valuesAsString
+}
diff --git a/prebid/prebid.go b/prebid/prebid.go
deleted file mode 100644
index 68c4a48c..00000000
--- a/prebid/prebid.go
+++ /dev/null
@@ -1,82 +0,0 @@
-package prebid
-
-import (
-	"net"
-	"net/http"
-	"strings"
-)
-
-var xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For")
-var xRealIP = http.CanonicalHeaderKey("X-Real-IP")
-var xForwardedProto = http.CanonicalHeaderKey("X-Forwarded-Proto")
-
-// IsSecure attempts to detect whether the request is https
-func IsSecure(r *http.Request) bool {
-	// lowercase for case-insensitive match for X-Forwarded-Proto header
-	if strings.ToLower(r.Header.Get(xForwardedProto)) == "https" {
-		return true
-	}
-	// ensure that URL.Scheme is lowercase (it should be "https")
-	if strings.ToLower(r.URL.Scheme) == "https" {
-		return true
-	}
-	// use strings.HasPrefix because a valid example is "HTTP/1.0"
-	if strings.HasPrefix(r.Proto, "HTTPS") {
-		return true
-	}
-	// check if TLS is not-nil as a final fallback
-	if r.TLS != nil {
-		return true
-	}
-	return false
-}
-
-// GetIP will attempt to get the IP Address by first checking headers
-// and then falling back on the RemoteAddr
-func GetIP(r *http.Request) string {
-	// first check headers
-	if ip := GetForwardedIP(r); ip != "" {
-		return ip
-	}
-	// next try to parse the RemoteAddr.
-	// if err is not nil then weird hosts might appear as the ip: https://github.com/golang/go/issues/14827
-	if ip, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
-		return ip
-	}
-	return ""
-}
-
-// GetForwardedIP will return back X-Forwarded-For or X-Real-IP (if set)
-func GetForwardedIP(r *http.Request) string {
-	// first attempt to parse X-Forwarded-For
-	if ip := getForwardedFor(r); ip != "" {
-		return ip
-	}
-	// if we don't have X-Forwarded-For then try X-Real-IP
-	if ip := getRealIP(r); ip != "" {
-		return ip
-	}
-	return ""
-}
-
-// getForwardedFor will attempt to parse the X-Forwarded-For header
-func getForwardedFor(r *http.Request) string {
-	if xff := r.Header.Get(xForwardedFor); xff != "" {
-		// X-Forwarded-For: client1, proxy1, proxy2
-		i := strings.Index(xff, ", ")
-		if i == -1 {
-			i = len(xff)
-		}
-		return xff[:i]
-	}
-	return ""
-}
-
-// getRealIP will attempt to parse the X-Real-IP header
-// Header.Get is case-insensitive
-func getRealIP(r *http.Request) string {
-	if xrip := r.Header.Get(xRealIP); xrip != "" {
-		return xrip
-	}
-	return ""
-}
diff --git a/router/router.go b/router/router.go
index 045c86ef..30936705 100644
--- a/router/router.go
+++ b/router/router.go
@@ -188,6 +188,7 @@ func New(cfg *config.Configuration, rateConvertor *currencies.RateConverter) (r
 
 	generalHttpClient := &http.Client{
 		Transport: &http.Transport{
+			MaxConnsPerHost:     cfg.Client.MaxConnsPerHost,
 			MaxIdleConns:        cfg.Client.MaxIdleConns,
 			MaxIdleConnsPerHost: cfg.Client.MaxIdleConnsPerHost,
 			IdleConnTimeout:     time.Duration(cfg.Client.IdleConnTimeout) * time.Second,
@@ -197,6 +198,7 @@ func New(cfg *config.Configuration, rateConvertor *currencies.RateConverter) (r
 
 	cacheHttpClient := &http.Client{
 		Transport: &http.Transport{
+			MaxConnsPerHost:     cfg.CacheClient.MaxConnsPerHost,
 			MaxIdleConns:        cfg.CacheClient.MaxIdleConns,
 			MaxIdleConnsPerHost: cfg.CacheClient.MaxIdleConnsPerHost,
 			IdleConnTimeout:     time.Duration(cfg.CacheClient.IdleConnTimeout) * time.Second,
diff --git a/static/bidder-info/adman.yaml b/static/bidder-info/adman.yaml
new file mode 100644
index 00000000..932ef2e4
--- /dev/null
+++ b/static/bidder-info/adman.yaml
@@ -0,0 +1,11 @@
+maintainer:
+  email: "prebid@admanmedia.com"
+capabilities:
+  app:
+    mediaTypes:
+      - banner
+      - video
+  site:
+    mediaTypes:
+      - banner
+      - video
\ No newline at end of file
diff --git a/static/bidder-params/adman.json b/static/bidder-params/adman.json
new file mode 100644
index 00000000..90021e2c
--- /dev/null
+++ b/static/bidder-params/adman.json
@@ -0,0 +1,15 @@
+{
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "title": "Adman Adapter Params",
+    "description": "A schema which validates params accepted by the Adman adapter",
+  
+    "type": "object",
+    "properties": {
+      "TagID": {
+        "type": "string",
+        "description": "An ID which identifies the adman ad tag"
+      }
+    },
+    "required" : [ "TagID" ]
+  }
+  
\ No newline at end of file
diff --git a/static/bidder-params/kubient.json b/static/bidder-params/kubient.json
index a75dd734..9b975289 100644
--- a/static/bidder-params/kubient.json
+++ b/static/bidder-params/kubient.json
@@ -3,5 +3,11 @@
   "title": "Kubient Adapter Params",
   "description": "A schema which validates params accepted by the Kubient adapter",
   "type": "object",
-  "properties": {  }
+  "properties": {
+    "zoneid": {
+      "type": "string",
+      "description": "Zone ID identifies Kubient placement ID.",
+      "minLength": 1
+    }
+  }
 }
diff --git a/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go
index 5657c8b7..f1f643af 100755
--- a/usersync/usersyncers/syncer.go
+++ b/usersync/usersyncers/syncer.go
@@ -9,6 +9,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/adform"
 	"github.com/prebid/prebid-server/adapters/adkernel"
 	"github.com/prebid/prebid-server/adapters/adkernelAdn"
+	"github.com/prebid/prebid-server/adapters/adman"
 	"github.com/prebid/prebid-server/adapters/admixer"
 	"github.com/prebid/prebid-server/adapters/adocean"
 	"github.com/prebid/prebid-server/adapters/adpone"
@@ -84,6 +85,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderAdform, adform.NewAdformSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderAdkernel, adkernel.NewAdkernelSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderAdkernelAdn, adkernelAdn.NewAdkernelAdnSyncer)
+	insertIntoMap(cfg, syncers, openrtb_ext.BidderAdman, adman.NewAdmanSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderAdmixer, admixer.NewAdmixerSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderAdOcean, adocean.NewAdOceanSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderAdpone, adpone.NewadponeSyncer)
diff --git a/util/httputil/httputil.go b/util/httputil/httputil.go
new file mode 100644
index 00000000..46151277
--- /dev/null
+++ b/util/httputil/httputil.go
@@ -0,0 +1,99 @@
+package httputil
+
+import (
+	"net"
+	"net/http"
+	"strings"
+
+	"github.com/prebid/prebid-server/util/iputil"
+)
+
+var (
+	trueClientIP    = http.CanonicalHeaderKey("True-Client-IP")
+	xForwardedProto = http.CanonicalHeaderKey("X-Forwarded-Proto")
+	xForwardedFor   = http.CanonicalHeaderKey("X-Forwarded-For")
+	xRealIP         = http.CanonicalHeaderKey("X-Real-IP")
+)
+
+const (
+	https = "https"
+)
+
+// IsSecure determines if a http request uses https.
+func IsSecure(r *http.Request) bool {
+	if strings.EqualFold(r.Header.Get(xForwardedProto), https) {
+		return true
+	}
+
+	if strings.EqualFold(r.URL.Scheme, https) {
+		return true
+	}
+
+	if r.TLS != nil {
+		return true
+	}
+
+	return false
+}
+
+// FindIP returns the first ip address found in the http request matching the predicate v.
+func FindIP(r *http.Request, v iputil.IPValidator) (net.IP, iputil.IPVersion) {
+	if ip, ver := findTrueClientIP(r, v); ip != nil {
+		return ip, ver
+	}
+
+	if ip, ver := findForwardedFor(r, v); ip != nil {
+		return ip, ver
+	}
+
+	if ip, ver := findRealIP(r, v); ip != nil {
+		return ip, ver
+	}
+
+	if ip, ver := findRemoteAddr(r, v); ip != nil {
+		return ip, ver
+	}
+
+	return nil, iputil.IPvUnknown
+}
+
+func findTrueClientIP(r *http.Request, v iputil.IPValidator) (net.IP, iputil.IPVersion) {
+	if value := r.Header.Get(trueClientIP); value != "" {
+		value = strings.TrimSpace(value)
+		if ip, ver := iputil.ParseIP(value); ip != nil && v.IsValid(ip, ver) {
+			return ip, ver
+		}
+	}
+	return nil, iputil.IPvUnknown
+}
+
+func findForwardedFor(r *http.Request, v iputil.IPValidator) (net.IP, iputil.IPVersion) {
+	if value := r.Header.Get(xForwardedFor); value != "" {
+		for _, p := range strings.Split(value, ",") {
+			p = strings.TrimSpace(p)
+			if ip, ver := iputil.ParseIP(p); ip != nil && v.IsValid(ip, ver) {
+				return ip, ver
+			}
+		}
+	}
+	return nil, iputil.IPvUnknown
+}
+
+func findRealIP(r *http.Request, v iputil.IPValidator) (net.IP, iputil.IPVersion) {
+	if value := r.Header.Get(xRealIP); value != "" {
+		value = strings.TrimSpace(value)
+		if ip, ver := iputil.ParseIP(value); ip != nil && v.IsValid(ip, ver) {
+			return ip, ver
+		}
+	}
+	return nil, iputil.IPvUnknown
+}
+
+func findRemoteAddr(r *http.Request, v iputil.IPValidator) (net.IP, iputil.IPVersion) {
+	if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
+		if ip, ver := iputil.ParseIP(host); ip != nil && v.IsValid(ip, ver) {
+			return ip, ver
+		}
+	}
+	return nil, iputil.IPvUnknown
+}
diff --git a/util/iputil/parse.go b/util/iputil/parse.go
new file mode 100644
index 00000000..bcb00760
--- /dev/null
+++ b/util/iputil/parse.go
@@ -0,0 +1,27 @@
+package iputil
+
+import (
+	"net"
+	"strings"
+)
+
+// IPVersion is the numerical version of the IP address spec (4 or 6).
+type IPVersion int
+
+// IP address versions.
+const (
+	IPvUnknown IPVersion = 0
+	IPv4       IPVersion = 4
+	IPv6       IPVersion = 6
+)
+
+// ParseIP parses v as an ip address returning the result and version, or nil and unknown if invalid.
+func ParseIP(v string) (net.IP, IPVersion) {
+	if ip := net.ParseIP(v); ip != nil {
+		if strings.ContainsRune(v, ':') {
+			return ip, IPv6
+		}
+		return ip, IPv4
+	}
+	return nil, IPvUnknown
+}
diff --git a/util/iputil/validator.go b/util/iputil/validator.go
new file mode 100644
index 00000000..e4b822f0
--- /dev/null
+++ b/util/iputil/validator.go
@@ -0,0 +1,48 @@
+package iputil
+
+import (
+	"net"
+)
+
+// IPValidator is the interface for validating an ip address and version.
+type IPValidator interface {
+	// IsValid returns true when an IP address is determined to be valid.
+	IsValid(net.IP, IPVersion) bool
+}
+
+// PublicNetworkIPValidator validates an ip address which is not contained in the list of known private networks.
+type PublicNetworkIPValidator struct {
+	IPv4PrivateNetworks []net.IPNet
+	IPv6PrivateNetworks []net.IPNet
+}
+
+// IsValid implements the IPValidator interface.
+func (v PublicNetworkIPValidator) IsValid(ip net.IP, ver IPVersion) bool {
+	var privateNetworks []net.IPNet
+	switch ver {
+	case IPv4:
+		privateNetworks = v.IPv4PrivateNetworks
+	case IPv6:
+		privateNetworks = v.IPv6PrivateNetworks
+	default:
+		return false
+	}
+
+	for _, ipNet := range privateNetworks {
+		if ipNet.Contains(ip) {
+			return false
+		}
+	}
+
+	return true
+}
+
+// VersionIPValidator validates an ip address based on the desired ip version.
+type VersionIPValidator struct {
+	Version IPVersion
+}
+
+// IsValid implements the IPValidator interface.
+func (v VersionIPValidator) IsValid(ip net.IP, ver IPVersion) bool {
+	return ver == v.Version
+}
diff --git a/util/maputil/maputil.go b/util/maputil/maputil.go
new file mode 100644
index 00000000..0d1d7dbb
--- /dev/null
+++ b/util/maputil/maputil.go
@@ -0,0 +1,21 @@
+package maputil
+
+// ReadEmbeddedMap reads element k from the map m as a map[string]interface{}.
+func ReadEmbeddedMap(m map[string]interface{}, k string) (map[string]interface{}, bool) {
+	if v, ok := m[k]; ok {
+		vCasted, ok := v.(map[string]interface{})
+		return vCasted, ok
+	}
+
+	return nil, false
+}
+
+// ReadEmbeddedSlice reads element k from the map m as a []interface{}.
+func ReadEmbeddedSlice(m map[string]interface{}, k string) ([]interface{}, bool) {
+	if v, ok := m[k]; ok {
+		vCasted, ok := v.([]interface{})
+		return vCasted, ok
+	}
+
+	return nil, false
+}


Test patch:
diff --git a/adapters/adapterstest/test_json.go b/adapters/adapterstest/test_json.go
index 8fdb9c5d..a25a4f19 100644
--- a/adapters/adapterstest/test_json.go
+++ b/adapters/adapterstest/test_json.go
@@ -164,14 +164,16 @@ type httpRequest struct {
 }
 
 type httpResponse struct {
-	Status int             `json:"status"`
-	Body   json.RawMessage `json:"body"`
+	Status  int             `json:"status"`
+	Body    json.RawMessage `json:"body"`
+	Headers http.Header     `json:"headers"`
 }
 
 func (resp *httpResponse) ToResponseData(t *testing.T) *adapters.ResponseData {
 	return &adapters.ResponseData{
 		StatusCode: resp.Status,
 		Body:       resp.Body,
+		Headers:    resp.Headers,
 	}
 }
 
diff --git a/adapters/adman/adman_test.go b/adapters/adman/adman_test.go
new file mode 100644
index 00000000..4ef88933
--- /dev/null
+++ b/adapters/adman/adman_test.go
@@ -0,0 +1,12 @@
+package adman
+
+import (
+	"testing"
+
+	"github.com/prebid/prebid-server/adapters/adapterstest"
+)
+
+func TestJsonSamples(t *testing.T) {
+	admanAdapter := NewAdmanBidder("http://pub.admanmedia.com/?c=o&m=ortb")
+	adapterstest.RunJSONBidderTest(t, "admantest", admanAdapter)
+}
diff --git a/adapters/adman/admantest/exemplary/simple-banner.json b/adapters/adman/admantest/exemplary/simple-banner.json
new file mode 100644
index 00000000..8bbe16aa
--- /dev/null
+++ b/adapters/adman/admantest/exemplary/simple-banner.json
@@ -0,0 +1,134 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+        {
+            "id": "test-imp-id",
+            "banner": {
+                "format": [
+                    {
+                        "w": 300,
+                        "h": 250
+                    },
+                    {
+                        "w": 300,
+                        "h": 600
+                    }
+                ]
+            },
+            "tagid": "16",
+            "ext": {
+                "bidder": {
+                    "TagID": "16"
+                }
+            }
+        }
+    ],
+    "app": {
+        "id": "1",
+        "bundle": "com.wls.testwlsapplication"
+    },
+    "device": {
+        "ip": "123.123.123.123",
+        "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx"
+    }
+},
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://pub.admanmedia.com/?c=o&m=ortb",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+              {
+                  "id": "test-imp-id",
+                  "banner": {
+                      "format": [
+                          {
+                              "w": 300,
+                              "h": 250
+                          },
+                          {
+                              "w": 300,
+                              "h": 600
+                          }
+                      ]
+                  },
+                  "tagid": "16",
+                  "ext": {
+                      "bidder": {
+                          "TagID": "16"
+                      }
+                  }
+              }
+          ],
+          "app": {
+              "id": "1",
+              "bundle": "com.wls.testwlsapplication"
+          },
+          "device": {
+              "ip": "123.123.123.123",
+              "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx"
+          }
+      }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+              {
+                  "bid": [
+                      {
+                          "id": "test_bid_id",
+                          "impid": "test-imp-id",
+                          "price": 0.27543,
+                          "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"https://pub.admanmedia.com/?c=o&m=adm&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                          "cid": "test_cid",
+                          "crid": "test_crid",
+                          "dealid": "test_dealid",
+                          "w": 300,
+                          "h": 250,
+                          "ext": {
+                              "prebid": {
+                                  "type": "banner"
+                              }
+                          }
+                      }
+                  ],
+                  "seat": "adman"
+              }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+
+  "expectedBidResponses": [
+    {
+      "bids":[
+        {
+          "bid":  {
+            "id": "test_bid_id",
+            "impid": "test-imp-id",
+            "price": 0.27543,
+            "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"https://pub.admanmedia.com/?c=o&m=adm&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+            "cid": "test_cid",
+            "crid": "test_crid",
+            "dealid": "test_dealid",
+            "w": 300,
+            "h": 250,
+            "ext": {
+              "prebid": {
+                "type": "banner"
+              }
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/adman/admantest/exemplary/simple-video.json b/adapters/adman/admantest/exemplary/simple-video.json
new file mode 100644
index 00000000..159a30a9
--- /dev/null
+++ b/adapters/adman/admantest/exemplary/simple-video.json
@@ -0,0 +1,119 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "device": {
+      "ip": "123.123.123.123",
+      "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx"
+    },
+    "app": {
+      "id": "1",
+      "bundle": "com.wls.testwlsapplication"
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "video": {
+          "mimes": ["video/mp4"],
+          "protocols": [2, 5],
+          "w": 1024,
+          "h": 576
+        },
+        "ext": {
+          "bidder": {
+            "TagID": "22"
+          }
+        }
+      }
+    ]
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://pub.admanmedia.com/?c=o&m=ortb",
+        "body": {
+          "id": "test-request-id",
+          "device": {
+            "ip": "123.123.123.123",
+            "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx"
+          },
+          "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+          },
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "video": {
+                "mimes": ["video/mp4"],
+                "protocols": [2, 5],
+                "w": 1024,
+                "h": 576
+              },
+              "tagid": "22",
+              "ext": {
+                "bidder": {
+                  "TagID": "22"
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+              {
+                  "bid": [
+                      {
+                          "id": "test_bid_id",
+                          "impid": "test-imp-id",
+                          "price": 0.27543,
+                          "adm": "<VAST version=\"3.0\"><Ad id=\"20001\" sequence=\"1\"><InLine><AdSystem version=\"4.0\"><![CDATA[iabtechlab]]></AdSystem><AdTitle><![CDATA[Inline Simple Ad]]></AdTitle><Impression><![CDATA[]]></Impression><Creatives><Creative id=\"5480\" sequence=\"1\"><Linear><Duration>00:01:00</Duration><MediaFiles><MediaFile id=\"5246\" delivery=\"progressive\" type=\"video/mp4\" bitrate=\"600\" width=\"640\" height=\"360\" minBitrate=\"500\" maxBitrate=\"700\" scalable=\"1\" maintainAspectRatio=\"1\" codec=\"0\"><![CDATA[https://s0.2mdn.net/4253510/google_ddm_animation_480P.mp4]]></MediaFile></MediaFiles><VideoClicks><ClickThrough id=\"blog\"><![CDATA[https://admanmedia.com]]></ClickThrough></VideoClicks></Linear></Creative></Creatives></InLine></Ad></VAST>",
+                          "cid": "test_cid",
+                          "crid": "test_crid",
+                          "dealid": "test_dealid",
+                          "ext": {
+                              "prebid": {
+                                  "type": "video"
+                              }
+                          }
+                      }
+                  ],
+                  "seat": "adman"
+              }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+
+
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "test_bid_id",
+                          "impid": "test-imp-id",
+                          "price": 0.27543,
+                          "adm": "<VAST version=\"3.0\"><Ad id=\"20001\" sequence=\"1\"><InLine><AdSystem version=\"4.0\"><![CDATA[iabtechlab]]></AdSystem><AdTitle><![CDATA[Inline Simple Ad]]></AdTitle><Impression><![CDATA[]]></Impression><Creatives><Creative id=\"5480\" sequence=\"1\"><Linear><Duration>00:01:00</Duration><MediaFiles><MediaFile id=\"5246\" delivery=\"progressive\" type=\"video/mp4\" bitrate=\"600\" width=\"640\" height=\"360\" minBitrate=\"500\" maxBitrate=\"700\" scalable=\"1\" maintainAspectRatio=\"1\" codec=\"0\"><![CDATA[https://s0.2mdn.net/4253510/google_ddm_animation_480P.mp4]]></MediaFile></MediaFiles><VideoClicks><ClickThrough id=\"blog\"><![CDATA[https://admanmedia.com]]></ClickThrough></VideoClicks></Linear></Creative></Creatives></InLine></Ad></VAST>",
+                          "cid": "test_cid",
+                          "crid": "test_crid",
+                          "dealid": "test_dealid",
+                          "ext": {
+                              "prebid": {
+                                  "type": "video"
+                              }
+                          }
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/adman/admantest/exemplary/simple-web-banner.json b/adapters/adman/admantest/exemplary/simple-web-banner.json
new file mode 100644
index 00000000..0ceaac7c
--- /dev/null
+++ b/adapters/adman/admantest/exemplary/simple-web-banner.json
@@ -0,0 +1,133 @@
+{
+    "mockBidRequest": {
+      "id": "test-request-id",
+      "imp": [
+          {
+              "id": "test-imp-id",
+              "banner": {
+                  "format": [
+                      {
+                          "w": 300,
+                          "h": 250
+                      },
+                      {
+                          "w": 300,
+                          "h": 600
+                      }
+                  ]
+              },
+              "tagid": "1",
+              "ext": {
+                  "bidder": {
+                      "TagID": "1"
+                  }
+              }
+          }
+      ],
+      "site": {
+          "id": "1",
+          "domain": "test.com"
+      },
+      "device": {
+          "ip": "123.123.123.123"
+      }
+  },
+  
+    "httpCalls": [
+      {
+        "expectedRequest": {
+          "uri": "http://pub.admanmedia.com/?c=o&m=ortb",
+          "body": {
+            "id": "test-request-id",
+            "imp": [
+                {
+                    "id": "test-imp-id",
+                    "banner": {
+                        "format": [
+                            {
+                                "w": 300,
+                                "h": 250
+                            },
+                            {
+                                "w": 300,
+                                "h": 600
+                            }
+                        ]
+                    },
+                    "tagid": "1",
+                    "ext": {
+                        "bidder": {
+                            "TagID": "1"
+                        }
+                    }
+                }
+            ],
+            "site": {
+                "id": "1",
+                "domain": "test.com"
+            },
+            "device": {
+                "ip": "123.123.123.123"
+            }
+        }
+        },
+        "mockResponse": {
+          "status": 200,
+          "body": {
+            "id": "test-request-id",
+            "seatbid": [
+                {
+                    "bid": [
+                        {
+                            "id": "test_bid_id",
+                            "impid": "test-imp-id",
+                            "price": 0.27543,
+                            "adm": "<iframe id=\"adm-banner-1\" width=\"468\" height=\"60\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"https://pub.admanmedia.com/?c=o&m=adm&k=bc2d316f39931a07d9a8dd249bf85fc0\"></iframe>",
+                            "cid": "test_cid",
+                            "crid": "test_crid",
+                            "dealid": "test_dealid",
+                            "w": 468,
+                            "h": 60,
+                            "ext": {
+                                "prebid": {
+                                    "type": "banner"
+                                }
+                            }
+                        }
+                    ],
+                    "seat": "adman"
+                }
+            ],
+            "cur": "USD"
+          }
+        }
+      }
+    ],
+  
+    "expectedBidResponses": [
+      {
+        "bids":[
+          {
+            "bid":  {
+                "id": "test_bid_id",
+                "impid": "test-imp-id",
+                "price": 0.27543,
+                "adm": "<iframe id=\"adm-banner-1\" width=\"468\" height=\"60\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"https://pub.admanmedia.com/?c=o&m=adm&k=bc2d316f39931a07d9a8dd249bf85fc0\"></iframe>",
+                "cid": "test_cid",
+                "crid": "test_crid",
+                "dealid": "test_dealid",
+                "w": 468,
+                "h": 60,
+                "ext": {
+                    "prebid": {
+                        "type": "banner"
+                    }
+                }
+            },
+            "type": "banner"
+          }
+        ]
+      }
+    ]
+  }
+  
\ No newline at end of file
diff --git a/adapters/adman/admantest/params/banner.json b/adapters/adman/admantest/params/banner.json
new file mode 100644
index 00000000..03fa8f3f
--- /dev/null
+++ b/adapters/adman/admantest/params/banner.json
@@ -0,0 +1,3 @@
+{
+    "TagID": "16"
+}
\ No newline at end of file
diff --git a/adapters/adman/admantest/params/race/banner.json b/adapters/adman/admantest/params/race/banner.json
new file mode 100644
index 00000000..03fa8f3f
--- /dev/null
+++ b/adapters/adman/admantest/params/race/banner.json
@@ -0,0 +1,3 @@
+{
+    "TagID": "16"
+}
\ No newline at end of file
diff --git a/adapters/adman/admantest/params/race/video.json b/adapters/adman/admantest/params/race/video.json
new file mode 100644
index 00000000..e776c928
--- /dev/null
+++ b/adapters/adman/admantest/params/race/video.json
@@ -0,0 +1,3 @@
+{
+    "TagID": "22"
+}
\ No newline at end of file
diff --git a/adapters/adman/admantest/params/video.json b/adapters/adman/admantest/params/video.json
new file mode 100644
index 00000000..e776c928
--- /dev/null
+++ b/adapters/adman/admantest/params/video.json
@@ -0,0 +1,3 @@
+{
+    "TagID": "22"
+}
\ No newline at end of file
diff --git a/adapters/adman/admantest/supplemental/bad-imp-ext.json b/adapters/adman/admantest/supplemental/bad-imp-ext.json
new file mode 100644
index 00000000..db3c8de5
--- /dev/null
+++ b/adapters/adman/admantest/supplemental/bad-imp-ext.json
@@ -0,0 +1,42 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+        {
+            "id": "test-imp-id",
+            "banner": {
+                "format": [
+                    {
+                        "w": 300,
+                        "h": 250
+                    },
+                    {
+                        "w": 300,
+                        "h": 600
+                    }
+                ]
+            },
+            "tagid": "16",
+            "ext": {
+                "adman": {
+                    "TagID": "16"
+                }
+            }
+        }
+    ],
+    "app": {
+        "id": "1",
+        "bundle": "com.wls.testwlsapplication"
+    },
+    "device": {
+        "ip": "123.123.123.123",
+        "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx"
+    }
+},
+"expectedMakeRequestsErrors": [
+  {
+    "value": "unexpected end of JSON input",
+    "comparison": "literal"
+  }
+]
+}
diff --git a/adapters/adman/admantest/supplemental/bad_response.json b/adapters/adman/admantest/supplemental/bad_response.json
new file mode 100644
index 00000000..d5a28c74
--- /dev/null
+++ b/adapters/adman/admantest/supplemental/bad_response.json
@@ -0,0 +1,85 @@
+{
+	"mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "tagid": "17",
+                "ext": {
+                    "bidder": {
+                        "TagID": "17"
+                    }
+                }
+            }
+        ],
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "device": {
+            "ip": "123.123.123.123",
+            "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+        }
+    },
+	"httpCalls": [{
+		"expectedRequest": {
+			"uri": "http://pub.admanmedia.com/?c=o&m=ortb",
+			"body": {
+                "id": "test-request-id",
+                "imp": [
+                    {
+                        "id": "test-imp-id",
+                        "banner": {
+                            "format": [
+                                {
+                                    "w": 300,
+                                    "h": 250
+                                },
+                                {
+                                    "w": 300,
+                                    "h": 600
+                                }
+                            ]
+                        },
+                        "tagid": "17",
+                        "ext": {
+                            "bidder": {
+                                "TagID": "17"
+                            }
+                        }
+                    }
+                ],
+                "app": {
+                    "id": "1",
+                    "bundle": "com.wls.testwlsapplication"
+                },
+                "device": {
+                    "ip": "123.123.123.123",
+                    "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+                }
+            }
+		},
+		"mockResponse": {
+			"status": 200,
+			"body": ""
+        }
+    }],
+    "expectedMakeBidsErrors": [
+        {
+          "value": "json: cannot unmarshal string into Go value of type openrtb.BidResponse",
+          "comparison": "literal"
+        }
+    ]
+}
diff --git a/adapters/adman/admantest/supplemental/no-imp-ext-1.json b/adapters/adman/admantest/supplemental/no-imp-ext-1.json
new file mode 100644
index 00000000..8fad5ba5
--- /dev/null
+++ b/adapters/adman/admantest/supplemental/no-imp-ext-1.json
@@ -0,0 +1,39 @@
+{
+    "mockBidRequest": {
+      "id": "test-request-id",
+      "imp": [
+          {
+              "id": "test-imp-id",
+              "banner": {
+                  "format": [
+                      {
+                          "w": 300,
+                          "h": 250
+                      },
+                      {
+                          "w": 300,
+                          "h": 600
+                      }
+                  ]
+              },
+              "tagid": "16",
+              "ext": ""
+          }
+      ],
+      "app": {
+          "id": "1",
+          "bundle": "com.wls.testwlsapplication"
+      },
+      "device": {
+          "ip": "123.123.123.123",
+          "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx"
+      }
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "json: cannot unmarshal string into Go value of type adapters.ExtImpBidder",
+      "comparison": "literal"
+    }
+  ]
+  }
+  
\ No newline at end of file
diff --git a/adapters/adman/admantest/supplemental/no-imp-ext-2.json b/adapters/adman/admantest/supplemental/no-imp-ext-2.json
new file mode 100644
index 00000000..337dfd04
--- /dev/null
+++ b/adapters/adman/admantest/supplemental/no-imp-ext-2.json
@@ -0,0 +1,39 @@
+{
+    "mockBidRequest": {
+      "id": "test-request-id",
+      "imp": [
+          {
+              "id": "test-imp-id",
+              "banner": {
+                  "format": [
+                      {
+                          "w": 300,
+                          "h": 250
+                      },
+                      {
+                          "w": 300,
+                          "h": 600
+                      }
+                  ]
+              },
+              "tagid": "16",
+              "ext": {}
+          }
+      ],
+      "app": {
+          "id": "1",
+          "bundle": "com.wls.testwlsapplication"
+      },
+      "device": {
+          "ip": "123.123.123.123",
+          "ifa": "zxcjbzxmc-zxcbmz-zxbcz-zxczx"
+      }
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "unexpected end of JSON input",
+      "comparison": "literal"
+    }
+  ]
+  }
+  
\ No newline at end of file
diff --git a/adapters/adman/admantest/supplemental/status-204.json b/adapters/adman/admantest/supplemental/status-204.json
new file mode 100644
index 00000000..72b28bff
--- /dev/null
+++ b/adapters/adman/admantest/supplemental/status-204.json
@@ -0,0 +1,79 @@
+{
+	"mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "tagid": "17",
+                "ext": {
+                    "bidder": {
+                        "TagID": "17"
+                    }
+                }
+            }
+        ],
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "device": {
+            "ip": "123.123.123.123",
+            "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+        }
+    },
+	"httpCalls": [{
+		"expectedRequest": {
+			"uri": "http://pub.admanmedia.com/?c=o&m=ortb",
+			"body": {
+                "id": "test-request-id",
+                "imp": [
+                    {
+                        "id": "test-imp-id",
+                        "banner": {
+                            "format": [
+                                {
+                                    "w": 300,
+                                    "h": 250
+                                },
+                                {
+                                    "w": 300,
+                                    "h": 600
+                                }
+                            ]
+                        },
+                        "tagid": "17",
+                        "ext": {
+                            "bidder": {
+                                "TagID": "17"
+                            }
+                        }
+                    }
+                ],
+                "app": {
+                    "id": "1",
+                    "bundle": "com.wls.testwlsapplication"
+                },
+                "device": {
+                    "ip": "123.123.123.123",
+                    "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+                }
+            }
+		},
+		"mockResponse": {
+			"status": 204,
+			"body": {}
+		}
+	}]
+}
diff --git a/adapters/adman/admantest/supplemental/status-404.json b/adapters/adman/admantest/supplemental/status-404.json
new file mode 100644
index 00000000..043afbdc
--- /dev/null
+++ b/adapters/adman/admantest/supplemental/status-404.json
@@ -0,0 +1,85 @@
+{
+	"mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "tagid": "100000000",
+                "ext": {
+                    "bidder": {
+                        "TagID": "100000000"
+                    }
+                }
+            }
+        ],
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "device": {
+            "ip": "123.123.123.123",
+            "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+        }
+    },
+	"httpCalls": [{
+		"expectedRequest": {
+			"uri": "http://pub.admanmedia.com/?c=o&m=ortb",
+			"body": {
+                "id": "test-request-id",
+                "imp": [
+                    {
+                        "id": "test-imp-id",
+                        "banner": {
+                            "format": [
+                                {
+                                    "w": 300,
+                                    "h": 250
+                                },
+                                {
+                                    "w": 300,
+                                    "h": 600
+                                }
+                            ]
+                        },
+                        "tagid": "100000000",
+                        "ext": {
+                            "bidder": {
+                                "TagID": "100000000"
+                            }
+                        }
+                    }
+                ],
+                "app": {
+                    "id": "1",
+                    "bundle": "com.wls.testwlsapplication"
+                },
+                "device": {
+                    "ip": "123.123.123.123",
+                    "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+                }
+            }
+		},
+		"mockResponse": {
+			"status": 404,
+			"body": {}
+		}
+    }],
+    "expectedMakeBidsErrors": [
+        {
+          "value": "Unexpected status code: 404. Run with request.debug = 1 for more info",
+          "comparison": "literal"
+        }
+    ]
+}
diff --git a/adapters/adman/params_test.go b/adapters/adman/params_test.go
new file mode 100644
index 00000000..a80c2a44
--- /dev/null
+++ b/adapters/adman/params_test.go
@@ -0,0 +1,46 @@
+package adman
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+// TestValidParams makes sure that the adman schema accepts all imp.ext fields which we intend to support.
+func TestValidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json-schemas. %v", err)
+	}
+
+	for _, validParam := range validParams {
+		if err := validator.Validate(openrtb_ext.BidderAdman, json.RawMessage(validParam)); err != nil {
+			t.Errorf("Schema rejected adman params: %s", validParam)
+		}
+	}
+}
+
+// TestInvalidParams makes sure that the adman schema rejects all the imp.ext fields we don't support.
+func TestInvalidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json-schemas. %v", err)
+	}
+
+	for _, invalidParam := range invalidParams {
+		if err := validator.Validate(openrtb_ext.BidderAdman, json.RawMessage(invalidParam)); err == nil {
+			t.Errorf("Schema allowed unexpected params: %s", invalidParam)
+		}
+	}
+}
+
+var validParams = []string{
+	`{"TagID": "16"}`,
+}
+
+var invalidParams = []string{
+	`{"id": "123"}`,
+	`{"tagid": "123"}`,
+	`{"TagID": 16}`,
+}
diff --git a/adapters/adman/usersync_test.go b/adapters/adman/usersync_test.go
new file mode 100644
index 00000000..55a6e2ce
--- /dev/null
+++ b/adapters/adman/usersync_test.go
@@ -0,0 +1,35 @@
+package adman
+
+import (
+	"testing"
+	"text/template"
+
+	"github.com/prebid/prebid-server/privacy"
+	"github.com/prebid/prebid-server/privacy/ccpa"
+	"github.com/prebid/prebid-server/privacy/gdpr"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAdmanSyncer(t *testing.T) {
+	syncURL := "https://sync.admanmedia.com/pbs.gif?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir=http%3A%2F%2Flocalhost%3A8000%2Fsetuid%3Fbidder%3Dadman%26uid%3D%5BUID%5D"
+	syncURLTemplate := template.Must(
+		template.New("sync-template").Parse(syncURL),
+	)
+
+	syncer := NewAdmanSyncer(syncURLTemplate)
+	syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{
+		GDPR: gdpr.Policy{
+			Signal:  "0",
+			Consent: "ANDFJDS",
+		},
+		CCPA: ccpa.Policy{
+			Value: "1-YY",
+		},
+	})
+
+	assert.NoError(t, err)
+	assert.Equal(t, "https://sync.admanmedia.com/pbs.gif?gdpr=0&gdpr_consent=ANDFJDS&us_privacy=1-YY&redir=http%3A%2F%2Flocalhost%3A8000%2Fsetuid%3Fbidder%3Dadman%26uid%3D%5BUID%5D", syncInfo.URL)
+	assert.Equal(t, "redirect", syncInfo.Type)
+	assert.EqualValues(t, 149, syncer.GDPRVendorID())
+	assert.Equal(t, false, syncInfo.SupportCORS)
+}
diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/banner-app.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/banner-app.json
new file mode 100644
index 00000000..3ac62d90
--- /dev/null
+++ b/adapters/audienceNetwork/audienceNetworktest/exemplary/banner-app.json
@@ -0,0 +1,116 @@
+{
+    "mockBidRequest": {
+        "id": "test-req-id",
+        "imp": [{
+            "id": "test-imp-id",
+            "banner": {
+                "format": [{
+                    "w": 300,
+                    "h": 250
+                }],
+                "w": 300,
+                "h": 250
+            },
+            "ext": {
+                "bidder": {
+                    "publisherid": "123",
+                    "placementid": "456"
+                }
+            }
+        }],
+        "app": {
+            "id": "app-abc",
+            "bundle": "com.prebid"
+        },
+        "device": {
+            "ip": "152.193.6.74"
+        },
+        "user": {
+            "id": "db089de9-a62e-4861-a881-0ff15e052516",
+            "buyeruid": "v4_bidder_token"
+        },
+        "tmax": 500
+    },
+    "httpcalls": [{
+        "expectedRequest": {
+            "uri": "https://an.facebook.com/placementbid.ortb",
+            "headers": {
+                "Accept": [
+                    "application/json"
+                ],
+                "Content-Type": [
+                    "application/json;charset=utf-8"
+                ],
+                "X-Fb-Pool-Routing-Token": [
+                    "v4_bidder_token"
+                ]
+            },
+            "body": {
+                "id": "test-imp-id",
+                "imp": [{
+                    "id": "test-imp-id",
+                    "banner": {
+                        "w": -1,
+                        "h": 250
+                    },
+                    "tagid": "123_456"
+                }],
+                "app": {
+                    "id": "app-abc",
+                    "bundle": "com.prebid",
+                    "publisher": {
+                        "id": "123"
+                    }
+                },
+                "device": {
+                    "ip": "152.193.6.74"
+                },
+                "user": {
+                    "id": "db089de9-a62e-4861-a881-0ff15e052516",
+                    "buyeruid": "v4_bidder_token"
+                },
+                "tmax": 500,
+                "ext": {
+                    "authentication_id": "4e24a2b23fbfb5e41a9093b921d6cddf497c24dd5f63879038cec2ab2f27d174",
+                    "platformid": "test-platform-id"
+                }
+            }
+        },
+        "mockResponse": {
+            "status": 200,
+            "body": {
+                "id": "test-imp-id",
+                "seatbid": [{
+                    "bid": [{
+                        "id": "987",
+                        "impid": "test-imp-id",
+                        "price": 1.000000,
+                        "adm": "{\"type\":\"ID\",\"bid_id\":\"987\",\"placement_id\":\"123_456\",\"resolved_placement_id\":\"123_456\",\"sdk_version\":\"5.5.0\",\"device_id\":\"abc\",\"template\":1,\"payload\":null,\"bid_time_token\":\"v4_bidder_token=\"}",
+                        "nurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=0&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0",
+                        "lurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=${AUCTION_LOSS}&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0",
+                        "burl": "https://www.facebook.com/audiencenetwork/burl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&clearing_price=${AUCTION_PRICE}"
+                    }]
+                }],
+                "bidid": "654",
+                "cur": "USD"
+            }
+        }
+    }],
+    "expectedBidResponses": [{
+        "currency": "USD",
+        "bids": [{
+            "bid": {
+                "id": "987",
+                "impid": "test-imp-id",
+                "price": 1,
+                "adm": "{\"type\":\"ID\",\"bid_id\":\"987\",\"placement_id\":\"123_456\",\"resolved_placement_id\":\"123_456\",\"sdk_version\":\"5.5.0\",\"device_id\":\"abc\",\"template\":1,\"payload\":null,\"bid_time_token\":\"v4_bidder_token=\"}",
+                "adid": "987",
+                "crid": "987",
+                "nurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=0&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0",
+                "lurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=${AUCTION_LOSS}&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0",
+                "burl": "https://www.facebook.com/audiencenetwork/burl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&clearing_price=${AUCTION_PRICE}"
+            },
+            "type": "banner"
+        }]
+    }]
+}
\ No newline at end of file
diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/banner.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/banner-site.json
similarity index 96%
rename from adapters/audienceNetwork/audienceNetworktest/exemplary/banner.json
rename to adapters/audienceNetwork/audienceNetworktest/exemplary/banner-site.json
index f5f92515..01bab3df 100644
--- a/adapters/audienceNetwork/audienceNetworktest/exemplary/banner.json
+++ b/adapters/audienceNetwork/audienceNetworktest/exemplary/banner-site.json
@@ -62,12 +62,6 @@
                             "tagid": "123_456"
                         }
                     ],
-                    "ext": {
-                        "appnexus": {
-                            "hb_source": 5
-                        },
-                        "prebid": {}
-                    },
                     "site": {
                         "domain": "prebid.org",
                         "page": "prebid.org",
diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/interstitial.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/interstitial.json
index bad228d5..9f563f11 100644
--- a/adapters/audienceNetwork/audienceNetworktest/exemplary/interstitial.json
+++ b/adapters/audienceNetwork/audienceNetworktest/exemplary/interstitial.json
@@ -64,12 +64,6 @@
                             "tagid": "123_456"
                         }
                     ],
-                    "ext": {
-                        "appnexus": {
-                            "hb_source": 5
-                        },
-                        "prebid": {}
-                    },
                     "site": {
                         "domain": "prebid.org",
                         "page": "prebid.org",
diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/native-1.1.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/native-1.1.json
index 9090d80d..16bed344 100644
--- a/adapters/audienceNetwork/audienceNetworktest/exemplary/native-1.1.json
+++ b/adapters/audienceNetwork/audienceNetworktest/exemplary/native-1.1.json
@@ -56,12 +56,6 @@
                             "tagid": "123_456"
                         }
                     ],
-                    "ext": {
-                        "appnexus": {
-                            "hb_source": 5
-                        },
-                        "prebid": {}
-                    },
                     "site": {
                         "domain": "prebid.org",
                         "page": "prebid.org",
diff --git a/adapters/audienceNetwork/audienceNetworktest/exemplary/video.json b/adapters/audienceNetwork/audienceNetworktest/exemplary/video.json
index 22c62f8b..5ece0f08 100644
--- a/adapters/audienceNetwork/audienceNetworktest/exemplary/video.json
+++ b/adapters/audienceNetwork/audienceNetworktest/exemplary/video.json
@@ -66,12 +66,6 @@
                             "tagid": "123_456"
                         }
                     ],
-                    "ext": {
-                        "appnexus": {
-                            "hb_source": 5
-                        },
-                        "prebid": {}
-                    },
                     "site": {
                         "domain": "prebid.org",
                         "page": "prebid.org",
diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/banner-format-only.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/banner-format-only.json
index 3edd6569..5469fefb 100644
--- a/adapters/audienceNetwork/audienceNetworktest/supplemental/banner-format-only.json
+++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/banner-format-only.json
@@ -64,12 +64,6 @@
                             "tagid": "123_456"
                         }
                     ],
-                    "ext": {
-                        "appnexus": {
-                            "hb_source": 5
-                        },
-                        "prebid": {}
-                    },
                     "site": {
                         "domain": "prebid.org",
                         "page": "prebid.org",
diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-adm.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-adm.json
new file mode 100644
index 00000000..f145f5fe
--- /dev/null
+++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-adm.json
@@ -0,0 +1,103 @@
+{
+    "mockBidRequest": {
+        "id": "test-req-id",
+        "imp": [{
+            "id": "test-imp-id",
+            "banner": {
+                "format": [{
+                    "w": 300,
+                    "h": 250
+                }],
+                "w": 300,
+                "h": 250
+            },
+            "ext": {
+                "bidder": {
+                    "publisherid": "123",
+                    "placementid": "456"
+                }
+            }
+        }],
+        "site": {
+            "domain": "prebid.org",
+            "page": "prebid.org"
+        },
+        "device": {
+            "ip": "152.193.6.74"
+        },
+        "user": {
+            "id": "db089de9-a62e-4861-a881-0ff15e052516",
+            "buyeruid": "v4_bidder_token"
+        },
+        "tmax": 500
+    },
+    "httpcalls": [{
+        "expectedRequest": {
+            "uri": "https://an.facebook.com/placementbid.ortb",
+            "headers": {
+                "Accept": [
+                    "application/json"
+                ],
+                "Content-Type": [
+                    "application/json;charset=utf-8"
+                ],
+                "X-Fb-Pool-Routing-Token": [
+                    "v4_bidder_token"
+                ]
+            },
+            "body": {
+                "id": "test-imp-id",
+                "imp": [{
+                    "id": "test-imp-id",
+                    "banner": {
+                        "w": -1,
+                        "h": 250
+                    },
+                    "tagid": "123_456"
+                }],
+                "site": {
+                    "domain": "prebid.org",
+                    "page": "prebid.org",
+                    "publisher": {
+                        "id": "123"
+                    }
+                },
+                "device": {
+                    "ip": "152.193.6.74"
+                },
+                "user": {
+                    "id": "db089de9-a62e-4861-a881-0ff15e052516",
+                    "buyeruid": "v4_bidder_token"
+                },
+                "tmax": 500,
+                "ext": {
+                    "authentication_id": "4e24a2b23fbfb5e41a9093b921d6cddf497c24dd5f63879038cec2ab2f27d174",
+                    "platformid": "test-platform-id"
+                }
+            }
+        },
+        "mockResponse": {
+            "status": 200,
+            "body": {
+                "id": "test-imp-id",
+                "seatbid": [{
+                    "bid": [{
+                        "id": "987",
+                        "impid": "test-imp-id",
+                        "price": 1.000000,
+                        "adm": "malformed",
+                        "nurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=0&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0",
+                        "lurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=${AUCTION_LOSS}&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0",
+                        "burl": "https://www.facebook.com/audiencenetwork/burl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&clearing_price=${AUCTION_PRICE}"
+                    }]
+                }],
+                "bidid": "654",
+                "cur": "USD"
+            }
+        }
+    }],
+    "expectedMakeBidsErrors": [{
+        "value": "invalid character 'm' looking for beginning of value",
+        "comparison": "literal"
+    }]
+}
\ No newline at end of file
diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-interstitial.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-interstitial.json
new file mode 100644
index 00000000..ad19d94c
--- /dev/null
+++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/invalid-interstitial.json
@@ -0,0 +1,40 @@
+{
+    "mockBidRequest": {
+        "id": "test-req-id",
+        "imp": [{
+            "id": "test-imp-id",
+            "video": {
+                "mimes": ["video/mp4"],
+                "minduration": 15,
+                "maxduration": 30,
+                "protocols": [2, 3, 5, 6, 7, 8],
+                "linearity": 1,
+                "w": 940,
+                "h": 560
+            },
+            "instl": 1,
+            "ext": {
+                "bidder": {
+                    "publisherid": "123",
+                    "placementid": "456"
+                }
+            }
+        }],
+        "site": {
+            "domain": "prebid.org",
+            "page": "prebid.org"
+        },
+        "device": {
+            "ip": "152.193.6.74"
+        },
+        "user": {
+            "id": "db089de9-a62e-4861-a881-0ff15e052516",
+            "buyeruid": "v4_bidder_token"
+        },
+        "tmax": 500
+    },
+    "expectedMakeRequestsErrors": [{
+        "value": "imp #test-imp-id: interstitial imps are only supported for banner",
+        "comparison": "literal"
+    }]
+}
\ No newline at end of file
diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm-bidid.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm-bidid.json
new file mode 100644
index 00000000..b57c9001
--- /dev/null
+++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm-bidid.json
@@ -0,0 +1,107 @@
+{
+    "mockBidRequest": {
+        "id": "test-req-id",
+        "imp": [{
+            "id": "test-imp-id",
+            "banner": {
+                "format": [{
+                    "w": 300,
+                    "h": 250
+                }],
+                "w": 300,
+                "h": 250
+            },
+            "ext": {
+                "bidder": {
+                    "publisherid": "123",
+                    "placementid": "456"
+                }
+            }
+        }],
+        "site": {
+            "domain": "prebid.org",
+            "page": "prebid.org"
+        },
+        "device": {
+            "ip": "152.193.6.74"
+        },
+        "user": {
+            "id": "db089de9-a62e-4861-a881-0ff15e052516",
+            "buyeruid": "v4_bidder_token"
+        },
+        "tmax": 500
+    },
+    "httpcalls": [{
+        "expectedRequest": {
+            "uri": "https://an.facebook.com/placementbid.ortb",
+            "headers": {
+                "Accept": [
+                    "application/json"
+                ],
+                "Content-Type": [
+                    "application/json;charset=utf-8"
+                ],
+                "X-Fb-Pool-Routing-Token": [
+                    "v4_bidder_token"
+                ]
+            },
+            "body": {
+                "id": "test-imp-id",
+                "imp": [{
+                    "id": "test-imp-id",
+                    "banner": {
+                        "w": -1,
+                        "h": 250
+                    },
+                    "tagid": "123_456"
+                }],
+                "site": {
+                    "domain": "prebid.org",
+                    "page": "prebid.org",
+                    "publisher": {
+                        "id": "123"
+                    }
+                },
+                "device": {
+                    "ip": "152.193.6.74"
+                },
+                "user": {
+                    "id": "db089de9-a62e-4861-a881-0ff15e052516",
+                    "buyeruid": "v4_bidder_token"
+                },
+                "tmax": 500,
+                "ext": {
+                    "authentication_id": "4e24a2b23fbfb5e41a9093b921d6cddf497c24dd5f63879038cec2ab2f27d174",
+                    "platformid": "test-platform-id"
+                }
+            }
+        },
+        "mockResponse": {
+            "status": 200,
+            "body": {
+                "id": "test-imp-id",
+                "seatbid": [{
+                    "bid": [{
+                        "id": "987",
+                        "impid": "test-imp-id",
+                        "price": 1.000000,
+                        "adm": "{\"type\":\"ID\",\"placement_id\":\"123_456\",\"resolved_placement_id\":\"123_456\",\"sdk_version\":\"5.5.0\",\"device_id\":\"abc\",\"template\":1,\"payload\":null,\"bid_time_token\":\"v4_bidder_token=\"}",
+                        "nurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=0&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0",
+                        "lurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=${AUCTION_LOSS}&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0",
+                        "burl": "https://www.facebook.com/audiencenetwork/burl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&clearing_price=${AUCTION_PRICE}"
+                    }]
+                }],
+                "bidid": "654",
+                "cur": "USD"
+            }
+        }
+    }],
+    "expectedBidResponses": [{
+        "currency": "USD",
+        "bids": []
+    }],
+    "expectedMakeBidsErrors": [{
+        "value": "bid 987 missing 'bid_id' in 'adm'",
+        "comparison": "literal"
+    }]
+}
\ No newline at end of file
diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm.json
new file mode 100644
index 00000000..23227aab
--- /dev/null
+++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/missing-adm.json
@@ -0,0 +1,106 @@
+{
+    "mockBidRequest": {
+        "id": "test-req-id",
+        "imp": [{
+            "id": "test-imp-id",
+            "banner": {
+                "format": [{
+                    "w": 300,
+                    "h": 250
+                }],
+                "w": 300,
+                "h": 250
+            },
+            "ext": {
+                "bidder": {
+                    "publisherid": "123",
+                    "placementid": "456"
+                }
+            }
+        }],
+        "site": {
+            "domain": "prebid.org",
+            "page": "prebid.org"
+        },
+        "device": {
+            "ip": "152.193.6.74"
+        },
+        "user": {
+            "id": "db089de9-a62e-4861-a881-0ff15e052516",
+            "buyeruid": "v4_bidder_token"
+        },
+        "tmax": 500
+    },
+    "httpcalls": [{
+        "expectedRequest": {
+            "uri": "https://an.facebook.com/placementbid.ortb",
+            "headers": {
+                "Accept": [
+                    "application/json"
+                ],
+                "Content-Type": [
+                    "application/json;charset=utf-8"
+                ],
+                "X-Fb-Pool-Routing-Token": [
+                    "v4_bidder_token"
+                ]
+            },
+            "body": {
+                "id": "test-imp-id",
+                "imp": [{
+                    "id": "test-imp-id",
+                    "banner": {
+                        "w": -1,
+                        "h": 250
+                    },
+                    "tagid": "123_456"
+                }],
+                "site": {
+                    "domain": "prebid.org",
+                    "page": "prebid.org",
+                    "publisher": {
+                        "id": "123"
+                    }
+                },
+                "device": {
+                    "ip": "152.193.6.74"
+                },
+                "user": {
+                    "id": "db089de9-a62e-4861-a881-0ff15e052516",
+                    "buyeruid": "v4_bidder_token"
+                },
+                "tmax": 500,
+                "ext": {
+                    "authentication_id": "4e24a2b23fbfb5e41a9093b921d6cddf497c24dd5f63879038cec2ab2f27d174",
+                    "platformid": "test-platform-id"
+                }
+            }
+        },
+        "mockResponse": {
+            "status": 200,
+            "body": {
+                "id": "test-imp-id",
+                "seatbid": [{
+                    "bid": [{
+                        "id": "987",
+                        "impid": "test-imp-id",
+                        "price": 1.000000,
+                        "nurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=0&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0",
+                        "lurl": "https://www.facebook.com/audiencenetwork/nurl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&ortb_loss_code=${AUCTION_LOSS}&clearing_price=${AUCTION_PRICE}&app_version=iOS-1.0",
+                        "burl": "https://www.facebook.com/audiencenetwork/burl/?partner=test-platform-id&app=def&placement=456&auction=123&impression=123&request=123478&bid=987&clearing_price=${AUCTION_PRICE}"
+                    }]
+                }],
+                "bidid": "654",
+                "cur": "USD"
+            }
+        }
+    }],
+    "expectedBidResponses": [{
+        "currency": "USD",
+        "bids": []
+    }],
+    "expectedMakeBidsErrors": [{
+        "value": "Bid 987 missing 'adm'",
+        "comparison": "literal"
+    }]
+}
\ No newline at end of file
diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/multi-imp.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/multi-imp.json
index 16e8aede..231c2826 100644
--- a/adapters/audienceNetwork/audienceNetworktest/supplemental/multi-imp.json
+++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/multi-imp.json
@@ -81,12 +81,6 @@
                             "tagid": "pub1_plmt1"
                         }
                     ],
-                    "ext": {
-                        "appnexus": {
-                            "hb_source": 5
-                        },
-                        "prebid": {}
-                    },
                     "site": {
                         "domain": "prebid.org",
                         "page": "prebid.org",
@@ -158,12 +152,6 @@
                             "tagid": "pub2_plmt2"
                         }
                     ],
-                    "ext": {
-                        "appnexus": {
-                            "hb_source": 5
-                        },
-                        "prebid": {}
-                    },
                     "site": {
                         "domain": "prebid.org",
                         "page": "prebid.org",
diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/no-bid-204.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/no-bid-204.json
index bb192aad..45b35e05 100644
--- a/adapters/audienceNetwork/audienceNetworktest/supplemental/no-bid-204.json
+++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/no-bid-204.json
@@ -56,12 +56,6 @@
                             "tagid": "123_456"
                         }
                     ],
-                    "ext": {
-                        "appnexus": {
-                            "hb_source": 5
-                        },
-                        "prebid": {}
-                    },
                     "site": {
                         "domain": "prebid.org",
                         "page": "prebid.org",
diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/no-imps.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/no-imps.json
new file mode 100644
index 00000000..7420f7e8
--- /dev/null
+++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/no-imps.json
@@ -0,0 +1,22 @@
+{
+    "mockBidRequest": {
+        "id": "test-req-id",
+        "imp": [],
+        "site": {
+            "domain": "prebid.org",
+            "page": "prebid.org"
+        },
+        "device": {
+            "ip": "152.193.6.74"
+        },
+        "user": {
+            "id": "db089de9-a62e-4861-a881-0ff15e052516",
+            "buyeruid": "v4_bidder_token"
+        },
+        "tmax": 500
+    },
+    "expectedMakeRequestsErrors": [{
+        "value": "No impressions provided",
+        "comparison": "literal"
+    }]
+}
\ No newline at end of file
diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/server-error-500.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/server-error-500.json
new file mode 100644
index 00000000..7ff88861
--- /dev/null
+++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/server-error-500.json
@@ -0,0 +1,87 @@
+{
+    "mockBidRequest": {
+        "id": "test-req-id",
+        "imp": [{
+            "id": "test-imp-id",
+            "native": {
+                "request": "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":500}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":1,\"hmin\":1}},{\"id\":3,\"required\":0,\"data\":{\"type\":1,\"len\":200}},{\"id\":4,\"required\":0,\"data\":{\"type\":2,\"len\":15000}},{\"id\":5,\"required\":0,\"data\":{\"type\":6,\"len\":40}},{\"id\":6,\"required\":0,\"data\":{\"type\":500}}]}",
+                "ver": "1.1"
+            },
+            "ext": {
+                "bidder": {
+                    "publisherid": "123",
+                    "placementid": "456"
+                }
+            }
+        }],
+        "site": {
+            "domain": "prebid.org",
+            "page": "prebid.org"
+        },
+        "device": {
+            "ip": "152.193.6.74"
+        },
+        "user": {
+            "id": "db089de9-a62e-4861-a881-0ff15e052516",
+            "buyeruid": "v4_bidder_token"
+        },
+        "tmax": 500
+    },
+    "httpcalls": [{
+        "expectedRequest": {
+            "uri": "https://an.facebook.com/placementbid.ortb",
+            "headers": {
+                "Accept": [
+                    "application/json"
+                ],
+                "Content-Type": [
+                    "application/json;charset=utf-8"
+                ],
+                "X-Fb-Pool-Routing-Token": [
+                    "v4_bidder_token"
+                ]
+            },
+            "body": {
+                "id": "test-imp-id",
+                "imp": [{
+                    "id": "test-imp-id",
+                    "native": {
+                        "w": -1,
+                        "h": -1
+                    },
+                    "tagid": "123_456"
+                }],
+                "site": {
+                    "domain": "prebid.org",
+                    "page": "prebid.org",
+                    "publisher": {
+                        "id": "123"
+                    }
+                },
+                "device": {
+                    "ip": "152.193.6.74"
+                },
+                "user": {
+                    "id": "db089de9-a62e-4861-a881-0ff15e052516",
+                    "buyeruid": "v4_bidder_token"
+                },
+                "tmax": 500,
+                "ext": {
+                    "authentication_id": "4e24a2b23fbfb5e41a9093b921d6cddf497c24dd5f63879038cec2ab2f27d174",
+                    "platformid": "test-platform-id"
+                }
+            }
+        },
+        "mockResponse": {
+            "headers": {
+            "X-Fb-An-Errors": [
+                "someError"
+            ]},
+            "status": 500
+        }
+    }],
+    "expectedMakeBidsErrors": [{
+        "value": "Unexpected status code 500 with error message 'someError'",
+        "comparison": "literal"
+    }]
+}
\ No newline at end of file
diff --git a/adapters/audienceNetwork/audienceNetworktest/supplemental/split-placementId.json b/adapters/audienceNetwork/audienceNetworktest/supplemental/split-placementId.json
index 4c561c55..34c1eccc 100644
--- a/adapters/audienceNetwork/audienceNetworktest/supplemental/split-placementId.json
+++ b/adapters/audienceNetwork/audienceNetworktest/supplemental/split-placementId.json
@@ -50,12 +50,6 @@
               "tagid": "123_456"
             }
           ],
-          "ext": {
-            "appnexus": {
-              "hb_source": 5
-            },
-            "prebid": {}
-          },
           "site": {
             "domain": "prebid.org",
             "page": "prebid.org",
diff --git a/adapters/audienceNetwork/facebook_test.go b/adapters/audienceNetwork/facebook_test.go
index 784a540e..7f567d61 100644
--- a/adapters/audienceNetwork/facebook_test.go
+++ b/adapters/audienceNetwork/facebook_test.go
@@ -1,6 +1,7 @@
 package audienceNetwork
 
 import (
+	"errors"
 	"testing"
 	"time"
 
@@ -40,14 +41,14 @@ type FacebookExt struct {
 }
 
 func TestJsonSamples(t *testing.T) {
-	adapterstest.RunJSONBidderTest(t, "audienceNetworktest", NewFacebookBidder(nil, "test-platform-id", "test-app-secret"))
+	adapterstest.RunJSONBidderTest(t, "audienceNetworktest", NewFacebookBidder("test-platform-id", "test-app-secret"))
 }
 
 func TestMakeTimeoutNoticeApp(t *testing.T) {
 	req := adapters.RequestData{
 		Body: []byte(`{"id":"1234","imp":[{"id":"1234"}],"app":{"publisher":{"id":"5678"}}}`),
 	}
-	fba := NewFacebookBidder(nil, "test-platform-id", "test-app-secret")
+	fba := NewFacebookBidder("test-platform-id", "test-app-secret")
 
 	tb, ok := fba.(adapters.TimeoutBidder)
 	if !ok {
@@ -64,7 +65,7 @@ func TestMakeTimeoutNoticeSite(t *testing.T) {
 	req := adapters.RequestData{
 		Body: []byte(`{"id":"1234","imp":[{"id":"1234"}],"site":{"publisher":{"id":"5678"}}}`),
 	}
-	fba := NewFacebookBidder(nil, "test-platform-id", "test-app-secret")
+	fba := NewFacebookBidder("test-platform-id", "test-app-secret")
 
 	tb, ok := fba.(adapters.TimeoutBidder)
 	if !ok {
@@ -81,7 +82,7 @@ func TestMakeTimeoutNoticeBadRequest(t *testing.T) {
 	req := adapters.RequestData{
 		Body: []byte(`{"imp":[{{"id":"1234"}}`),
 	}
-	fba := NewFacebookBidder(nil, "test-platform-id", "test-app-secret")
+	fba := NewFacebookBidder("test-platform-id", "test-app-secret")
 
 	tb, ok := fba.(adapters.TimeoutBidder)
 	if !ok {
@@ -93,3 +94,25 @@ func TestMakeTimeoutNoticeBadRequest(t *testing.T) {
 	assert.NotNil(t, err, "Facebook MakeTimeoutNotification() did not return an error")
 
 }
+
+func TestNewFacebookBidderMissingPlatformID(t *testing.T) {
+	result := NewFacebookBidder("", "anyAppSecret")
+
+	expected := &adapters.MisconfiguredBidder{
+		Name:  "audienceNetwork",
+		Error: errors.New("Audience Network is not configured properly on this Prebid Server deploy. If you believe this should work, contact the company hosting the service and tell them to check their configuration."),
+	}
+
+	assert.Equal(t, expected, result)
+}
+
+func TestNewFacebookBidderMissingAppSecret(t *testing.T) {
+	result := NewFacebookBidder("anyPlatformID", "")
+
+	expected := &adapters.MisconfiguredBidder{
+		Name:  "audienceNetwork",
+		Error: errors.New("Audience Network is not configured properly on this Prebid Server deploy. If you believe this should work, contact the company hosting the service and tell them to check their configuration."),
+	}
+
+	assert.Equal(t, expected, result)
+}
diff --git a/adapters/kubient/kubienttest/exemplary/banner.json b/adapters/kubient/kubienttest/exemplary/banner.json
index a32c761a..9af4f9f8 100644
--- a/adapters/kubient/kubienttest/exemplary/banner.json
+++ b/adapters/kubient/kubienttest/exemplary/banner.json
@@ -17,7 +17,9 @@
           ]
         },
         "ext": {
-          "bidder": {}
+          "bidder": {
+            "zoneid": "9042"
+          }
         }
       }
     ]
@@ -44,7 +46,9 @@
                 ]
               },
               "ext": {
-                "bidder": {}
+                "bidder": {
+                  "zoneid": "9042"
+                }
               }
             }
           ]
diff --git a/adapters/kubient/kubienttest/exemplary/video.json b/adapters/kubient/kubienttest/exemplary/video.json
index 59d32874..d9346c3f 100644
--- a/adapters/kubient/kubienttest/exemplary/video.json
+++ b/adapters/kubient/kubienttest/exemplary/video.json
@@ -11,7 +11,9 @@
           "h": 576
         },
         "ext": {
-          "bidder": {}
+          "bidder": {
+            "zoneid": "9010"
+          }
         }
       }
     ]
@@ -32,7 +34,9 @@
                 "h": 576
               },
               "ext": {
-                "bidder": {}
+                "bidder": {
+                  "zoneid": "9010"
+                }
               }
             }
           ]
diff --git a/adapters/kubient/kubienttest/supplemental/bad_response.json b/adapters/kubient/kubienttest/supplemental/bad_response.json
index 166743cf..076acf29 100644
--- a/adapters/kubient/kubienttest/supplemental/bad_response.json
+++ b/adapters/kubient/kubienttest/supplemental/bad_response.json
@@ -13,7 +13,9 @@
           ]
         },
         "ext": {
-          "bidder": {}
+          "bidder": {
+            "zoneid": "23"
+          }
         }
       }
     ]
@@ -36,7 +38,9 @@
                 ]
               },
               "ext": {
-                "bidder": {}
+                "bidder": {
+                  "zoneid": "23"
+                }
               }
             }
           ]
diff --git a/adapters/kubient/kubienttest/supplemental/missing-zoneid.json b/adapters/kubient/kubienttest/supplemental/missing-zoneid.json
new file mode 100644
index 00000000..cfd61662
--- /dev/null
+++ b/adapters/kubient/kubienttest/supplemental/missing-zoneid.json
@@ -0,0 +1,31 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-missing-req-param-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {}
+        }
+      }
+    ]
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "zoneid is empty",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/kubient/kubienttest/supplemental/no-imps.json b/adapters/kubient/kubienttest/supplemental/no-imps.json
new file mode 100644
index 00000000..189adf9a
--- /dev/null
+++ b/adapters/kubient/kubienttest/supplemental/no-imps.json
@@ -0,0 +1,12 @@
+{
+  "mockBidRequest": {
+    "id": "test-no-imp-request-id",
+    "imp": []
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "No impression in the bid request",
+      "comparison": "literal"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/kubient/kubienttest/supplemental/status_204.json b/adapters/kubient/kubienttest/supplemental/status_204.json
index 58bb2629..6794d58b 100644
--- a/adapters/kubient/kubienttest/supplemental/status_204.json
+++ b/adapters/kubient/kubienttest/supplemental/status_204.json
@@ -14,6 +14,7 @@
         },
         "ext": {
           "bidder": {
+            "zoneid": "203"
           }
         }
       }
@@ -39,6 +40,7 @@
               },
               "ext": {
                 "bidder": {
+                  "zoneid": "203"
                 }
               }
             }
diff --git a/adapters/kubient/kubienttest/supplemental/status_400.json b/adapters/kubient/kubienttest/supplemental/status_400.json
index e895f793..29438cc3 100644
--- a/adapters/kubient/kubienttest/supplemental/status_400.json
+++ b/adapters/kubient/kubienttest/supplemental/status_400.json
@@ -13,7 +13,9 @@
           ]
         },
         "ext": {
-          "bidder": {}
+          "bidder": {
+            "zoneid": "102"
+          }
         }
       }
     ]
@@ -37,7 +39,9 @@
                 ]
               },
               "ext": {
-                "bidder": {}
+                "bidder": {
+                  "zoneid": "102"
+                }
               }
             }
           ]
diff --git a/adapters/pubnative/pubnativetest/exemplary/simple-banner.json b/adapters/pubnative/pubnativetest/exemplary/simple-banner.json
index 7c7d1319..5297cd32 100644
--- a/adapters/pubnative/pubnativetest/exemplary/simple-banner.json
+++ b/adapters/pubnative/pubnativetest/exemplary/simple-banner.json
@@ -111,6 +111,7 @@
           },
           "at": 1,
           "tmax": 200,
+          "test": 1,
           "source": {
             "tid": "283746293874293"
           },
diff --git a/config/config_test.go b/config/config_test.go
index 2b291fe9..3456694d 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -2,7 +2,7 @@ package config
 
 import (
 	"bytes"
-	"fmt"
+	"net"
 	"strings"
 	"testing"
 	"time"
@@ -67,10 +67,12 @@ external_cache:
   host: www.externalprebidcache.net
   path: endpoints/cache
 http_client:
+  max_connections_per_host: 10
   max_idle_connections: 500
   max_idle_connections_per_host: 20
   idle_connection_timeout_seconds: 30
 http_client_cache:
+  max_connections_per_host: 5
   max_idle_connections: 1
   max_idle_connections_per_host: 2
   idle_connection_timeout_seconds: 3
@@ -117,6 +119,9 @@ adapters:
 blacklisted_apps: ["spamAppID","sketchy-app-id"]
 account_required: true
 certificates_file: /etc/ssl/cert.pem
+request_validation:
+    ipv4_private_networks: ["1.1.1.0/24"]
+    ipv6_private_networks: ["1111::/16", "2222::/16"]
 `)
 
 var adapterExtraInfoConfig = []byte(`
@@ -217,9 +222,11 @@ func TestFullConfig(t *testing.T) {
 	cmpStrings(t, "cache.query", cfg.CacheURL.Query, "uuid=%PBS_CACHE_UUID%")
 	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)
@@ -288,6 +295,9 @@ func TestFullConfig(t *testing.T) {
 	cmpBools(t, "account_required", cfg.AccountRequired, true)
 	cmpBools(t, "account_adapter_details", cfg.Metrics.Disabled.AccountAdapterDetails, 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")
 }
 
 func TestUnmarshalAdapterExtraInfo(t *testing.T) {
@@ -408,23 +418,63 @@ func TestLimitTimeout(t *testing.T) {
 }
 
 func TestCookieSizeError(t *testing.T) {
-	type aTest struct {
-		cookieHost  *HostCookie
+	testCases := []struct {
+		description string
+		cookieSize  int
 		expectError bool
+	}{
+		{"MIN_COOKIE_SIZE_BYTES + 1", MIN_COOKIE_SIZE_BYTES + 1, false},
+		{"MIN_COOKIE_SIZE_BYTES", MIN_COOKIE_SIZE_BYTES, false},
+		{"MIN_COOKIE_SIZE_BYTES - 1", MIN_COOKIE_SIZE_BYTES - 1, true},
+		{"Zero", 0, false},
+		{"Negative", -100, true},
 	}
-	testCases := []aTest{
-		{cookieHost: &HostCookie{MaxCookieSizeBytes: 1 << 15}, expectError: false}, //32 KB, no error
-		{cookieHost: &HostCookie{MaxCookieSizeBytes: 800}, expectError: false},
-		{cookieHost: &HostCookie{MaxCookieSizeBytes: 500}, expectError: false},
-		{cookieHost: &HostCookie{MaxCookieSizeBytes: 0}, expectError: false},
-		{cookieHost: &HostCookie{MaxCookieSizeBytes: 200}, expectError: true},
-		{cookieHost: &HostCookie{MaxCookieSizeBytes: -100}, expectError: true},
+
+	for _, test := range testCases {
+		resultErr := isValidCookieSize(test.cookieSize)
+
+		if test.expectError {
+			assert.Error(t, resultErr, test.description)
+		} else {
+			assert.NoError(t, resultErr, test.description)
+		}
+	}
+}
+
+func TestNewCallsRequestValidation(t *testing.T) {
+	testCases := []struct {
+		description       string
+		privateIPNetworks string
+		expectedError     string
+		expectedIPs       []net.IPNet
+	}{
+		{
+			description:       "Valid",
+			privateIPNetworks: `["1.1.1.0/24"]`,
+			expectedIPs:       []net.IPNet{{IP: net.IP{1, 1, 1, 0}, Mask: net.CIDRMask(24, 32)}},
+		},
+		{
+			description:       "Invalid",
+			privateIPNetworks: `["1"]`,
+			expectedError:     "Invalid private IPv4 networks: '1'",
+		},
 	}
-	for i := range testCases {
-		if testCases[i].expectError {
-			assert.Error(t, isValidCookieSize(testCases[i].cookieHost.MaxCookieSizeBytes), fmt.Sprintf("Configuration.HostCookie.MaxCookieSizeBytes less than MIN_COOKIE_SIZE_BYTES = %d and not equal to zero should return an error", MIN_COOKIE_SIZE_BYTES))
+
+	for _, test := range testCases {
+		v := viper.New()
+		SetupViper(v, "")
+		v.SetConfigType("yaml")
+		v.ReadConfig(bytes.NewBuffer([]byte(
+			`request_validation:
+    ipv4_private_networks: ` + test.privateIPNetworks)))
+
+		result, resultErr := New(v)
+
+		if test.expectedError == "" {
+			assert.NoError(t, resultErr, test.description+":err")
+			assert.ElementsMatch(t, test.expectedIPs, result.RequestValidation.IPv4PrivateNetworksParsed, test.description+":parsed")
 		} else {
-			assert.NoError(t, isValidCookieSize(testCases[i].cookieHost.MaxCookieSizeBytes), fmt.Sprintf("Configuration.HostCookie.MaxCookieSizeBytes greater than MIN_COOKIE_SIZE_BYTES = %d or equal to zero should not return an error", MIN_COOKIE_SIZE_BYTES))
+			assert.Error(t, resultErr, test.description+":err")
 		}
 	}
 }
diff --git a/config/requestvalidation_test.go b/config/requestvalidation_test.go
new file mode 100644
index 00000000..cacb4f2d
--- /dev/null
+++ b/config/requestvalidation_test.go
@@ -0,0 +1,145 @@
+package config
+
+import (
+	"net"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestParse(t *testing.T) {
+	ipv4Mask16 := net.CIDRMask(16, 32)
+	ipv4Mask24 := net.CIDRMask(24, 32)
+
+	ipv6Mask16 := net.CIDRMask(16, 128)
+	ipv6Mask32 := net.CIDRMask(32, 128)
+
+	testCases := []struct {
+		description  string
+		ipv4         []string
+		ipv4Expected []net.IPNet
+		ipv6         []string
+		ipv6Expected []net.IPNet
+		expectedErr  string
+	}{
+		{
+			description:  "Empty",
+			ipv4:         []string{},
+			ipv4Expected: []net.IPNet{},
+			ipv6:         []string{},
+			ipv6Expected: []net.IPNet{},
+		},
+		{
+			description:  "One",
+			ipv4:         []string{"1.1.1.1/24"},
+			ipv4Expected: []net.IPNet{{IP: net.IP{1, 1, 1, 0}, Mask: ipv4Mask24}},
+			ipv6:         []string{"1111:2222::/16"},
+			ipv6Expected: []net.IPNet{{IP: net.ParseIP("1111::"), Mask: ipv6Mask16}},
+		},
+		{
+			description:  "One - Ignore Whitespace",
+			ipv4:         []string{"   1.1.1.1/24 "},
+			ipv4Expected: []net.IPNet{{IP: net.IP{1, 1, 1, 0}, Mask: ipv4Mask24}},
+			ipv6:         []string{"   1111:2222::/16 "},
+			ipv6Expected: []net.IPNet{{IP: net.ParseIP("1111::"), Mask: ipv6Mask16}},
+		},
+		{
+			description:  "Many",
+			ipv4:         []string{"1.1.1.1/24", "2.2.2.2/16"},
+			ipv4Expected: []net.IPNet{{IP: net.IP{1, 1, 1, 0}, Mask: ipv4Mask24}, {IP: net.IP{2, 2, 0, 0}, Mask: ipv4Mask16}},
+			ipv6:         []string{"1111:2222::/16", "1111:2222:3333::/32"},
+			ipv6Expected: []net.IPNet{{IP: net.ParseIP("1111::"), Mask: ipv6Mask16}, {IP: net.ParseIP("1111:2222::"), Mask: ipv6Mask32}},
+		},
+		{
+			description: "Malformed - IPv4 - One",
+			ipv4:        []string{"malformed1"},
+			ipv6:        []string{},
+			expectedErr: "Invalid private IPv4 network: 'malformed1'",
+		},
+		{
+			description: "Malformed - IPv4 - Many",
+			ipv4:        []string{"malformed1", "malformed2"},
+			ipv6:        []string{},
+			expectedErr: "Invalid private IPv4 network: 'malformed1','malformed2'",
+		},
+		{
+			description: "Malformed - IPv6 - One",
+			ipv4:        []string{},
+			ipv6:        []string{"malformed2"},
+			expectedErr: "Invalid private IPv6 network: 'malformed2'",
+		},
+		{
+			description: "Malformed - IPv6 - Many",
+			ipv4:        []string{},
+			ipv6:        []string{"malformed1", "malformed2"},
+			expectedErr: "Invalid private IPv6 network: 'malformed1','malformed2'",
+		},
+		{
+			description: "Malformed - Mixed",
+			ipv4:        []string{"malformed1"},
+			ipv6:        []string{"malformed2"},
+			expectedErr: "Invalid private IPv4 network: 'malformed1'",
+		},
+		{
+			description: "Malformed - IPv4 - Ignore Whitespace",
+			ipv4:        []string{"   malformed1 "},
+			ipv6:        []string{},
+			expectedErr: "Invalid private IPv4 network: 'malformed1'",
+		},
+		{
+			description: "Malformed - IPv6 - Ignore Whitespace",
+			ipv4:        []string{},
+			ipv6:        []string{"   malformed2 "},
+			expectedErr: "Invalid private IPv6 network: 'malformed2'",
+		},
+		{
+			description: "Malformed - IPv4 - Missing Network Mask",
+			ipv4:        []string{"1.1.1.1"},
+			ipv6:        []string{},
+			expectedErr: "Invalid private IPv4 network: '1.1.1.1'",
+		},
+		{
+			description: "Malformed - IPv6 - Missing Network Mask",
+			ipv4:        []string{},
+			ipv6:        []string{"1111::"},
+			expectedErr: "Invalid private IPv6 network: '1111::'",
+		},
+		{
+			description: "Malformed - IPv4 - Wrong IP Version",
+			ipv4:        []string{"1111::/16"},
+			ipv6:        []string{},
+			expectedErr: "Invalid private IPv4 network: '1111::/16'",
+		},
+		{
+			description: "Malformed - IPv6 - Wrong IP Version",
+			ipv4:        []string{},
+			ipv6:        []string{"1.1.1.1/16"},
+			expectedErr: "Invalid private IPv6 network: '1.1.1.1/16'",
+		},
+		{
+			description: "Malformed - IPv6 Mapped IPv4",
+			ipv4:        []string{"::FFFF:1.1.1.1"},
+			ipv6:        []string{},
+			expectedErr: "Invalid private IPv4 network: '::FFFF:1.1.1.1'",
+		},
+	}
+
+	for _, test := range testCases {
+		requestValidation := &RequestValidation{
+			IPv4PrivateNetworks: test.ipv4,
+			IPv6PrivateNetworks: test.ipv6,
+		}
+
+		err := requestValidation.Parse()
+
+		if test.expectedErr == "" {
+			assert.NoError(t, err, test.description+":err")
+		} else {
+			assert.Error(t, err, test.description+":err")
+			assert.Equal(t, test.expectedErr, err.Error(), test.description+":err_msg")
+		}
+
+		assert.ElementsMatch(t, requestValidation.IPv4PrivateNetworksParsed, test.ipv4Expected, test.description+":ipv4")
+		assert.ElementsMatch(t, requestValidation.IPv6PrivateNetworksParsed, test.ipv6Expected, test.description+":ipv6")
+	}
+}
diff --git a/currencies/rate_converter_test.go b/currencies/rate_converter_test.go
index cb5e2a0b..d717d1a3 100644
--- a/currencies/rate_converter_test.go
+++ b/currencies/rate_converter_test.go
@@ -13,6 +13,20 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+func getMockRates() []byte {
+	return []byte(`{
+		"dataAsOf":"2018-09-12",
+		"conversions":{
+			"USD":{
+				"GBP":0.77208
+			},
+			"GBP":{
+				"USD":1.2952
+			}
+		}
+	}`)
+}
+
 func TestFetch_Success(t *testing.T) {
 
 	// Setup:
@@ -21,19 +35,7 @@ func TestFetch_Success(t *testing.T) {
 		func(rw http.ResponseWriter, req *http.Request) {
 			calledURLs = append(calledURLs, req.RequestURI)
 			rw.WriteHeader(http.StatusOK)
-			rw.Write([]byte(
-				`{
-					"dataAsOf":"2018-09-12",
-					"conversions":{
-						"USD":{
-							"GBP":0.77208
-						},
-						"GBP":{
-							"USD":1.2952
-						}
-					}
-				}`,
-			))
+			rw.Write([]byte(getMockRates()))
 		}),
 	)
 
@@ -57,6 +59,7 @@ func TestFetch_Success(t *testing.T) {
 		&http.Client{},
 		mockedHttpServer.URL,
 		time.Duration(24)*time.Hour,
+		time.Duration(24)*time.Hour,
 	)
 
 	// Verify:
@@ -87,12 +90,13 @@ func TestFetch_Fail404(t *testing.T) {
 		&http.Client{},
 		mockedHttpServer.URL,
 		time.Duration(24)*time.Hour,
+		time.Duration(24)*time.Hour,
 	)
 
 	// Verify:
 	assert.Equal(t, 1, len(calledURLs), "sync URL should have been called %d times but was %d", 1, len(calledURLs))
 	assert.Equal(t, currencyConverter.LastUpdated(), (time.Time{}), "LastUpdated() shouldn't return a time set")
-	assert.Nil(t, currencyConverter.Rates(), "Rates() should return nil")
+	assert.Equal(t, currencyConverter.Rates(), &currencies.ConstantRates{}, "Rates() should return constant rates")
 	assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil")
 }
 
@@ -114,12 +118,13 @@ func TestFetch_FailErrorHttpClient(t *testing.T) {
 		&http.Client{},
 		mockedHttpServer.URL,
 		time.Duration(24)*time.Hour,
+		time.Duration(24)*time.Hour,
 	)
 
 	// Verify:
 	assert.Equal(t, 1, len(calledURLs), "sync URL should have been called %d times but was %d", 1, len(calledURLs))
 	assert.Equal(t, currencyConverter.LastUpdated(), (time.Time{}), "LastUpdated() shouldn't return a time set")
-	assert.Nil(t, currencyConverter.Rates(), "Rates() should return nil")
+	assert.Equal(t, currencyConverter.Rates(), &currencies.ConstantRates{}, "Rates() should return constant rates")
 	assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil")
 }
 
@@ -132,11 +137,12 @@ func TestFetch_FailBadSyncURL(t *testing.T) {
 		&http.Client{},
 		"justaweirdurl",
 		time.Duration(24)*time.Hour,
+		time.Duration(24)*time.Hour,
 	)
 
 	// Verify:
 	assert.Equal(t, currencyConverter.LastUpdated(), (time.Time{}), "LastUpdated() shouldn't return a time set")
-	assert.Nil(t, currencyConverter.Rates(), "Rates() should return nil")
+	assert.Equal(t, currencyConverter.Rates(), &currencies.ConstantRates{}, "Rates() should return constant rates")
 	assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil")
 }
 
@@ -172,12 +178,13 @@ func TestFetch_FailBadJSON(t *testing.T) {
 		&http.Client{},
 		mockedHttpServer.URL,
 		time.Duration(24)*time.Hour,
+		time.Duration(24)*time.Hour,
 	)
 
 	// Verify:
 	assert.Equal(t, 1, len(calledURLs), "sync URL should have been called %d times but was %d", 1, len(calledURLs))
 	assert.Equal(t, currencyConverter.LastUpdated(), (time.Time{}), "LastUpdated() shouldn't return a time set")
-	assert.Nil(t, currencyConverter.Rates(), "Rates() should return nil")
+	assert.Equal(t, currencyConverter.Rates(), &currencies.ConstantRates{}, "Rates() should return constant rates")
 	assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil")
 }
 
@@ -200,12 +207,13 @@ func TestFetch_InvalidRemoteResponseContent(t *testing.T) {
 		&http.Client{},
 		mockedHttpServer.URL,
 		time.Duration(24)*time.Hour,
+		time.Duration(24)*time.Hour,
 	)
 
 	// Verify:
 	assert.Equal(t, 1, len(calledURLs), "sync URL should have been called %d times but was %d", 1, len(calledURLs))
 	assert.Equal(t, currencyConverter.LastUpdated(), (time.Time{}), "LastUpdated() shouldn't return a time set")
-	assert.Nil(t, currencyConverter.Rates(), "Rates() should return nil")
+	assert.Equal(t, currencyConverter.Rates(), &currencies.ConstantRates{}, "Rates() should return constant rates")
 	assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil")
 }
 
@@ -215,19 +223,7 @@ func TestInit(t *testing.T) {
 	mockedHttpServer := httptest.NewServer(http.HandlerFunc(
 		func(rw http.ResponseWriter, req *http.Request) {
 			rw.WriteHeader(http.StatusOK)
-			rw.Write([]byte(
-				`{
-					"dataAsOf":"2018-09-12",
-					"conversions":{
-						"USD":{
-							"GBP":0.77208
-						},
-						"GBP":{
-							"USD":1.2952
-						}
-					}
-				}`,
-			))
+			rw.Write([]byte(getMockRates()))
 		}),
 	)
 
@@ -239,6 +235,7 @@ func TestInit(t *testing.T) {
 		&http.Client{},
 		mockedHttpServer.URL,
 		time.Duration(100)*time.Millisecond,
+		time.Duration(24)*time.Hour,
 		ticks,
 	)
 
@@ -266,10 +263,8 @@ func TestInit(t *testing.T) {
 			assert.False(t, intervalDiff > float64(errorMargin*100), "Interval between ticks should be: %d but was: %d", expectedIntervalDuration, intervalDuration)
 		}
 
-		assert.NotNil(t, currencyConverter.Rates(), "Rates shouldn't be nil")
 		assert.NotEqual(t, currencyConverter.LastUpdated(), (time.Time{}), "LastUpdated should be set")
-		rates := currencyConverter.Rates()
-		assert.Equal(t, expectedRates, rates, "Conversions.Rates weren't the expected ones")
+		assert.Equal(t, expectedRates, currencyConverter.Rates(), "Conversions.Rates weren't the expected ones")
 		assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil")
 
 		if ticksCount == expectedTicks {
@@ -287,19 +282,7 @@ func TestStop(t *testing.T) {
 		func(rw http.ResponseWriter, req *http.Request) {
 			calledURLs = append(calledURLs, req.RequestURI)
 			rw.WriteHeader(http.StatusOK)
-			rw.Write([]byte(
-				`{
-					"dataAsOf":"2018-09-12",
-					"conversions":{
-						"USD":{
-							"GBP":0.77208
-						},
-						"GBP":{
-							"USD":1.2952
-						}
-					}
-				}`,
-			))
+			rw.Write([]byte(getMockRates()))
 		}),
 	)
 
@@ -310,6 +293,7 @@ func TestStop(t *testing.T) {
 		&http.Client{},
 		mockedHttpServer.URL,
 		time.Duration(100)*time.Millisecond,
+		time.Duration(24)*time.Hour,
 		ticks,
 	)
 
@@ -337,19 +321,7 @@ func TestInitWithZeroDuration(t *testing.T) {
 		func(rw http.ResponseWriter, req *http.Request) {
 			calledURLs = append(calledURLs, req.RequestURI)
 			rw.WriteHeader(http.StatusOK)
-			rw.Write([]byte(
-				`{
-					"dataAsOf":"2018-09-12",
-					"conversions":{
-						"USD":{
-							"GBP":0.77208
-						},
-						"GBP":{
-							"USD":1.2952
-						}
-					}
-				}`,
-			))
+			rw.Write([]byte(getMockRates()))
 		}),
 	)
 
@@ -358,6 +330,7 @@ func TestInitWithZeroDuration(t *testing.T) {
 		&http.Client{},
 		mockedHttpServer.URL,
 		time.Duration(0),
+		time.Duration(24)*time.Hour,
 	)
 
 	// Verify:
@@ -366,8 +339,7 @@ func TestInitWithZeroDuration(t *testing.T) {
 
 	assert.Equal(t, 0, len(calledURLs), "sync URL shouldn't have been called but was called %d times", 0, len(calledURLs))
 	assert.Equal(t, (time.Time{}), currencyConverter.LastUpdated(), "LastUpdated() shouldn't be set")
-	_, ok := currencyConverter.Rates().(*currencies.ConstantRates)
-	assert.True(t, ok, "Rates should be type of `currencies.ConstantRates`")
+	assert.Equal(t, currencyConverter.Rates(), &currencies.ConstantRates{}, "Rates() should return constant rates")
 	assert.NotNil(t, currencyConverter.GetInfo(), "GetInfo() should not return nil")
 }
 
@@ -393,19 +365,7 @@ func TestRates(t *testing.T) {
 	mockedHttpServer := httptest.NewServer(http.HandlerFunc(
 		func(rw http.ResponseWriter, req *http.Request) {
 			rw.WriteHeader(http.StatusOK)
-			rw.Write([]byte(
-				`{
-					"dataAsOf":"2018-09-12",
-					"conversions":{
-						"USD":{
-							"GBP":0.77208
-						},
-						"GBP":{
-							"USD":1.2952
-						}
-					}
-				}`,
-			))
+			rw.Write([]byte(getMockRates()))
 		}),
 	)
 
@@ -415,6 +375,7 @@ func TestRates(t *testing.T) {
 		&http.Client{},
 		mockedHttpServer.URL,
 		time.Duration(100)*time.Millisecond,
+		time.Duration(24)*time.Hour,
 		ticks,
 	)
 	rates := currencyConverter.Rates()
@@ -456,12 +417,161 @@ func TestRates_EmptyRates(t *testing.T) {
 		&http.Client{},
 		mockedHttpServer.URL,
 		time.Duration(100)*time.Millisecond,
+		time.Duration(24)*time.Hour,
 	)
 	defer currencyConverter.StopPeriodicFetching()
-	rates := currencyConverter.Rates()
 
 	// Verify:
-	assert.Nil(t, rates, "rates should be nil")
+	assert.Equal(t, currencyConverter.Rates(), &currencies.ConstantRates{}, "Rates() should return constant rates")
+}
+
+func TestSelectRatesBasedOnStaleness(t *testing.T) {
+	calledURLs := []string{}
+	callCnt := 0
+	mockedHttpServer := httptest.NewServer(http.HandlerFunc(
+		func(rw http.ResponseWriter, req *http.Request) {
+			calledURLs = append(calledURLs, req.RequestURI)
+			if callCnt == 0 || callCnt >= 5 {
+				rw.WriteHeader(http.StatusOK)
+				rw.Write([]byte(getMockRates()))
+			} else {
+				rw.WriteHeader(http.StatusNotFound)
+			}
+			callCnt++
+		}),
+	)
+
+	defer mockedHttpServer.Close()
+
+	expectedRates := &currencies.Rates{
+		DataAsOf: time.Date(2018, time.September, 12, 0, 0, 0, 0, time.UTC),
+		Conversions: map[string]map[string]float64{
+			"USD": {
+				"GBP": 0.77208,
+			},
+			"GBP": {
+				"USD": 1.2952,
+			},
+		},
+	}
+
+	// Execute:
+	currencyConverter := currencies.NewRateConverter(
+		&http.Client{},
+		mockedHttpServer.URL,
+		time.Duration(100)*time.Millisecond,
+		time.Duration(200)*time.Millisecond,
+	)
+
+	// Verify:
+	// Rates are valid at t=0, then invalid for 500ms before being valid again
+	assert.Equal(t, currencyConverter.Rates(), expectedRates, "Rates() should return expected rates")
+
+	time.Sleep(100 * time.Millisecond)
+	// Rates have been invalid for ~100ms, rates not stale yet
+	assert.Equal(t, currencyConverter.Rates(), expectedRates, "Rates() should return expected rates")
+
+	time.Sleep(200 * time.Millisecond)
+	// Rates have been invalid for ~300ms, rates are stale
+	assert.Equal(t, currencyConverter.Rates(), &currencies.ConstantRates{}, "Rates() should return constant rates")
+
+	time.Sleep(300 * time.Millisecond)
+	// Rates have been valid again for ~100ms
+	assert.Equal(t, currencyConverter.Rates(), expectedRates, "Rates() should return expected rates")
+}
+
+func TestUseConstantRatesUntilFetchIsSuccessful(t *testing.T) {
+	callCnt := 0
+	mockedHttpServer := httptest.NewServer(http.HandlerFunc(
+		func(rw http.ResponseWriter, req *http.Request) {
+			if callCnt >= 5 {
+				rw.WriteHeader(http.StatusOK)
+				rw.Write([]byte(getMockRates()))
+			} else {
+				rw.WriteHeader(http.StatusNotFound)
+			}
+			callCnt++
+		}),
+	)
+
+	defer mockedHttpServer.Close()
+
+	expectedRates := &currencies.Rates{
+		DataAsOf: time.Date(2018, time.September, 12, 0, 0, 0, 0, time.UTC),
+		Conversions: map[string]map[string]float64{
+			"USD": {
+				"GBP": 0.77208,
+			},
+			"GBP": {
+				"USD": 1.2952,
+			},
+		},
+	}
+
+	// Execute:
+	currencyConverter := currencies.NewRateConverter(
+		&http.Client{},
+		mockedHttpServer.URL,
+		time.Duration(100)*time.Millisecond,
+		time.Duration(1)*time.Second,
+	)
+
+	// Verify:
+	// Rates are invalid at t=0 and remain invalid until 500ms have elapsed
+	assert.Equal(t, currencyConverter.Rates(), &currencies.ConstantRates{}, "Rates() should return constant rates")
+
+	time.Sleep(400 * time.Millisecond)
+	// Rates have been invalid for ~400ms
+	assert.Equal(t, currencyConverter.Rates(), &currencies.ConstantRates{}, "Rates() should return constant rates")
+
+	time.Sleep(200 * time.Millisecond)
+	// Rates have been valid for ~100ms
+	assert.Equal(t, currencyConverter.Rates(), expectedRates, "Rates() should return expected rates")
+}
+
+func TestRatesAreNeverStale(t *testing.T) {
+	callCnt := 0
+	mockedHttpServer := httptest.NewServer(http.HandlerFunc(
+		func(rw http.ResponseWriter, req *http.Request) {
+			if callCnt == 0 {
+				rw.WriteHeader(http.StatusOK)
+				rw.Write([]byte(getMockRates()))
+			} else {
+				rw.WriteHeader(http.StatusNotFound)
+			}
+			callCnt++
+		}),
+	)
+
+	defer mockedHttpServer.Close()
+
+	expectedRates := &currencies.Rates{
+		DataAsOf: time.Date(2018, time.September, 12, 0, 0, 0, 0, time.UTC),
+		Conversions: map[string]map[string]float64{
+			"USD": {
+				"GBP": 0.77208,
+			},
+			"GBP": {
+				"USD": 1.2952,
+			},
+		},
+	}
+
+	// Execute:
+	currencyConverter := currencies.NewRateConverter(
+		&http.Client{},
+		mockedHttpServer.URL,
+		time.Duration(100)*time.Millisecond,
+		time.Duration(0)*time.Millisecond,
+	)
+
+	// Verify:
+	// Rates are valid at t=0 and are then invalid at 100ms
+	assert.Equal(t, currencyConverter.Rates(), expectedRates, "Rates() should return expected rates")
+
+	time.Sleep(500 * time.Millisecond)
+	// Rates have been invalid for ~400ms
+	assert.Equal(t, currencyConverter.Rates(), expectedRates, "Rates() should return expected rates")
 }
 
 func TestRace(t *testing.T) {
@@ -495,6 +605,7 @@ func TestRace(t *testing.T) {
 		mockedHttpClient,
 		"currency.fake.com",
 		time.Duration(10)*time.Millisecond,
+		time.Duration(24)*time.Hour,
 	)
 	defer currencyConverter.StopPeriodicFetching()
 
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index c3b9267b..97f0038a 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -8,6 +8,7 @@ import (
 	"fmt"
 	"io"
 	"io/ioutil"
+	"net"
 	"net/http"
 	"net/http/httptest"
 	"os"
@@ -29,6 +30,7 @@ import (
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/pbsmetrics"
 	"github.com/prebid/prebid-server/stored_requests/backends/empty_fetcher"
+	"github.com/prebid/prebid-server/util/iputil"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -526,26 +528,79 @@ func TestAuctionTypeDefault(t *testing.T) {
 	}
 }
 
-// TestImplicitIPs prevents #230
-func TestImplicitIPs(t *testing.T) {
-	ex := &nobidExchange{}
-	// NewMetrics() will create a new go_metrics MetricsEngine, bypassing the need for a crafted configuration set to support it.
-	// As a side effect this gives us some coverage of the go_metrics piece of the metrics engine.
-	theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{})
-	endpoint, _ := NewEndpoint(ex, newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap)
+func TestImplicitIPsEndToEnd(t *testing.T) {
+	testCases := []struct {
+		description         string
+		reqJSONFile         string
+		xForwardedForHeader string
+		privateNetworksIPv4 []net.IPNet
+		privateNetworksIPv6 []net.IPNet
+		expectedDeviceIPv4  string
+		expectedDeviceIPv6  string
+	}{
+		{
+			description:         "IPv4",
+			reqJSONFile:         "site.json",
+			xForwardedForHeader: "1.1.1.1",
+			expectedDeviceIPv4:  "1.1.1.1",
+		},
+		{
+			description:         "IPv6",
+			reqJSONFile:         "site.json",
+			xForwardedForHeader: "1111::",
+			expectedDeviceIPv6:  "1111::",
+		},
+		{
+			description:         "IPv4 - Defined In Request",
+			reqJSONFile:         "site-has-ipv4.json",
+			xForwardedForHeader: "1.1.1.1",
+			expectedDeviceIPv4:  "8.8.8.8", // Hardcoded value in test file.
+		},
+		{
+			description:         "IPv6 - Defined In Request",
+			reqJSONFile:         "site-has-ipv6.json",
+			xForwardedForHeader: "1111::",
+			expectedDeviceIPv6:  "8888::", // Hardcoded value in test file.
+		},
+		{
+			description:         "IPv4 - Defined In Request - Private Network",
+			reqJSONFile:         "site-has-ipv4.json",
+			xForwardedForHeader: "1.1.1.1",
+			privateNetworksIPv4: []net.IPNet{{IP: net.IP{8, 8, 8, 0}, Mask: net.CIDRMask(24, 32)}}, // Hardcoded value in test file.
+			expectedDeviceIPv4:  "1.1.1.1",
+		},
+		{
+			description:         "IPv6 - Defined In Request - Private Network",
+			reqJSONFile:         "site-has-ipv6.json",
+			xForwardedForHeader: "1111::",
+			privateNetworksIPv6: []net.IPNet{{IP: net.ParseIP("8800::"), Mask: net.CIDRMask(8, 128)}}, // Hardcoded value in test file.
+			expectedDeviceIPv6:  "1111::",
+		},
+	}
 
-	httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, "site.json")))
-	httpReq.Header.Set("X-Forwarded-For", "123.456.78.90")
-	recorder := httptest.NewRecorder()
+	metrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{})
+	for _, test := range testCases {
+		exchange := &nobidExchange{}
+		cfg := &config.Configuration{
+			MaxRequestSize: maxSize,
+			RequestValidation: config.RequestValidation{
+				IPv4PrivateNetworksParsed: test.privateNetworksIPv4,
+				IPv6PrivateNetworksParsed: test.privateNetworksIPv6,
+			},
+		}
+		endpoint, _ := NewEndpoint(exchange, newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, cfg, metrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, []byte{}, openrtb_ext.BidderMap)
 
-	endpoint(recorder, httpReq, nil)
+		httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, test.reqJSONFile)))
+		httpReq.Header.Set("X-Forwarded-For", test.xForwardedForHeader)
 
-	if ex.gotRequest == nil {
-		t.Fatalf("The request never made it into the Exchange.")
-	}
+		endpoint(httptest.NewRecorder(), httpReq, nil)
 
-	if ex.gotRequest.Device.IP != "123.456.78.90" {
-		t.Errorf("Bad device IP. Expected 123.456.78.90, got %s", ex.gotRequest.Device.IP)
+		result := exchange.gotRequest
+		if !assert.NotEmpty(t, result, test.description+"Request received by the exchange.") {
+			t.FailNow()
+		}
+		assert.Equal(t, test.expectedDeviceIPv4, result.Device.IP, test.description+":ipv4")
+		assert.Equal(t, test.expectedDeviceIPv6, result.Device.IPv6, test.description+":ipv6")
 	}
 }
 
@@ -602,10 +657,26 @@ func TestStoredRequests(t *testing.T) {
 	// NewMetrics() will create a new go_metrics MetricsEngine, bypassing the need for a crafted configuration set to support it.
 	// As a side effect this gives us some coverage of the go_metrics piece of the metrics engine.
 	theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{})
-	edep := &endpointDeps{&nobidExchange{}, newParamsValidator(t), &mockStoredReqFetcher{}, empty_fetcher.EmptyFetcher{}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, theMetrics, analyticsConf.NewPBSAnalytics(&config.Analytics{}), map[string]string{}, false, []byte{}, openrtb_ext.BidderMap, nil, nil}
+	deps := &endpointDeps{
+		&nobidExchange{},
+		newParamsValidator(t),
+		&mockStoredReqFetcher{},
+		empty_fetcher.EmptyFetcher{},
+		empty_fetcher.EmptyFetcher{},
+		&config.Configuration{MaxRequestSize: maxSize},
+		theMetrics,
+		analyticsConf.NewPBSAnalytics(&config.Analytics{}),
+		map[string]string{},
+		false,
+		[]byte{},
+		openrtb_ext.BidderMap,
+		nil,
+		nil,
+		hardcodedResponseIPValidator{response: true},
+	}
 
 	for i, requestData := range testStoredRequests {
-		newRequest, errList := edep.processStoredRequests(context.Background(), json.RawMessage(requestData))
+		newRequest, errList := deps.processStoredRequests(context.Background(), json.RawMessage(requestData))
 		if len(errList) != 0 {
 			for _, err := range errList {
 				if err != nil {
@@ -640,6 +711,7 @@ func TestOversizedRequest(t *testing.T) {
 		openrtb_ext.BidderMap,
 		nil,
 		nil,
+		hardcodedResponseIPValidator{response: true},
 	}
 
 	req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(reqBody))
@@ -674,6 +746,7 @@ func TestRequestSizeEdgeCase(t *testing.T) {
 		openrtb_ext.BidderMap,
 		nil,
 		nil,
+		hardcodedResponseIPValidator{response: true},
 	}
 
 	req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(reqBody))
@@ -813,6 +886,7 @@ func TestDisabledBidder(t *testing.T) {
 		openrtb_ext.BidderMap,
 		nil,
 		nil,
+		hardcodedResponseIPValidator{response: true},
 	}
 
 	req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(reqBody))
@@ -848,6 +922,7 @@ func TestValidateImpExtDisabledBidder(t *testing.T) {
 		openrtb_ext.BidderMap,
 		nil,
 		nil,
+		hardcodedResponseIPValidator{response: true},
 	}
 	errs := deps.validateImpExt(imp, nil, 0)
 	assert.JSONEq(t, `{"appnexus":{"placement_id":555}}`, string(imp.Ext))
@@ -888,6 +963,7 @@ func TestCurrencyTrunc(t *testing.T) {
 		openrtb_ext.BidderMap,
 		nil,
 		nil,
+		hardcodedResponseIPValidator{response: true},
 	}
 
 	ui := uint64(1)
@@ -931,6 +1007,7 @@ func TestCCPAInvalid(t *testing.T) {
 		openrtb_ext.BidderMap,
 		nil,
 		nil,
+		hardcodedResponseIPValidator{response: true},
 	}
 
 	ui := uint64(1)
@@ -962,6 +1039,81 @@ func TestCCPAInvalid(t *testing.T) {
 	assert.Empty(t, req.Regs.Ext, "Invalid Consent Removed From Request")
 }
 
+func TestSanitizeRequest(t *testing.T) {
+	testCases := []struct {
+		description  string
+		req          *openrtb.BidRequest
+		ipValidator  iputil.IPValidator
+		expectedIPv4 string
+		expectedIPv6 string
+	}{
+		{
+			description: "Empty",
+			req: &openrtb.BidRequest{
+				Device: &openrtb.Device{
+					IP:   "",
+					IPv6: "",
+				},
+			},
+			expectedIPv4: "",
+			expectedIPv6: "",
+		},
+		{
+			description: "Valid",
+			req: &openrtb.BidRequest{
+				Device: &openrtb.Device{
+					IP:   "1.1.1.1",
+					IPv6: "1111::",
+				},
+			},
+			ipValidator:  hardcodedResponseIPValidator{response: true},
+			expectedIPv4: "1.1.1.1",
+			expectedIPv6: "1111::",
+		},
+		{
+			description: "Invalid",
+			req: &openrtb.BidRequest{
+				Device: &openrtb.Device{
+					IP:   "1.1.1.1",
+					IPv6: "1111::",
+				},
+			},
+			ipValidator:  hardcodedResponseIPValidator{response: false},
+			expectedIPv4: "",
+			expectedIPv6: "",
+		},
+		{
+			description: "Invalid - Wrong IP Types",
+			req: &openrtb.BidRequest{
+				Device: &openrtb.Device{
+					IP:   "1111::",
+					IPv6: "1.1.1.1",
+				},
+			},
+			ipValidator:  hardcodedResponseIPValidator{response: true},
+			expectedIPv4: "",
+			expectedIPv6: "",
+		},
+		{
+			description: "Malformed",
+			req: &openrtb.BidRequest{
+				Device: &openrtb.Device{
+					IP:   "malformed",
+					IPv6: "malformed",
+				},
+			},
+			expectedIPv4: "",
+			expectedIPv6: "",
+		},
+	}
+
+	for _, test := range testCases {
+		sanitizeRequest(test.req, test.ipValidator)
+		assert.Equal(t, test.expectedIPv4, test.req.Device.IP, test.description+":ipv4")
+		assert.Equal(t, test.expectedIPv6, test.req.Device.IPv6, test.description+":ipv6")
+	}
+}
+
 // nobidExchange is a well-behaved exchange which always bids "no bid".
 type nobidExchange struct {
 	gotRequest *openrtb.BidRequest
@@ -1385,3 +1537,11 @@ func newBidderInfo(cfg config.Adapter) adapters.BidderInfo {
 		Status: status,
 	}
 }
+
+type hardcodedResponseIPValidator struct {
+	response bool
+}
+
+func (v hardcodedResponseIPValidator) IsValid(net.IP, iputil.IPVersion) bool {
+	return v.response
+}
diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go
index 631cb277..f29ac3bf 100644
--- a/endpoints/openrtb2/video_auction_test.go
+++ b/endpoints/openrtb2/video_auction_test.go
@@ -1110,7 +1110,7 @@ func TestCCPA(t *testing.T) {
 func mockDepsWithMetrics(t *testing.T, ex *mockExchangeVideo) (*endpointDeps, *pbsmetrics.Metrics, *mockAnalyticsModule) {
 	theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{})
 	mockModule := &mockAnalyticsModule{}
-	edep := &endpointDeps{
+	deps := &endpointDeps{
 		ex,
 		newParamsValidator(t),
 		&mockVideoStoredReqFetcher{},
@@ -1125,9 +1125,10 @@ func mockDepsWithMetrics(t *testing.T, ex *mockExchangeVideo) (*endpointDeps, *p
 		openrtb_ext.BidderMap,
 		nil,
 		nil,
+		hardcodedResponseIPValidator{response: true},
 	}
 
-	return edep, theMetrics, mockModule
+	return deps, theMetrics, mockModule
 }
 
 type mockAnalyticsModule struct {
@@ -1151,7 +1152,7 @@ func (m *mockAnalyticsModule) LogAmpObject(ao *analytics.AmpObject) { return }
 
 func mockDeps(t *testing.T, ex *mockExchangeVideo) *endpointDeps {
 	theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{})
-	edep := &endpointDeps{
+	deps := &endpointDeps{
 		ex,
 		newParamsValidator(t),
 		&mockVideoStoredReqFetcher{},
@@ -1166,9 +1167,10 @@ func mockDeps(t *testing.T, ex *mockExchangeVideo) *endpointDeps {
 		openrtb_ext.BidderMap,
 		ex.cache,
 		regexp.MustCompile(`[<>]`),
+		hardcodedResponseIPValidator{response: true},
 	}
 
-	return edep
+	return deps
 }
 
 type mockCacheClient struct {
diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go
index fff397f0..b776715a 100644
--- a/exchange/bidder_test.go
+++ b/exchange/bidder_test.go
@@ -517,6 +517,7 @@ func TestMultiCurrencies(t *testing.T) {
 			&http.Client{},
 			mockedHTTPServer.URL,
 			time.Duration(10)*time.Second,
+			time.Duration(24)*time.Hour,
 		)
 		seatBid, errs := bidder.requestBid(
 			context.Background(),
@@ -831,6 +832,7 @@ func TestMultiCurrencies_RequestCurrencyPick(t *testing.T) {
 			&http.Client{},
 			mockedHTTPServer.URL,
 			time.Duration(10)*time.Second,
+			time.Duration(24)*time.Hour,
 		)
 		seatBid, errs := bidder.requestBid(
 			context.Background(),
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index 93cb60fb..161b24fd 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -16,19 +16,18 @@ import (
 	"time"
 
 	"github.com/prebid/prebid-server/adapters"
-	"github.com/prebid/prebid-server/currencies"
-	"github.com/prebid/prebid-server/prebid_cache_client"
-	"github.com/prebid/prebid-server/stored_requests"
-	"github.com/prebid/prebid-server/stored_requests/backends/file_fetcher"
-
-	"github.com/buger/jsonparser"
-	"github.com/mxmCherry/openrtb"
 	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/currencies"
 	"github.com/prebid/prebid-server/gdpr"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/pbsmetrics"
 	metricsConf "github.com/prebid/prebid-server/pbsmetrics/config"
 	pbc "github.com/prebid/prebid-server/prebid_cache_client"
+	"github.com/prebid/prebid-server/stored_requests"
+	"github.com/prebid/prebid-server/stored_requests/backends/file_fetcher"
+
+	"github.com/buger/jsonparser"
+	"github.com/mxmCherry/openrtb"
 	"github.com/rcrowley/go-metrics"
 	"github.com/stretchr/testify/assert"
 	"github.com/yudai/gojsondiff"
@@ -1837,7 +1836,7 @@ func (c *wellBehavedCache) GetExtCacheData() (string, string) {
 	return "www.pbcserver.com", "/pbcache/endpoint"
 }
 
-func (c *wellBehavedCache) PutJson(ctx context.Context, values []prebid_cache_client.Cacheable) ([]string, []error) {
+func (c *wellBehavedCache) PutJson(ctx context.Context, values []pbc.Cacheable) ([]string, []error) {
 	ids := make([]string, len(values))
 	for i := 0; i < len(values); i++ {
 		ids[i] = strconv.Itoa(i)
diff --git a/exchange/utils_test.go b/exchange/utils_test.go
index 4dad3f54..e50d0f77 100644
--- a/exchange/utils_test.go
+++ b/exchange/utils_test.go
@@ -80,7 +80,7 @@ func TestCleanOpenRTBRequests(t *testing.T) {
 	}
 
 	for _, test := range testCases {
-		reqByBidders, _, err := cleanOpenRTBRequests(context.Background(), test.req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig)
+		reqByBidders, _, _, err := cleanOpenRTBRequests(context.Background(), test.req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig)
 		if test.hasError {
 			assert.NotNil(t, err, "Error shouldn't be nil")
 		} else {
@@ -120,7 +120,7 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) {
 			},
 		}
 
-		results, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig)
+		results, _, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig)
 		result := results["appnexus"]
 
 		assert.Nil(t, errs)
@@ -182,7 +182,7 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) {
 			},
 		}
 
-		results, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig)
+		results, _, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig)
 		result := results["appnexus"]
 
 		assert.Nil(t, errs)
diff --git a/main_test.go b/main_test.go
index d7dc9dd2..70eea282 100644
--- a/main_test.go
+++ b/main_test.go
@@ -5,6 +5,7 @@ import (
 	"testing"
 
 	"github.com/prebid/prebid-server/config"
+	"github.com/stretchr/testify/assert"
 
 	"github.com/spf13/viper"
 )
@@ -56,10 +57,11 @@ func TestViperEnv(t *testing.T) {
 	ttl := forceEnv(t, "PBS_HOST_COOKIE_TTL_DAYS", "60")
 	defer ttl()
 
-	// Basic config set
-	compareStrings(t, "Viper error: port expected to be %s, found %s", "7777", v.Get("port").(string))
-	// Nested config set
-	compareStrings(t, "Viper error: adapters.pubmatic.endpoint expected to be %s, found %s", "not_an_endpoint", v.Get("adapters.pubmatic.endpoint").(string))
-	// Config set with underscores
-	compareStrings(t, "Viper error: host_cookie.ttl_days expected to be %s, found %s", "60", v.Get("host_cookie.ttl_days").(string))
+	ipv4Networks := forceEnv(t, "PBS_REQUEST_VALIDATION_IPV4_PRIVATE_NETWORKS", "1.1.1.1/24 2.2.2.2/24")
+	defer ipv4Networks()
+
+	assert.Equal(t, 7777, v.Get("port"), "Basic Config")
+	assert.Equal(t, "not_an_endpoint", v.Get("adapters.pubmatic.endpoint"), "Nested Config")
+	assert.Equal(t, 60, v.Get("host_cookie.ttl_days"), "Config With Underscores")
+	assert.ElementsMatch(t, []string{"1.1.1.1/24", "2.2.2.2/24"}, v.Get("request_validation.ipv4_private_networks"), "Arrays")
 }
diff --git a/pbsmetrics/go_metrics_test.go b/pbsmetrics/go_metrics_test.go
index 25f75e77..6d9eaf9f 100644
--- a/pbsmetrics/go_metrics_test.go
+++ b/pbsmetrics/go_metrics_test.go
@@ -56,6 +56,10 @@ func TestNewMetrics(t *testing.T) {
 
 	ensureContains(t, registry, "timeout_notification.ok", m.TimeoutNotificationSuccess)
 	ensureContains(t, registry, "timeout_notification.failed", m.TimeoutNotificationFailure)
+	ensureContains(t, registry, "privacy.request.tcf.v1", m.TCFReqVersion[TCFVersionV1])
+	ensureContains(t, registry, "privacy.request.tcf.v2", m.TCFReqVersion[TCFVersionV2])
+	ensureContains(t, registry, "privacy.request.tcf.err", m.TCFReqVersion[TCFVersionErr])
+
 }
 
 func TestRecordBidType(t *testing.T) {
diff --git a/pbsmetrics/prometheus/prometheus_test.go b/pbsmetrics/prometheus/prometheus_test.go
index 24c50492..03daff0d 100644
--- a/pbsmetrics/prometheus/prometheus_test.go
+++ b/pbsmetrics/prometheus/prometheus_test.go
@@ -930,13 +930,13 @@ func TestTimeoutNotifications(t *testing.T) {
 	m.RecordTimeoutNotice(true)
 	m.RecordTimeoutNotice(false)
 
-	assertCounterVecValue(t, "", "timeout_notifications:ok", m.timeout_notifications,
+	assertCounterVecValue(t, "", "timeout_notifications:ok", m.timeoutNotifications,
 		float64(2),
 		prometheus.Labels{
 			successLabel: requestSuccessful,
 		})
 
-	assertCounterVecValue(t, "", "timeout_notifications:fail", m.timeout_notifications,
+	assertCounterVecValue(t, "", "timeout_notifications:fail", m.timeoutNotifications,
 		float64(1),
 		prometheus.Labels{
 			successLabel: requestFailed,
@@ -944,6 +944,36 @@ func TestTimeoutNotifications(t *testing.T) {
 
 }
 
+func TestTCFMetrics(t *testing.T) {
+	m := createMetricsForTesting()
+
+	m.RecordTCFReq(pbsmetrics.TCFVersionToValue(0))
+	m.RecordTCFReq(pbsmetrics.TCFVersionToValue(1))
+	m.RecordTCFReq(pbsmetrics.TCFVersionToValue(2))
+	m.RecordTCFReq(pbsmetrics.TCFVersionToValue(1))
+
+	assertCounterVecValue(t, "", "privacy_tcf:err", m.tcfVersion,
+		float64(1),
+		prometheus.Labels{
+			versionLabel: "err",
+			sourceLabel:  sourceRequest,
+		})
+
+	assertCounterVecValue(t, "", "privacy_tcf:v1", m.tcfVersion,
+		float64(2),
+		prometheus.Labels{
+			versionLabel: "v1",
+			sourceLabel:  sourceRequest,
+		})
+
+	assertCounterVecValue(t, "", "privacy_tcf:v2", m.tcfVersion,
+		float64(1),
+		prometheus.Labels{
+			versionLabel: "v2",
+			sourceLabel:  sourceRequest,
+		})
+}
+
 func assertCounterValue(t *testing.T, description, name string, counter prometheus.Counter, expected float64) {
 	m := dto.Metric{}
 	counter.Write(&m)
diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go
index 363cd491..b23541ea 100755
--- a/usersync/usersyncers/syncer_test.go
+++ b/usersync/usersyncers/syncer_test.go
@@ -18,6 +18,7 @@ func TestNewSyncerMap(t *testing.T) {
 			string(openrtb_ext.BidderAdform):           syncConfig,
 			string(openrtb_ext.BidderAdkernel):         syncConfig,
 			string(openrtb_ext.BidderAdkernelAdn):      syncConfig,
+			string(openrtb_ext.BidderAdman):            syncConfig,
 			string(openrtb_ext.BidderAdmixer):          syncConfig,
 			string(openrtb_ext.BidderAdOcean):          syncConfig,
 			string(openrtb_ext.BidderAdpone):           syncConfig,
diff --git a/util/httputil/httputil_test.go b/util/httputil/httputil_test.go
new file mode 100644
index 00000000..f7166740
--- /dev/null
+++ b/util/httputil/httputil_test.go
@@ -0,0 +1,327 @@
+package httputil
+
+import (
+	"crypto/tls"
+	"net"
+	"net/http"
+	"testing"
+
+	"github.com/prebid/prebid-server/util/iputil"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestIsSecure(t *testing.T) {
+	testCases := []struct {
+		description     string
+		url             string
+		xForwardedProto string
+		tls             bool
+		expectIsSecure  bool
+	}{
+		{
+			description:    "HTTP",
+			url:            "http://host.com",
+			expectIsSecure: false,
+		},
+		{
+			description:     "HTTPS - Forwarded Protocol",
+			url:             "http://host.com",
+			xForwardedProto: "https",
+			expectIsSecure:  true,
+		},
+		{
+			description:     "HTTPS - Forwarded Protocol - Case Insensitive",
+			url:             "http://host.com",
+			xForwardedProto: "HTTPS",
+			expectIsSecure:  true,
+		},
+		{
+			description:    "HTTPS - Protocol",
+			url:            "https://host.com",
+			expectIsSecure: true,
+		},
+		{
+			description:    "HTTPS - Protocol - Case Insensitive",
+			url:            "HTTPS://host.com",
+			expectIsSecure: true,
+		},
+		{
+			description:    "HTTPS - TLS",
+			url:            "http://host.com",
+			tls:            true,
+			expectIsSecure: true,
+		},
+	}
+
+	for _, test := range testCases {
+		request, err := http.NewRequest("GET", test.url, nil)
+		if err != nil {
+			t.Fatalf("Unable to create test http request. Err: %v", err)
+		}
+		if test.xForwardedProto != "" {
+			request.Header.Add("X-Forwarded-Proto", test.xForwardedProto)
+		}
+		if test.tls {
+			request.TLS = &tls.ConnectionState{}
+		}
+
+		result := IsSecure(request)
+
+		assert.Equal(t, test.expectIsSecure, result, test.description)
+	}
+}
+
+func TestFindIP(t *testing.T) {
+	alwaysTrue := hardcodedResponseIPValidator{response: true}
+	alwaysFalse := hardcodedResponseIPValidator{response: false}
+
+	testCases := []struct {
+		description   string
+		trueClientIP  string
+		xForwardedFor string
+		xRealIP       string
+		remoteAddr    string
+		validator     iputil.IPValidator
+		expectedIP    net.IP
+		expectedVer   iputil.IPVersion
+	}{
+		{
+			description: "No Address",
+			expectedIP:  nil,
+			expectedVer: iputil.IPvUnknown,
+		},
+		{
+			description:   "False Validator - IPv4",
+			trueClientIP:  "1.1.1.1",
+			xForwardedFor: "2.2.2.2, 3.3.3.3",
+			xRealIP:       "4.4.4.4",
+			remoteAddr:    "5.5.5.5:5",
+			validator:     alwaysFalse,
+			expectedIP:    nil,
+			expectedVer:   iputil.IPvUnknown,
+		},
+		{
+			description:   "False Validator - IPv6",
+			trueClientIP:  "1111::",
+			xForwardedFor: "2222::, 3333::",
+			xRealIP:       "4444::",
+			remoteAddr:    "[5555::]:5]",
+			validator:     alwaysFalse,
+			expectedIP:    nil,
+			expectedVer:   iputil.IPvUnknown,
+		},
+		{
+			description:   "True Validator - IPv4 - True Client IP",
+			trueClientIP:  "1.1.1.1",
+			xForwardedFor: "2.2.2.2, 3.3.3.3",
+			xRealIP:       "4.4.4.4",
+			remoteAddr:    "5.5.5.5:5",
+			validator:     alwaysTrue,
+			expectedIP:    net.ParseIP("1.1.1.1"),
+			expectedVer:   iputil.IPv4,
+		},
+		{
+			description:   "True Validator - IPv4 - True Client IP - Ignore Whitespace",
+			trueClientIP:  "   1.1.1.1 ",
+			xForwardedFor: "2.2.2.2, 3.3.3.3",
+			xRealIP:       "4.4.4.4",
+			remoteAddr:    "5.5.5.5:5",
+			validator:     alwaysTrue,
+			expectedIP:    net.ParseIP("1.1.1.1"),
+			expectedVer:   iputil.IPv4,
+		},
+		{
+			description:   "True Validator - IPv4 - X Forwarded For",
+			trueClientIP:  "",
+			xForwardedFor: "2.2.2.2, 3.3.3.3",
+			xRealIP:       "4.4.4.4",
+			remoteAddr:    "5.5.5.5:5",
+			validator:     alwaysTrue,
+			expectedIP:    net.ParseIP("2.2.2.2"),
+			expectedVer:   iputil.IPv4,
+		},
+		{
+			description:   "True Validator - IPv4 - X Forwarded For - Ignore Whitespace",
+			trueClientIP:  "",
+			xForwardedFor: "   2.2.2.2, 3.3.3.3 ",
+			xRealIP:       "4.4.4.4",
+			remoteAddr:    "5.5.5.5:5",
+			validator:     alwaysTrue,
+			expectedIP:    net.ParseIP("2.2.2.2"),
+			expectedVer:   iputil.IPv4,
+		},
+		{
+			description:   "True Validator - IPv4 - X Real IP",
+			trueClientIP:  "",
+			xForwardedFor: "",
+			xRealIP:       "4.4.4.4",
+			remoteAddr:    "5.5.5.5:5",
+			validator:     alwaysTrue,
+			expectedIP:    net.ParseIP("4.4.4.4"),
+			expectedVer:   iputil.IPv4,
+		},
+		{
+			description:   "True Validator - IPv4 - X Real IP - Ignore Whitespace",
+			trueClientIP:  "",
+			xForwardedFor: "",
+			xRealIP:       "   4.4.4.4 ",
+			remoteAddr:    "5.5.5.5:5",
+			validator:     alwaysTrue,
+			expectedIP:    net.ParseIP("4.4.4.4"),
+			expectedVer:   iputil.IPv4,
+		},
+		{
+			description:   "True Validator - IPv4 - Remote Address",
+			trueClientIP:  "",
+			xForwardedFor: "",
+			xRealIP:       "",
+			remoteAddr:    "5.5.5.5:80",
+			validator:     alwaysTrue,
+			expectedIP:    net.ParseIP("5.5.5.5"),
+			expectedVer:   iputil.IPv4,
+		},
+		{
+			description:   "True Validator - IPv6 - True Client IP",
+			trueClientIP:  "1111::",
+			xForwardedFor: "2222::, 3333::",
+			xRealIP:       "4444::",
+			remoteAddr:    "[5555::]:5",
+			validator:     alwaysTrue,
+			expectedIP:    net.ParseIP("1111::"),
+			expectedVer:   iputil.IPv6,
+		},
+		{
+			description:   "True Validator - IPv6 - True Client IP - Ignore Whitespace",
+			trueClientIP:  "   1111:: ",
+			xForwardedFor: "2222::, 3333::",
+			xRealIP:       "4444::",
+			remoteAddr:    "[5555::]:5",
+			validator:     alwaysTrue,
+			expectedIP:    net.ParseIP("1111::"),
+			expectedVer:   iputil.IPv6,
+		},
+		{
+			description:   "True Validator - IPv6 - X Forwarded For",
+			trueClientIP:  "",
+			xForwardedFor: "2222::, 3333::",
+			xRealIP:       "4444::",
+			remoteAddr:    "[5555::]:5",
+			validator:     alwaysTrue,
+			expectedIP:    net.ParseIP("2222::"),
+			expectedVer:   iputil.IPv6,
+		},
+		{
+			description:   "True Validator - IPv6 - X Forwarded For - Ignore Whitespace",
+			trueClientIP:  "",
+			xForwardedFor: "   2222::, 3333:: ",
+			xRealIP:       "4444::",
+			remoteAddr:    "[5555::]:5",
+			validator:     alwaysTrue,
+			expectedIP:    net.ParseIP("2222::"),
+			expectedVer:   iputil.IPv6,
+		},
+		{
+			description:   "True Validator - IPv6 - X Real IP",
+			trueClientIP:  "",
+			xForwardedFor: "",
+			xRealIP:       "4444::",
+			remoteAddr:    "[5555::]:5",
+			validator:     alwaysTrue,
+			expectedIP:    net.ParseIP("4444::"),
+			expectedVer:   iputil.IPv6,
+		},
+		{
+			description:   "True Validator - IPv6 - X Real IP - Ignore Whitespace",
+			trueClientIP:  "",
+			xForwardedFor: "",
+			xRealIP:       "    4444:: ",
+			remoteAddr:    "[5555::]:5",
+			validator:     alwaysTrue,
+			expectedIP:    net.ParseIP("4444::"),
+			expectedVer:   iputil.IPv6,
+		},
+		{
+			description:   "True Validator - IPv6 - Remote Address",
+			trueClientIP:  "",
+			xForwardedFor: "",
+			xRealIP:       "",
+			remoteAddr:    "[5555::]:5",
+			validator:     alwaysTrue,
+			expectedIP:    net.ParseIP("5555::"),
+			expectedVer:   iputil.IPv6,
+		},
+		{
+			description:   "True Validator - Malformed - All",
+			trueClientIP:  "malformed",
+			xForwardedFor: "malformed",
+			xRealIP:       "malformed",
+			remoteAddr:    "malformed",
+			validator:     alwaysTrue,
+			expectedIP:    nil,
+			expectedVer:   iputil.IPvUnknown,
+		},
+		{
+			description:   "True Validator - Malformed - Some",
+			trueClientIP:  "malformed",
+			xForwardedFor: "malformed",
+			xRealIP:       "4.4.4.4",
+			remoteAddr:    "malformed",
+			validator:     alwaysTrue,
+			expectedIP:    net.ParseIP("4.4.4.4"),
+			expectedVer:   iputil.IPv4,
+		},
+		{
+			description:   "True Validator - Malformed - X Forwarded For - IPv4",
+			trueClientIP:  "malformed",
+			xForwardedFor: "malformed, 4.4.4.4, 3333::, malformed",
+			xRealIP:       "malformed",
+			remoteAddr:    "malformed",
+			validator:     alwaysTrue,
+			expectedIP:    net.ParseIP("4.4.4.4"),
+			expectedVer:   iputil.IPv4,
+		},
+		{
+			description:   "True Validator - Malformed - X Forwarded For - IPv6",
+			trueClientIP:  "malformed",
+			xForwardedFor: "malformed, 3333::, 4.4.4.4, malformed",
+			xRealIP:       "malformed",
+			remoteAddr:    "malformed",
+			validator:     alwaysTrue,
+			expectedIP:    net.ParseIP("3333::"),
+			expectedVer:   iputil.IPv6,
+		},
+	}
+
+	for _, test := range testCases {
+		// Build Request
+		request, err := http.NewRequest("GET", "http://anyurl.com", nil)
+		if err != nil {
+			t.Fatalf("Unable to create test http request. Err: %v", err)
+		}
+		if test.trueClientIP != "" {
+			request.Header.Add("True-Client-IP", test.trueClientIP)
+		}
+		if test.xForwardedFor != "" {
+			request.Header.Add("X-Forwarded-For", test.xForwardedFor)
+		}
+		if test.xRealIP != "" {
+			request.Header.Add("X-Real-IP", test.xRealIP)
+		}
+		request.RemoteAddr = test.remoteAddr
+
+		// Run Test
+		ip, ver := FindIP(request, test.validator)
+
+		// Assertions
+		assert.Equal(t, test.expectedIP, ip, test.description+":ip")
+		assert.Equal(t, test.expectedVer, ver, test.description+":ver")
+	}
+}
+
+type hardcodedResponseIPValidator struct {
+	response bool
+}
+
+func (v hardcodedResponseIPValidator) IsValid(net.IP, iputil.IPVersion) bool {
+	return v.response
+}
diff --git a/util/iputil/parse_test.go b/util/iputil/parse_test.go
new file mode 100644
index 00000000..53431b0f
--- /dev/null
+++ b/util/iputil/parse_test.go
@@ -0,0 +1,30 @@
+package iputil
+
+import (
+	"net"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestParseIP(t *testing.T) {
+	testCases := []struct {
+		input       string
+		expectedVer IPVersion
+		expectedIP  net.IP
+	}{
+		{"", IPvUnknown, nil},
+		{"1.1.1.1", IPv4, net.IPv4(1, 1, 1, 1)},
+		{"-1.-1.-1.-1", IPvUnknown, nil},
+		{"256.256.256.256", IPvUnknown, nil},
+		{"::ffff:1.1.1.1", IPv6, net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 1, 1, 1, 1}},
+		{"0101::", IPv6, net.IP{1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
+		{"zzzz::", IPvUnknown, nil},
+	}
+
+	for _, test := range testCases {
+		ip, ver := ParseIP(test.input)
+		assert.Equal(t, test.expectedVer, ver)
+		assert.Equal(t, test.expectedIP, ip)
+	}
+}
diff --git a/util/iputil/validator_test.go b/util/iputil/validator_test.go
new file mode 100644
index 00000000..4419af22
--- /dev/null
+++ b/util/iputil/validator_test.go
@@ -0,0 +1,222 @@
+package iputil
+
+import (
+	"net"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestPublicNetworkIPValidator(t *testing.T) {
+	ipv4Network1 := net.IPNet{IP: net.ParseIP("1.0.0.0"), Mask: net.CIDRMask(8, 32)}
+	ipv4Network2 := net.IPNet{IP: net.ParseIP("2.0.0.0"), Mask: net.CIDRMask(8, 32)}
+
+	ipv6Network1 := net.IPNet{IP: net.ParseIP("3300::"), Mask: net.CIDRMask(8, 128)}
+	ipv6Network2 := net.IPNet{IP: net.ParseIP("4400::"), Mask: net.CIDRMask(8, 128)}
+
+	testCases := []struct {
+		description         string
+		ip                  net.IP
+		ver                 IPVersion
+		ipv4PrivateNetworks []net.IPNet
+		ipv6PrivateNetworks []net.IPNet
+		expected            bool
+	}{
+		{
+			description:         "IPv4 - Public - None",
+			ip:                  net.ParseIP("1.1.1.1"),
+			ver:                 IPv4,
+			ipv4PrivateNetworks: []net.IPNet{},
+			ipv6PrivateNetworks: []net.IPNet{},
+			expected:            true,
+		},
+		{
+			description:         "IPv4 - Public - One",
+			ip:                  net.ParseIP("2.2.2.2"),
+			ver:                 IPv4,
+			ipv4PrivateNetworks: []net.IPNet{ipv4Network1},
+			ipv6PrivateNetworks: []net.IPNet{},
+			expected:            true,
+		},
+		{
+			description:         "IPv4 - Public - Many",
+			ip:                  net.ParseIP("3.3.3.3"),
+			ver:                 IPv4,
+			ipv4PrivateNetworks: []net.IPNet{ipv4Network1, ipv4Network2},
+			ipv6PrivateNetworks: []net.IPNet{},
+			expected:            true,
+		},
+		{
+			description:         "IPv4 - Private - One",
+			ip:                  net.ParseIP("1.1.1.1"),
+			ver:                 IPv4,
+			ipv4PrivateNetworks: []net.IPNet{ipv4Network1},
+			ipv6PrivateNetworks: []net.IPNet{},
+			expected:            false,
+		},
+		{
+			description:         "IPv4 - Private - Many",
+			ip:                  net.ParseIP("2.2.2.2"),
+			ver:                 IPv4,
+			ipv4PrivateNetworks: []net.IPNet{ipv4Network1, ipv4Network2},
+			ipv6PrivateNetworks: []net.IPNet{},
+			expected:            false,
+		},
+		{
+			description:         "IPv6 - Public - None",
+			ip:                  net.ParseIP("3333::"),
+			ver:                 IPv6,
+			ipv4PrivateNetworks: []net.IPNet{},
+			ipv6PrivateNetworks: []net.IPNet{},
+			expected:            true,
+		},
+		{
+			description:         "IPv6 - Public - One",
+			ip:                  net.ParseIP("4444::"),
+			ver:                 IPv6,
+			ipv4PrivateNetworks: []net.IPNet{},
+			ipv6PrivateNetworks: []net.IPNet{ipv6Network1},
+			expected:            true,
+		},
+		{
+			description:         "IPv6 - Public - Many",
+			ip:                  net.ParseIP("5555::"),
+			ver:                 IPv6,
+			ipv4PrivateNetworks: []net.IPNet{},
+			ipv6PrivateNetworks: []net.IPNet{ipv6Network1, ipv6Network2},
+			expected:            true,
+		},
+		{
+			description:         "IPv6 - Private - One",
+			ip:                  net.ParseIP("3333::"),
+			ver:                 IPv6,
+			ipv4PrivateNetworks: []net.IPNet{},
+			ipv6PrivateNetworks: []net.IPNet{ipv6Network1},
+			expected:            false,
+		},
+		{
+			description:         "IPv6 - Private - Many",
+			ip:                  net.ParseIP("4444::"),
+			ver:                 IPv6,
+			ipv4PrivateNetworks: []net.IPNet{},
+			ipv6PrivateNetworks: []net.IPNet{ipv6Network1, ipv6Network2},
+			expected:            false,
+		},
+		{
+			description:         "Mixed - Unknown",
+			ip:                  net.ParseIP("3.3.3.3"),
+			ver:                 IPvUnknown,
+			ipv4PrivateNetworks: []net.IPNet{ipv4Network1, ipv4Network1},
+			ipv6PrivateNetworks: []net.IPNet{ipv6Network1, ipv6Network2},
+			expected:            false,
+		},
+		{
+			description:         "Mixed - Public - IPv4",
+			ip:                  net.ParseIP("3.3.3.3"),
+			ver:                 IPv4,
+			ipv4PrivateNetworks: []net.IPNet{ipv4Network1, ipv4Network1},
+			ipv6PrivateNetworks: []net.IPNet{ipv6Network1, ipv6Network2},
+			expected:            true,
+		},
+		{
+			description:         "Mixed - Public - IPv6",
+			ip:                  net.ParseIP("5555::"),
+			ver:                 IPv6,
+			ipv4PrivateNetworks: []net.IPNet{ipv4Network1, ipv4Network1},
+			ipv6PrivateNetworks: []net.IPNet{ipv6Network1, ipv6Network2},
+			expected:            true,
+		},
+		{
+			description:         "Mixed - Private - IPv4",
+			ip:                  net.ParseIP("1.1.1.1"),
+			ver:                 IPv4,
+			ipv4PrivateNetworks: []net.IPNet{ipv4Network1, ipv4Network1},
+			ipv6PrivateNetworks: []net.IPNet{ipv6Network1, ipv6Network2},
+			expected:            false,
+		},
+		{
+			description:         "Mixed - Private - IPv6",
+			ip:                  net.ParseIP("3333::"),
+			ver:                 IPv6,
+			ipv4PrivateNetworks: []net.IPNet{ipv4Network1, ipv4Network1},
+			ipv6PrivateNetworks: []net.IPNet{ipv6Network1, ipv6Network2},
+			expected:            false,
+		},
+		{
+			description:         "Mixed - Public - IPv6 Encoded IPv4",
+			ip:                  net.ParseIP("::FFFF:1.1.1.1"),
+			ver:                 IPv6,
+			ipv4PrivateNetworks: []net.IPNet{{IP: net.ParseIP("1.0.0.0"), Mask: net.CIDRMask(8, 32)}},
+			ipv6PrivateNetworks: []net.IPNet{{IP: net.ParseIP("::FFFF:2.0.0.0"), Mask: net.CIDRMask(108, 128)}},
+			expected:            true,
+		},
+		{
+			description:         "Mixed - Private - IPv6 Encoded IPv4",
+			ip:                  net.ParseIP("::FFFF:2.2.2.2"),
+			ver:                 IPv6,
+			ipv4PrivateNetworks: []net.IPNet{{IP: net.ParseIP("1.0.0.0"), Mask: net.CIDRMask(8, 32)}},
+			ipv6PrivateNetworks: []net.IPNet{{IP: net.ParseIP("::FFFF:2.0.0.0"), Mask: net.CIDRMask(108, 128)}},
+			expected:            false,
+		},
+	}
+
+	for _, test := range testCases {
+		requestValidation := PublicNetworkIPValidator{
+			IPv4PrivateNetworks: test.ipv4PrivateNetworks,
+			IPv6PrivateNetworks: test.ipv6PrivateNetworks,
+		}
+
+		result := requestValidation.IsValid(test.ip, test.ver)
+
+		assert.Equal(t, test.expected, result, test.description)
+	}
+}
+
+func TestVersionIPValidator(t *testing.T) {
+	testCases := []struct {
+		description      string
+		validatorVersion IPVersion
+		ip               net.IP
+		ipVer            IPVersion
+		expected         bool
+	}{
+		{
+			description:      "IPv4",
+			validatorVersion: IPv4,
+			ip:               net.ParseIP("1.1.1.1"),
+			ipVer:            IPv4,
+			expected:         true,
+		},
+		{
+			description:      "IPv4 - Given Unknown",
+			validatorVersion: IPv4,
+			ip:               nil,
+			ipVer:            IPvUnknown,
+			expected:         false,
+		},
+		{
+			description:      "IPv6",
+			validatorVersion: IPv6,
+			ip:               net.ParseIP("1111::"),
+			ipVer:            IPv6,
+			expected:         true,
+		},
+		{
+			description:      "IPv6 - Given Unknown",
+			validatorVersion: IPv6,
+			ip:               nil,
+			ipVer:            IPvUnknown,
+			expected:         false,
+		},
+	}
+
+	for _, test := range testCases {
+		m := VersionIPValidator{
+			Version: test.validatorVersion,
+		}
+
+		result := m.IsValid(test.ip, test.ipVer)
+
+		assert.Equal(t, test.expected, result)
+	}
+}
diff --git a/util/maputil/maputil_test.go b/util/maputil/maputil_test.go
new file mode 100644
index 00000000..2e6955ce
--- /dev/null
+++ b/util/maputil/maputil_test.go
@@ -0,0 +1,113 @@
+package maputil
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestReadEmbeddedMap(t *testing.T) {
+	testCases := []struct {
+		description string
+		value       map[string]interface{}
+		key         string
+		expectedMap map[string]interface{}
+		expectedOK  bool
+	}{
+		{
+			description: "Nil",
+			value:       nil,
+			key:         "",
+			expectedMap: nil,
+			expectedOK:  false,
+		},
+		{
+			description: "Empty",
+			value:       map[string]interface{}{},
+			key:         "foo",
+			expectedMap: nil,
+			expectedOK:  false,
+		},
+		{
+			description: "Success",
+			value:       map[string]interface{}{"foo": map[string]interface{}{"bar": 42}},
+			key:         "foo",
+			expectedMap: map[string]interface{}{"bar": 42},
+			expectedOK:  true,
+		},
+		{
+			description: "Not Found",
+			value:       map[string]interface{}{"foo": map[string]interface{}{"bar": 42}},
+			key:         "notFound",
+			expectedMap: nil,
+			expectedOK:  false,
+		},
+		{
+			description: "Wrong Type",
+			value:       map[string]interface{}{"foo": 42},
+			key:         "foo",
+			expectedMap: nil,
+			expectedOK:  false,
+		},
+	}
+
+	for _, test := range testCases {
+		resultMap, resultOK := ReadEmbeddedMap(test.value, test.key)
+
+		assert.Equal(t, test.expectedMap, resultMap, test.description+":map")
+		assert.Equal(t, test.expectedOK, resultOK, test.description+":ok")
+	}
+}
+
+func TestReadEmbeddedSlice(t *testing.T) {
+	testCases := []struct {
+		description   string
+		value         map[string]interface{}
+		key           string
+		expectedSlice []interface{}
+		expectedOK    bool
+	}{
+		{
+			description:   "Nil",
+			value:         nil,
+			key:           "",
+			expectedSlice: nil,
+			expectedOK:    false,
+		},
+		{
+			description:   "Empty",
+			value:         map[string]interface{}{},
+			key:           "foo",
+			expectedSlice: nil,
+			expectedOK:    false,
+		},
+		{
+			description:   "Success",
+			value:         map[string]interface{}{"foo": []interface{}{42}},
+			key:           "foo",
+			expectedSlice: []interface{}{42},
+			expectedOK:    true,
+		},
+		{
+			description:   "Not Found",
+			value:         map[string]interface{}{"foo": []interface{}{42}},
+			key:           "notFound",
+			expectedSlice: nil,
+			expectedOK:    false,
+		},
+		{
+			description:   "Wrong Type",
+			value:         map[string]interface{}{"foo": 42},
+			key:           "foo",
+			expectedSlice: nil,
+			expectedOK:    false,
+		},
+	}
+
+	for _, test := range testCases {
+		resultSlice, resultOK := ReadEmbeddedSlice(test.value, test.key)
+
+		assert.Equal(t, test.expectedSlice, resultSlice, test.description+":slicd")
+		assert.Equal(t, test.expectedOK, resultOK, test.description+":ok")
+	}
+}