Repository: prebid/prebid-server
Pull request #2166 (2022-02-23 01:20:57)

[Sharethrough] Bid Adapter Revamp connecting to new Endpoint
We have a new endpoint to connect to, leveraging Open RTB format, so we're refreshing our old adapter and all its custom logic.

Changes are mostly cleaning up old complex logic, and keeping it simple from open RTB to open RTB. We also removed all the custom unit tests to leverage the json-test framework.

maphe: Thanks @SyntaxNode for the in depth review! I haven't had the time to dive deep into it today but I responded inline with a couple of questions. I'll address the rest tomorrow. Go is not my primary language, as you probably figured 😆 but I'll do my best.
SyntaxNode: > Go is not my primary language, as you probably figured 😆 but I'll do my best.

Not a problem. Your code looks good to me. Happy to provide guidance.

Patch:
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index 1eb13746..d8f969b5 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -10,7 +10,7 @@ jobs:
   validate:
     strategy:
       matrix:
-        go-version: [1.15.x, 1.16.x]
+        go-version: [1.16.x, 1.17.x]
         os: [ubuntu-18.04]
     runs-on: ${{ matrix.os }}
     
diff --git a/README.md b/README.md
index 64486fd4..dab5523e 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ Please consider [registering your Prebid Server](https://docs.prebid.org/prebid-
 
 ## Installation
 
-First install [Go](https://golang.org/doc/install) version 1.15 or newer.
+First install [Go](https://golang.org/doc/install) version 1.16 or newer.
 
 Note that prebid-server is using [Go modules](https://blog.golang.org/using-go-modules).
 We officially support the most recent two major versions of the Go runtime. However, if you'd like to use a version <1.13 and are inside GOPATH `GO111MODULE` needs to be set to `GO111MODULE=on`.
diff --git a/account/account.go b/account/account.go
index 1b2cf158..fc38b943 100644
--- a/account/account.go
+++ b/account/account.go
@@ -5,7 +5,8 @@ import (
 	"encoding/json"
 	"fmt"
 
-	jsonpatch "github.com/evanphx/json-patch/v5"
+	jsonpatch "gopkg.in/evanphx/json-patch.v4"
+
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/metrics"
diff --git a/adapters/adot/adot.go b/adapters/adot/adot.go
index 5f99cf84..55aa0b95 100644
--- a/adapters/adot/adot.go
+++ b/adapters/adot/adot.go
@@ -37,6 +37,7 @@ func Builder(bidderName openrtb_ext.BidderName, config config.Adapter) (adapters
 // MakeRequests makes the HTTP requests which should be made to fetch bids.
 func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
 	var reqJSON []byte
+	var publisherPath string
 	var err error
 
 	if reqJSON, err = json.Marshal(request); err != nil {
@@ -46,9 +47,17 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.E
 	headers := http.Header{}
 	headers.Add("Content-Type", "application/json;charset=utf-8")
 
+	if adotExt := getImpAdotExt(&request.Imp[0]); adotExt != nil {
+		publisherPath = adotExt.PublisherPath
+	} else {
+		publisherPath = ""
+	}
+
+	endpoint := strings.Replace(a.endpoint, "{PUBLISHER_PATH}", publisherPath, -1)
+
 	return []*adapters.RequestData{{
 		Method:  "POST",
-		Uri:     a.endpoint,
+		Uri:     endpoint,
 		Body:    reqJSON,
 		Headers: headers,
 	}}, nil
@@ -129,3 +138,18 @@ func resolveMacros(bid *openrtb2.Bid) {
 	bid.NURL = strings.Replace(bid.NURL, "${AUCTION_PRICE}", price, -1)
 	bid.AdM = strings.Replace(bid.AdM, "${AUCTION_PRICE}", price, -1)
 }
+
+// getImpAdotExt parses and return first imp ext or nil
+func getImpAdotExt(imp *openrtb2.Imp) *openrtb_ext.ExtImpAdot {
+	var extImpAdot openrtb_ext.ExtImpAdot
+	var extBidder adapters.ExtImpBidder
+	err := json.Unmarshal(imp.Ext, &extBidder)
+	if err != nil {
+		return nil
+	}
+	err = json.Unmarshal(extBidder.Bidder, &extImpAdot)
+	if err != nil {
+		return nil
+	}
+	return &extImpAdot
+}
diff --git a/adapters/sharethrough/butler.go b/adapters/sharethrough/butler.go
deleted file mode 100644
index b7dd5003..00000000
--- a/adapters/sharethrough/butler.go
+++ /dev/null
@@ -1,264 +0,0 @@
-package sharethrough
-
-import (
-	"encoding/json"
-	"fmt"
-	"net/http"
-	"net/url"
-	"regexp"
-	"strconv"
-	"time"
-
-	"github.com/mxmCherry/openrtb/v15/openrtb2"
-	"github.com/prebid/prebid-server/adapters"
-	"github.com/prebid/prebid-server/errortypes"
-	"github.com/prebid/prebid-server/openrtb_ext"
-	"github.com/prebid/prebid-server/privacy/ccpa"
-)
-
-const defaultTmax = 10000 // 10 sec
-
-type StrAdSeverParams struct {
-	Pkey               string
-	BidID              string
-	GPID               string
-	ConsentRequired    bool
-	ConsentString      string
-	USPrivacySignal    string
-	InstantPlayCapable bool
-	Iframe             bool
-	Height             uint64
-	Width              uint64
-	TheTradeDeskUserId string
-	SharethroughUserId string
-}
-
-type StrOpenRTBInterface interface {
-	requestFromOpenRTB(openrtb2.Imp, *openrtb2.BidRequest, string) (*adapters.RequestData, error)
-	responseToOpenRTB([]byte, *adapters.RequestData) (*adapters.BidderResponse, []error)
-}
-
-type StrAdServerUriInterface interface {
-	buildUri(StrAdSeverParams) string
-	parseUri(string) (*StrAdSeverParams, error)
-}
-
-type UserAgentParsers struct {
-	ChromeVersion    *regexp.Regexp
-	ChromeiOSVersion *regexp.Regexp
-	SafariVersion    *regexp.Regexp
-}
-
-type ButlerRequestBody struct {
-	BlockedAdvDomains []string `json:"badv,omitempty"`
-	MaxTimeout        int64    `json:"tmax"`
-	Deadline          string   `json:"deadline"`
-	BidFloor          float64  `json:"bidfloor,omitempty"`
-}
-
-type StrUriHelper struct {
-	BaseURI string
-	Clock   ClockInterface
-}
-
-type StrBodyHelper struct {
-	Clock ClockInterface
-}
-
-type StrOpenRTBTranslator struct {
-	UriHelper        StrAdServerUriInterface
-	Util             UtilityInterface
-	UserAgentParsers UserAgentParsers
-}
-
-func (s StrOpenRTBTranslator) requestFromOpenRTB(imp openrtb2.Imp, request *openrtb2.BidRequest, domain string) (*adapters.RequestData, error) {
-	headers := http.Header{}
-	headers.Add("Content-Type", "application/json;charset=utf-8")
-	headers.Add("Accept", "application/json")
-	headers.Add("Origin", domain)
-	headers.Add("Referer", request.Site.Page)
-	headers.Add("X-Forwarded-For", request.Device.IP)
-	headers.Add("User-Agent", request.Device.UA)
-
-	var strImpExt adapters.ExtImpBidder
-	if err := json.Unmarshal(imp.Ext, &strImpExt); err != nil {
-		return nil, err
-	}
-	var strImpParams openrtb_ext.ExtImpSharethrough
-	if err := json.Unmarshal(strImpExt.Bidder, &strImpParams); err != nil {
-		return nil, err
-	}
-
-	pKey := strImpParams.Pkey
-	userInfo := s.Util.parseUserInfo(request.User)
-	height, width := s.Util.getPlacementSize(imp, strImpParams)
-
-	jsonBody, err := (StrBodyHelper{Clock: s.Util.getClock()}).buildBody(request, strImpParams)
-	if err != nil {
-		return nil, err
-	}
-
-	var gpid string
-	if strImpParams.Data != nil && strImpParams.Data.PBAdSlot != "" {
-		gpid = strImpParams.Data.PBAdSlot
-	}
-
-	usPolicySignal := ""
-	if usPolicy, err := ccpa.ReadFromRequest(request); err == nil {
-		usPolicySignal = usPolicy.Consent
-	}
-
-	return &adapters.RequestData{
-		Method: "POST",
-		Uri: s.UriHelper.buildUri(StrAdSeverParams{
-			Pkey:               pKey,
-			BidID:              imp.ID,
-			GPID:               gpid,
-			ConsentRequired:    s.Util.gdprApplies(request),
-			ConsentString:      userInfo.Consent,
-			USPrivacySignal:    usPolicySignal,
-			Iframe:             strImpParams.Iframe,
-			Height:             uint64(height),
-			Width:              uint64(width),
-			InstantPlayCapable: s.Util.canAutoPlayVideo(request.Device.UA, s.UserAgentParsers),
-			TheTradeDeskUserId: userInfo.TtdUid,
-			SharethroughUserId: userInfo.StxUid,
-		}),
-		Body:    jsonBody,
-		Headers: headers,
-	}, nil
-}
-
-func (s StrOpenRTBTranslator) responseToOpenRTB(strRawResp []byte, btlrReq *adapters.RequestData) (*adapters.BidderResponse, []error) {
-	var errs []error
-
-	var strResp openrtb_ext.ExtImpSharethroughResponse
-	if err := json.Unmarshal(strRawResp, &strResp); err != nil {
-		return nil, []error{&errortypes.BadInput{Message: "Unable to parse response JSON"}}
-	}
-	bidResponse := adapters.NewBidderResponse()
-
-	bidResponse.Currency = "USD"
-	typedBid := &adapters.TypedBid{BidType: openrtb_ext.BidTypeBanner}
-
-	if len(strResp.Creatives) == 0 {
-		errs = append(errs, &errortypes.BadInput{Message: "No creative provided"})
-		return nil, errs
-	}
-	creative := strResp.Creatives[0]
-
-	btlrParams, parseHBUriErr := s.UriHelper.parseUri(btlrReq.Uri)
-	if parseHBUriErr != nil {
-		errs = append(errs, &errortypes.BadInput{Message: parseHBUriErr.Error()})
-		return nil, errs
-	}
-
-	adm, admErr := s.Util.getAdMarkup(strRawResp, strResp, btlrParams)
-	if admErr != nil {
-		errs = append(errs, &errortypes.BadServerResponse{Message: admErr.Error()})
-		return nil, errs
-	}
-
-	bid := &openrtb2.Bid{
-		AdID:   strResp.AdServerRequestID,
-		ID:     strResp.BidID,
-		ImpID:  btlrParams.BidID,
-		Price:  creative.CPM,
-		CID:    creative.Metadata.CampaignKey,
-		CrID:   creative.Metadata.CreativeKey,
-		DealID: creative.Metadata.DealID,
-		AdM:    adm,
-		H:      int64(btlrParams.Height),
-		W:      int64(btlrParams.Width),
-	}
-
-	typedBid.Bid = bid
-	bidResponse.Bids = append(bidResponse.Bids, typedBid)
-
-	return bidResponse, errs
-}
-
-func (h StrBodyHelper) buildBody(request *openrtb2.BidRequest, strImpParams openrtb_ext.ExtImpSharethrough) (body []byte, err error) {
-	timeout := request.TMax
-	if timeout == 0 {
-		timeout = defaultTmax
-	}
-
-	body, err = json.Marshal(ButlerRequestBody{
-		BlockedAdvDomains: request.BAdv,
-		MaxTimeout:        timeout,
-		Deadline:          h.Clock.now().Add(time.Duration(timeout) * time.Millisecond).Format(time.RFC3339Nano),
-		BidFloor:          strImpParams.BidFloor,
-	})
-
-	return
-}
-
-func (h StrUriHelper) buildUri(params StrAdSeverParams) string {
-	v := url.Values{}
-	v.Set("placement_key", params.Pkey)
-	v.Set("bidId", params.BidID)
-	if params.GPID != "" {
-		v.Set("gpid", params.GPID)
-	}
-	v.Set("consent_required", fmt.Sprintf("%t", params.ConsentRequired))
-	v.Set("consent_string", params.ConsentString)
-	if params.USPrivacySignal != "" {
-		v.Set("us_privacy", params.USPrivacySignal)
-	}
-	if params.TheTradeDeskUserId != "" {
-		v.Set("ttduid", params.TheTradeDeskUserId)
-	}
-	if params.SharethroughUserId != "" {
-		v.Set("stxuid", params.SharethroughUserId)
-	}
-
-	v.Set("instant_play_capable", fmt.Sprintf("%t", params.InstantPlayCapable))
-	v.Set("stayInIframe", fmt.Sprintf("%t", params.Iframe))
-	v.Set("height", strconv.FormatUint(params.Height, 10))
-	v.Set("width", strconv.FormatUint(params.Width, 10))
-
-	v.Set("adRequestAt", h.Clock.now().Format(time.RFC3339Nano))
-	v.Set("supplyId", supplyId)
-	v.Set("strVersion", strconv.FormatInt(strVersion, 10))
-
-	return h.BaseURI + "?" + v.Encode()
-}
-
-func (h StrUriHelper) parseUri(uri string) (*StrAdSeverParams, error) {
-	btlrUrl, err := url.Parse(uri)
-	if err != nil {
-		return nil, err
-	}
-
-	params := btlrUrl.Query()
-	height, err := strconv.ParseUint(params.Get("height"), 10, 64)
-	if err != nil {
-		return nil, err
-	}
-
-	width, err := strconv.ParseUint(params.Get("width"), 10, 64)
-	if err != nil {
-		return nil, err
-	}
-
-	stayInIframe, err := strconv.ParseBool(params.Get("stayInIframe"))
-	if err != nil {
-		stayInIframe = false
-	}
-
-	consentRequired, err := strconv.ParseBool(params.Get("consent_required"))
-	if err != nil {
-		consentRequired = false
-	}
-
-	return &StrAdSeverParams{
-		Pkey:            params.Get("placement_key"),
-		BidID:           params.Get("bidId"),
-		Iframe:          stayInIframe,
-		Height:          height,
-		Width:           width,
-		ConsentRequired: consentRequired,
-		ConsentString:   params.Get("consent_string"),
-	}, nil
-}
diff --git a/adapters/sharethrough/sharethrough.go b/adapters/sharethrough/sharethrough.go
index 410ca391..248859bb 100644
--- a/adapters/sharethrough/sharethrough.go
+++ b/adapters/sharethrough/sharethrough.go
@@ -1,62 +1,109 @@
 package sharethrough
 
 import (
+	"encoding/json"
 	"fmt"
 	"net/http"
-	"regexp"
+	"strings"
 
 	"github.com/mxmCherry/openrtb/v15/openrtb2"
 	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/prebid/prebid-server/version"
 )
 
-const supplyId = "FGMrCMMc"
-const strVersion = 8
+var adapterVersion = "10.0"
+
+type adapter struct {
+	endpoint string
+}
 
 // Builder builds a new instance of the Sharethrough adapter for the given bidder with the given config.
 func Builder(bidderName openrtb_ext.BidderName, config config.Adapter) (adapters.Bidder, error) {
-	bidder := &SharethroughAdapter{
-		AdServer: StrOpenRTBTranslator{
-			UriHelper: StrUriHelper{BaseURI: config.Endpoint, Clock: Clock{}},
-			Util:      Util{Clock: Clock{}},
-			UserAgentParsers: UserAgentParsers{
-				ChromeVersion:    regexp.MustCompile(`Chrome\/(?P<ChromeVersion>\d+)`),
-				ChromeiOSVersion: regexp.MustCompile(`CriOS\/(?P<chromeiOSVersion>\d+)`),
-				SafariVersion:    regexp.MustCompile(`Version\/(?P<safariVersion>\d+)`),
-			},
-		},
+	bidder := &adapter{
+		endpoint: config.Endpoint,
 	}
 	return bidder, nil
 }
 
-type SharethroughAdapter struct {
-	AdServer StrOpenRTBInterface
-}
+func (a adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	var requests []*adapters.RequestData
+	var errors []error
 
-func (a SharethroughAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
-	var reqs []*adapters.RequestData
+	headers := http.Header{}
+	headers.Add("Content-Type", "application/json;charset=utf-8")
+	headers.Add("Accept", "application/json")
+
+	modifiableSource := openrtb2.Source{}
+	if request.Source != nil {
+		modifiableSource = *request.Source
+	}
+	var sourceExt map[string]interface{}
+	if err := json.Unmarshal(modifiableSource.Ext, &sourceExt); err == nil {
+		sourceExt["str"] = adapterVersion
+		sourceExt["version"] = version.Ver
+	} else {
+		sourceExt = map[string]interface{}{"str": adapterVersion, "version": version.Ver}
+	}
 
-	if request.Site == nil {
-		return nil, []error{fmt.Errorf("request must include a site; in-app placements are not supported")}
+	var err error
+	if modifiableSource.Ext, err = json.Marshal(sourceExt); err != nil {
+		errors = append(errors, err)
 	}
-	var domain = Util{}.parseDomain(request.Site.Page)
+	request.Source = &modifiableSource
+
+	requestCopy := *request
+	for _, imp := range request.Imp {
+		// Extract Sharethrough Params
+		var strImpExt adapters.ExtImpBidder
+		if err := json.Unmarshal(imp.Ext, &strImpExt); err != nil {
+			errors = append(errors, err)
+			continue
+		}
+		var strImpParams openrtb_ext.ExtImpSharethrough
+		if err := json.Unmarshal(strImpExt.Bidder, &strImpParams); err != nil {
+			errors = append(errors, err)
+			continue
+		}
+
+		// Convert Floor into USD
+		if imp.BidFloor > 0 && imp.BidFloorCur != "" && !strings.EqualFold(imp.BidFloorCur, "USD") {
+			convertedValue, err := reqInfo.ConvertCurrency(imp.BidFloor, imp.BidFloorCur, "USD")
+			if err != nil {
+				return nil, []error{err}
+			}
+			imp.BidFloorCur = "USD"
+			imp.BidFloor = convertedValue
+		}
+
+		// Relocate Custom Params
+		imp.TagID = strImpParams.Pkey
+		requestCopy.BCat = append(requestCopy.BCat, strImpParams.BCat...)
+		requestCopy.BAdv = append(requestCopy.BAdv, strImpParams.BAdv...)
 
-	for i := 0; i < len(request.Imp); i++ {
-		req, err := a.AdServer.requestFromOpenRTB(request.Imp[i], request, domain)
+		requestCopy.Imp = []openrtb2.Imp{imp}
 
+		requestJSON, err := json.Marshal(requestCopy)
 		if err != nil {
-			return nil, []error{err}
+			errors = append(errors, err)
+			continue
 		}
-		reqs = append(reqs, req)
+
+		requestData := &adapters.RequestData{
+			Method:  "POST",
+			Uri:     a.endpoint,
+			Body:    requestJSON,
+			Headers: headers,
+		}
+		requests = append(requests, requestData)
 	}
 
-	// We never add to the errs slice (early return), so we just create an empty one to return
-	return reqs, []error{}
+	return requests, errors
 }
 
-func (a SharethroughAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+func (a adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
 	if response.StatusCode == http.StatusNoContent {
 		return nil, nil
 	}
@@ -71,5 +118,32 @@ func (a SharethroughAdapter) MakeBids(internalRequest *openrtb2.BidRequest, exte
 		return nil, []error{fmt.Errorf("unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode)}
 	}
 
-	return a.AdServer.responseToOpenRTB(response.Body, externalRequest)
+	var bidReq openrtb2.BidRequest
+	if err := json.Unmarshal(requestData.Body, &bidReq); err != nil {
+		return nil, []error{err}
+	}
+
+	var bidResp openrtb2.BidResponse
+	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
+		return nil, []error{err}
+	}
+
+	bidderResponse := adapters.NewBidderResponse()
+	bidderResponse.Currency = "USD"
+
+	for _, seatBid := range bidResp.SeatBid {
+		for i := range seatBid.Bid {
+			bidType := openrtb_ext.BidTypeBanner
+			if bidReq.Imp[0].Video != nil {
+				bidType = openrtb_ext.BidTypeVideo
+			}
+
+			bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{
+				BidType: bidType,
+				Bid:     &seatBid.Bid[i],
+			})
+		}
+	}
+
+	return bidderResponse, nil
 }
diff --git a/adapters/sharethrough/utils.go b/adapters/sharethrough/utils.go
deleted file mode 100644
index e10a8f90..00000000
--- a/adapters/sharethrough/utils.go
+++ /dev/null
@@ -1,262 +0,0 @@
-package sharethrough
-
-import (
-	"bytes"
-	"encoding/base64"
-	"encoding/json"
-	"fmt"
-	"html/template"
-	"net"
-	"net/url"
-	"regexp"
-	"strconv"
-	"time"
-
-	"github.com/buger/jsonparser"
-	"github.com/mxmCherry/openrtb/v15/openrtb2"
-	"github.com/prebid/prebid-server/openrtb_ext"
-)
-
-const minChromeVersion = 53
-const minSafariVersion = 10
-
-type UtilityInterface interface {
-	gdprApplies(*openrtb2.BidRequest) bool
-	parseUserInfo(*openrtb2.User) userInfo
-
-	getAdMarkup([]byte, openrtb_ext.ExtImpSharethroughResponse, *StrAdSeverParams) (string, error)
-	getBestFormat([]openrtb2.Format) (int64, int64)
-	getPlacementSize(openrtb2.Imp, openrtb_ext.ExtImpSharethrough) (int64, int64)
-
-	canAutoPlayVideo(string, UserAgentParsers) bool
-	isAndroid(string) bool
-	isiOS(string) bool
-	isAtMinChromeVersion(string, *regexp.Regexp) bool
-	isAtMinSafariVersion(string, *regexp.Regexp) bool
-
-	parseDomain(string) string
-	getClock() ClockInterface
-}
-
-type ClockInterface interface {
-	now() time.Time
-}
-
-type Clock struct{}
-
-type Util struct {
-	Clock ClockInterface
-}
-
-type userExt struct {
-	Consent string                   `json:"consent,omitempty"`
-	Eids    []openrtb_ext.ExtUserEid `json:"eids,omitempty"`
-}
-
-type userInfo struct {
-	Consent string
-	TtdUid  string
-	StxUid  string
-}
-
-func (u Util) getAdMarkup(strRawResp []byte, strResp openrtb_ext.ExtImpSharethroughResponse, params *StrAdSeverParams) (string, error) {
-	landingTime := u.Clock.now()
-	strRespId := fmt.Sprintf("str_response_%s", strResp.BidID)
-
-	tmplBody := `
-		<img src="//b.sharethrough.com/butler?type=s2s-win&arid={{.Arid}}&adReceivedAt={{.LandingTime}}" />
-
-		<div data-str-native-key="{{.Pkey}}" data-stx-response-name="{{.StrRespId}}"></div>
-	 	<script>var {{.StrRespId}} = "{{.B64EncodedJson}}"</script>
-	`
-
-	if params.Iframe {
-		tmplBody = tmplBody + `
-			<script src="//native.sharethrough.com/assets/sfp.js"></script>
-		`
-	} else {
-		tmplBody = tmplBody + `
-			<script src="//native.sharethrough.com/assets/sfp-set-targeting.js"></script>
-	    	<script>
-		     (function() {
-		       if (!(window.STR && window.STR.Tag) && !(window.top.STR && window.top.STR.Tag)) {
-		         var sfp_js = document.createElement('script');
-		         sfp_js.src = "//native.sharethrough.com/assets/sfp.js";
-		         sfp_js.type = 'text/javascript';
-		         sfp_js.charset = 'utf-8';
-		         try {
-		             window.top.document.getElementsByTagName('body')[0].appendChild(sfp_js);
-		         } catch (e) {
-		           console.log(e);
-		         }
-		       }
-		     })()
-		   </script>
-		`
-	}
-
-	tmpl, err := template.New("sfpjs").Parse(tmplBody)
-	if err != nil {
-		return "", err
-	}
-
-	var buf []byte
-	templatedBuf := bytes.NewBuffer(buf)
-
-	b64EncodedJson := base64.StdEncoding.EncodeToString(strRawResp)
-	err = tmpl.Execute(templatedBuf, struct {
-		Arid           template.JS
-		Pkey           string
-		StrRespId      template.JS
-		B64EncodedJson string
-		LandingTime    string
-	}{
-		template.JS(strResp.AdServerRequestID),
-		params.Pkey,
-		template.JS(strRespId),
-		b64EncodedJson,
-		landingTime.Format(time.RFC3339Nano),
-	})
-	if err != nil {
-		return "", err
-	}
-
-	return templatedBuf.String(), nil
-}
-
-func (u Util) getPlacementSize(imp openrtb2.Imp, strImpParams openrtb_ext.ExtImpSharethrough) (height, width int64) {
-	height, width = 1, 1
-	if len(strImpParams.IframeSize) >= 2 {
-		height, width = int64(strImpParams.IframeSize[0]), int64(strImpParams.IframeSize[1])
-	} else if imp.Banner != nil {
-		height, width = u.getBestFormat(imp.Banner.Format)
-	}
-
-	return
-}
-
-func (u Util) getBestFormat(formats []openrtb2.Format) (height, width int64) {
-	height, width = 1, 1
-	for i := 0; i < len(formats); i++ {
-		format := formats[i]
-		if (format.H * format.W) > (height * width) {
-			height = format.H
-			width = format.W
-		}
-	}
-
-	return
-}
-
-func (u Util) canAutoPlayVideo(userAgent string, parsers UserAgentParsers) bool {
-	if u.isAndroid(userAgent) {
-		return u.isAtMinChromeVersion(userAgent, parsers.ChromeVersion)
-	} else if u.isiOS(userAgent) {
-		return u.isAtMinSafariVersion(userAgent, parsers.SafariVersion) || u.isAtMinChromeVersion(userAgent, parsers.ChromeiOSVersion)
-	}
-	return true
-}
-
-func (u Util) isAndroid(userAgent string) bool {
-	isAndroid, err := regexp.MatchString("(?i)Android", userAgent)
-	if err != nil {
-		return false
-	}
-	return isAndroid
-}
-
-func (u Util) isiOS(userAgent string) bool {
-	isiOS, err := regexp.MatchString("(?i)iPhone|iPad|iPod", userAgent)
-	if err != nil {
-		return false
-	}
-	return isiOS
-}
-
-func (u Util) isAtMinVersion(userAgent string, versionParser *regexp.Regexp, minVersion int64) bool {
-	var version int64
-	var err error
-
-	versionMatch := versionParser.FindStringSubmatch(userAgent)
-	if len(versionMatch) > 1 {
-		version, err = strconv.ParseInt(versionMatch[1], 10, 64)
-	}
-	if err != nil {
-		return false
-	}
-
-	return version >= minVersion
-}
-
-func (u Util) isAtMinChromeVersion(userAgent string, parser *regexp.Regexp) bool {
-	return u.isAtMinVersion(userAgent, parser, minChromeVersion)
-}
-
-func (u Util) isAtMinSafariVersion(userAgent string, parser *regexp.Regexp) bool {
-	return u.isAtMinVersion(userAgent, parser, minSafariVersion)
-}
-
-func (u Util) gdprApplies(request *openrtb2.BidRequest) bool {
-	var gdprApplies int64
-
-	if request.Regs != nil {
-		if jsonExtRegs, err := request.Regs.Ext.MarshalJSON(); err == nil {
-			// 0 is the return value if error, so no need to handle
-			gdprApplies, _ = jsonparser.GetInt(jsonExtRegs, "gdpr")
-		}
-	}
-
-	return gdprApplies != 0
-}
-
-func (u Util) parseUserInfo(user *openrtb2.User) (ui userInfo) {
-	if user == nil {
-		return
-	}
-
-	ui.StxUid = user.BuyerUID
-
-	var userExt userExt
-	if user.Ext != nil {
-		if err := json.Unmarshal(user.Ext, &userExt); err == nil {
-			ui.Consent = userExt.Consent
-			for i := 0; i < len(userExt.Eids); i++ {
-				if userExt.Eids[i].Source == "adserver.org" && len(userExt.Eids[i].Uids) > 0 {
-					if userExt.Eids[i].Uids[0].ID != "" {
-						ui.TtdUid = userExt.Eids[i].Uids[0].ID
-					}
-					break
-				}
-			}
-		}
-	}
-
-	return
-}
-
-func (u Util) parseDomain(fullUrl string) string {
-	domain := ""
-	uri, err := url.Parse(fullUrl)
-	if err == nil {
-		host, _, errSplit := net.SplitHostPort(uri.Host)
-		if errSplit == nil {
-			domain = host
-		} else {
-			domain = uri.Host
-		}
-
-		if domain != "" {
-			domain = uri.Scheme + "://" + domain
-		}
-	}
-
-	return domain
-}
-
-func (u Util) getClock() ClockInterface {
-	return u.Clock
-}
-
-func (c Clock) now() time.Time {
-	return time.Now().UTC()
-}
diff --git a/config/accounts.go b/config/accounts.go
index dd4d273e..0e6c4114 100644
--- a/config/accounts.go
+++ b/config/accounts.go
@@ -21,6 +21,7 @@ type Account struct {
 	GDPR               AccountGDPR `mapstructure:"gdpr" json:"gdpr"`
 	DebugAllow         bool        `mapstructure:"debug_allow" json:"debug_allow"`
 	DefaultIntegration string      `mapstructure:"default_integration" json:"default_integration"`
+	Events             Events      `mapstructure:"events" json:"events"` // Don't enable this feature. It is still under developmment - https://github.com/prebid/prebid-server/issues/1725
 }
 
 // AccountCCPA represents account-specific CCPA configuration
diff --git a/config/bidderinfo.go b/config/bidderinfo.go
index 12e51a3c..536acf31 100644
--- a/config/bidderinfo.go
+++ b/config/bidderinfo.go
@@ -6,7 +6,7 @@ import (
 	"strings"
 
 	"github.com/prebid/prebid-server/openrtb_ext"
-	"gopkg.in/yaml.v2"
+	"gopkg.in/yaml.v3"
 )
 
 // BidderInfos contains a mapping of bidder name to bidder info.
diff --git a/config/config.go b/config/config.go
index 8dc15786..cfff7666 100644
--- a/config/config.go
+++ b/config/config.go
@@ -120,6 +120,9 @@ func (cfg *Configuration) validate(v *viper.Viper) []error {
 	if cfg.AccountDefaults.Disabled {
 		glog.Warning(`With account_defaults.disabled=true, host-defined accounts must exist and have "disabled":false. All other requests will be rejected.`)
 	}
+	if cfg.AccountDefaults.Events.Enabled {
+		glog.Warning(`account_defaults.events will currently not do anything as the feature is still under development. Please follow https://github.com/prebid/prebid-server/issues/1725 for more updates`)
+	}
 	return errs
 }
 
@@ -416,8 +419,10 @@ func (cfg *Metrics) validate(errs []error) []error {
 type InfluxMetrics struct {
 	Host               string `mapstructure:"host"`
 	Database           string `mapstructure:"database"`
+	Measurement        string `mapstructure:"measurement"`
 	Username           string `mapstructure:"username"`
 	Password           string `mapstructure:"password"`
+	AlignTimestamps    bool   `mapstructure:"align_timestamps"`
 	MetricSendInterval int    `mapstructure:"metric_send_interval"`
 }
 
@@ -692,8 +697,10 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("metrics.disabled_metrics.adapter_gdpr_request_blocked", false)
 	v.SetDefault("metrics.influxdb.host", "")
 	v.SetDefault("metrics.influxdb.database", "")
+	v.SetDefault("metrics.influxdb.measurement", "")
 	v.SetDefault("metrics.influxdb.username", "")
 	v.SetDefault("metrics.influxdb.password", "")
+	v.SetDefault("metrics.influxdb.align_timestamps", false)
 	v.SetDefault("metrics.influxdb.metric_send_interval", 20)
 	v.SetDefault("metrics.prometheus.port", 0)
 	v.SetDefault("metrics.prometheus.namespace", "")
@@ -818,7 +825,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.adocean.endpoint", "https://{{.Host}}")
 	v.SetDefault("adapters.adnuntius.endpoint", "https://ads.adnuntius.delivery/i")
 	v.SetDefault("adapters.adoppler.endpoint", "http://{{.AccountID}}.trustedmarketplace.io/ads/processHeaderBid/{{.AdUnit}}")
-	v.SetDefault("adapters.adot.endpoint", "https://dsp.adotmob.com/headerbidding/bidrequest")
+	v.SetDefault("adapters.adot.endpoint", "https://dsp.adotmob.com/headerbidding{PUBLISHER_PATH}/bidrequest")
 	v.SetDefault("adapters.adpone.endpoint", "http://rtb.adpone.com/bid-request?src=prebid_server")
 	v.SetDefault("adapters.adprime.endpoint", "http://delta.adprime.com/pserver")
 	v.SetDefault("adapters.adtarget.endpoint", "http://ghb.console.adtarget.com.tr/pbs/ortb")
@@ -878,6 +885,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.inmobi.endpoint", "https://api.w.inmobi.com/showad/openrtb/bidder/prebid")
 	v.SetDefault("adapters.interactiveoffers.endpoint", "https://prebid-server.ioadx.com/bidRequest/?partnerId={{.AccountID}}")
 	v.SetDefault("adapters.ix.disabled", true)
+	v.SetDefault("adapters.janet.endpoint", "http://ghb.bidder.jmgads.com/pbs/ortb")
 	v.SetDefault("adapters.jixie.endpoint", "https://hb.jixie.io/v2/hbsvrpost")
 	v.SetDefault("adapters.kayzen.endpoint", "https://bids-{{.ZoneID}}.bidder.kayzen.io/?exchange={{.AccountID}}")
 	v.SetDefault("adapters.krushmedia.endpoint", "http://ads4.krushmedia.com/?c=rtb&m=req&key={{.AccountID}}")
@@ -919,7 +927,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.rtbhouse.endpoint", "http://prebidserver-s2s-ams.creativecdn.com/bidder/prebidserver/bids")
 	v.SetDefault("adapters.rubicon.disabled", true)
 	v.SetDefault("adapters.rubicon.endpoint", "http://exapi-us-east.rubiconproject.com/a/api/exchange.json")
-	v.SetDefault("adapters.sharethrough.endpoint", "http://btlr.sharethrough.com/FGMrCMMc/v1")
+	v.SetDefault("adapters.sharethrough.endpoint", "https://btlr.sharethrough.com/universal/v1?supply_id=FGMrCMMc")
 	v.SetDefault("adapters.silvermob.endpoint", "http://{{.Host}}.silvermob.com/marketplace/api/dsp/bid/{{.ZoneID}}")
 	v.SetDefault("adapters.smaato.endpoint", "https://prebid.ad.smaato.net/oapi/prebid")
 	v.SetDefault("adapters.smartadserver.endpoint", "https://ssb-global.smartadserver.com")
@@ -1078,6 +1086,9 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("gdpr.tcf2.purpose_one_treatment.access_allowed", true)
 	v.SetDefault("gdpr.tcf2.special_feature1.enforce", true)
 	v.SetDefault("gdpr.tcf2.special_feature1.vendor_exceptions", []openrtb_ext.BidderName{})
+
+	// Defaults for account_defaults.events.default_url
+	v.SetDefault("account_defaults.events.default_url", "https://PBS_HOST/event?t=##PBS-EVENTTYPE##&vtype=##PBS-VASTEVENT##&b=##PBS-BIDID##&f=i&a=##PBS-ACCOUNTID##&ts=##PBS-TIMESTAMP##&bidder=##PBS-BIDDER##&int=##PBS-INTEGRATION##&mt=##PBS-MEDIATYPE##&ch=##PBS-CHANNEL##&aid=##PBS-AUCTIONID##&l=##PBS-LINEID##")
 }
 
 func migrateConfig(v *viper.Viper) {
diff --git a/config/events.go b/config/events.go
new file mode 100644
index 00000000..cf3139d8
--- /dev/null
+++ b/config/events.go
@@ -0,0 +1,149 @@
+package config
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+
+	validator "github.com/asaskevich/govalidator"
+)
+
+// VASTEventElement indicates valid VAST event element
+type VASTEventElement string
+
+const (
+	ImpressionVASTElement             VASTEventElement = "impression"
+	TrackingVASTElement               VASTEventElement = "tracking"
+	ClickTrackingVASTElement          VASTEventElement = "clicktracking"
+	CompanionClickThroughVASTElement  VASTEventElement = "companionclickthrough"
+	ErrorVASTElement                  VASTEventElement = "error"
+	NonLinearClickTrackingVASTElement VASTEventElement = "nonlinearclicktracking"
+)
+
+var vastEventElementMap = map[VASTEventElement]struct{}{
+	ImpressionVASTElement:             {},
+	TrackingVASTElement:               {},
+	ClickTrackingVASTElement:          {},
+	CompanionClickThroughVASTElement:  {},
+	ErrorVASTElement:                  {},
+	NonLinearClickTrackingVASTElement: {},
+}
+
+// TrackingEventType indicates quartile events
+type TrackingEventType string
+
+const (
+	Start         TrackingEventType = "start"
+	FirstQuartile TrackingEventType = "firstQuartile"
+	MidPoint      TrackingEventType = "midPoint"
+	ThirdQuartile TrackingEventType = "thirdQuartile"
+	Complete      TrackingEventType = "complete"
+)
+
+var trackingEventTypeMap = map[TrackingEventType]struct{}{
+	Start:         {},
+	FirstQuartile: {},
+	MidPoint:      {},
+	ThirdQuartile: {},
+	Complete:      {},
+}
+
+// VASTEvent indicates the configurations required for injecting VAST event trackers within
+// VAST XML
+type VASTEvent struct {
+	CreateElement     VASTEventElement  `mapstructure:"create_element" json:"create_element"`
+	Type              TrackingEventType `mapstructure:"type" json:"type"`
+	ExcludeDefaultURL bool              `mapstructure:"exclude_default_url" json:"exclude_default_url"`
+	URLs              []string          `mapstructure:"urls" json:"urls"`
+}
+
+// Events indicates the various types of events to be captured typically for injecting tracker URLs
+// within the VAST XML
+// Don't enable this feature. It is still under developmment. Please follow https://github.com/prebid/prebid-server/issues/1725 for more updates
+type Events struct {
+	Enabled    bool        `mapstructure:"enabled" json:"enabled"`
+	DefaultURL string      `mapstructure:"default_url" json:"default_url"`
+	VASTEvents []VASTEvent `mapstructure:"vast_events" json:"vast_events,omitempty"`
+}
+
+// validate verifies the events object  and returns error if at least one is invalid.
+func (e Events) validate(errs []error) []error {
+	if e.Enabled {
+		if !isValidURL(e.DefaultURL) {
+			return append(errs, errors.New("Invalid events.default_url"))
+		}
+		err := validateVASTEvents(e.VASTEvents)
+		if err != nil {
+			return append(errs, err)
+		}
+	}
+	return errs
+}
+
+// validateVASTEvents verifies the all VASTEvent objects and returns error if at least one is invalid.
+func validateVASTEvents(events []VASTEvent) error {
+	if events != nil {
+		for i, event := range events {
+			if err := event.validate(); err != nil {
+				return fmt.Errorf(err.Error(), i, i)
+			}
+		}
+	}
+	return nil
+}
+
+// validate validates event object and  returns error if at least one is invalid
+func (e VASTEvent) validate() error {
+	if !e.CreateElement.isValid() {
+		return fmt.Errorf("Invalid events.vast_events[%s].create_element", "%d")
+	}
+	validType := e.Type.isValid()
+
+	if e.isTrackingEvent() && !validType {
+		var ele []string
+		for k := range vastEventElementMap {
+			ele = append(ele, string(k))
+		}
+		return fmt.Errorf("Missing or Invalid events.vast_events[%s].type. Valid values are %v", "%d", strings.Join(ele, ", "))
+	}
+	if validType && !e.isTrackingEvent() {
+		return fmt.Errorf("events.vast_events[%s].type is not applicable for create element '%s'", "%d", e.CreateElement)
+	}
+	for i, url := range e.URLs {
+		if !isValidURL(url) {
+			return fmt.Errorf("Invalid events.vast_events[%s].urls[%d]", "%d", i)
+		}
+	}
+	// ensure at least one valid url exists when default URL to be excluded
+	if e.ExcludeDefaultURL && len(e.URLs) == 0 {
+		return fmt.Errorf("Please provide at least one valid URL in events.vast_events[%s].urls or set events.vast_events[%s].exclude_default_url=false", "%d", "%d")
+	}
+
+	return nil // no errors
+}
+
+// isValid checks create_element has valid value
+// if value is value returns true, otherwise false
+func (element VASTEventElement) isValid() bool {
+	// validate create element
+	if _, ok := vastEventElementMap[element]; ok {
+		return true
+	}
+	return false
+}
+
+// isValid checks if valid type is provided (case-sensitive)
+func (t TrackingEventType) isValid() bool {
+	_, valid := trackingEventTypeMap[t]
+	return valid
+}
+
+// isValidURL validates the event URL
+func isValidURL(eventURL string) bool {
+	return validator.IsURL(eventURL) && validator.IsRequestURL(eventURL)
+}
+
+// isTrackingEvent returns true if event object contains event.CreateElement == "tracking"
+func (e VASTEvent) isTrackingEvent() bool {
+	return e.CreateElement == TrackingVASTElement
+}
diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go
index 452e7ed5..6248a1f9 100644
--- a/endpoints/openrtb2/amp_auction.go
+++ b/endpoints/openrtb2/amp_auction.go
@@ -58,6 +58,7 @@ func NewAmpEndpoint(
 	disabledBidders map[string]string,
 	defReqJSON []byte,
 	bidderMap map[string]openrtb_ext.BidderName,
+	storedRespFetcher stored_requests.Fetcher,
 ) (httprouter.Handle, error) {
 
 	if ex == nil || validator == nil || requestsById == nil || accounts == nil || cfg == nil || met == nil {
@@ -88,7 +89,7 @@ func NewAmpEndpoint(
 		nil,
 		nil,
 		ipValidator,
-		empty_fetcher.EmptyFetcher{}}).AmpAuction), nil
+		storedRespFetcher}).AmpAuction), nil
 
 }
 
@@ -136,7 +137,7 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
 	w.Header().Set("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin")
 	w.Header().Set("X-Prebid", version.BuildXPrebidHeader(version.Ver))
 
-	req, errL := deps.parseAmpRequest(r)
+	req, storedAuctionResponses, errL := deps.parseAmpRequest(r)
 	ao.Errors = append(ao.Errors, errL...)
 
 	if errortypes.ContainsFatalError(errL) {
@@ -199,6 +200,7 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
 		StartTime:                  start,
 		LegacyLabels:               labels,
 		GlobalPrivacyControlHeader: secGPC,
+		StoredAuctionResponses:     storedAuctionResponses,
 	}
 
 	response, err := deps.ex.HoldAuction(ctx, auctionRequest, nil)
@@ -299,9 +301,9 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
 // possible, it will return errors with messages that suggest improvements.
 //
 // If the errors list has at least one element, then no guarantees are made about the returned request.
-func (deps *endpointDeps) parseAmpRequest(httpRequest *http.Request) (req *openrtb2.BidRequest, errs []error) {
+func (deps *endpointDeps) parseAmpRequest(httpRequest *http.Request) (req *openrtb2.BidRequest, storedAuctionResponses map[string]json.RawMessage, errs []error) {
 	// Load the stored request for the AMP ID.
-	req, e := deps.loadRequestJSONForAmp(httpRequest)
+	req, storedAuctionResponses, e := deps.loadRequestJSONForAmp(httpRequest)
 	if errs = append(errs, e...); errortypes.ContainsFatalError(errs) {
 		return
 	}
@@ -316,19 +318,20 @@ func (deps *endpointDeps) parseAmpRequest(httpRequest *http.Request) (req *openr
 	}
 
 	// At this point, we should have a valid request that definitely has Targeting and Cache turned on
-	e = deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: req}, true)
+	hasStoredResponses := len(storedAuctionResponses) > 0
+	e = deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: req}, true, hasStoredResponses)
 	errs = append(errs, e...)
 	return
 }
 
 // Load the stored OpenRTB request for an incoming AMP request, or return the errors found.
-func (deps *endpointDeps) loadRequestJSONForAmp(httpRequest *http.Request) (req *openrtb2.BidRequest, errs []error) {
+func (deps *endpointDeps) loadRequestJSONForAmp(httpRequest *http.Request) (req *openrtb2.BidRequest, storedAuctionResponses map[string]json.RawMessage, errs []error) {
 	req = &openrtb2.BidRequest{}
 	errs = nil
 
 	ampParams, err := amp.ParseParams(httpRequest)
 	if err != nil {
-		return nil, []error{err}
+		return nil, nil, []error{err}
 	}
 
 	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(storedRequestTimeoutMillis)*time.Millisecond)
@@ -336,7 +339,7 @@ func (deps *endpointDeps) loadRequestJSONForAmp(httpRequest *http.Request) (req
 
 	storedRequests, _, errs := deps.storedReqFetcher.FetchRequests(ctx, []string{ampParams.StoredRequestID}, nil)
 	if len(errs) > 0 {
-		return nil, errs
+		return nil, nil, errs
 	}
 	if len(storedRequests) == 0 {
 		errs = []error{fmt.Errorf("No AMP config found for tag_id '%s'", ampParams.StoredRequestID)}
@@ -350,6 +353,12 @@ func (deps *endpointDeps) loadRequestJSONForAmp(httpRequest *http.Request) (req
 		return
 	}
 
+	storedAuctionResponses, errs = deps.processStoredAuctionResponses(ctx, requestJSON)
+	if err != nil {
+		errs = []error{err}
+		return
+	}
+
 	if deps.cfg.GenerateRequestID || req.ID == "{{UUID}}" {
 		newBidRequestId, err := deps.uuidGenerator.Generate()
 		if err != nil {
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index 33db0a94..074f6739 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -13,17 +13,16 @@ import (
 	"strconv"
 	"time"
 
-	"github.com/prebid/prebid-server/firstpartydata"
-	"github.com/prebid/prebid-server/version"
-
 	"github.com/buger/jsonparser"
-	jsonpatch "github.com/evanphx/json-patch/v5"
 	"github.com/gofrs/uuid"
 	"github.com/golang/glog"
 	"github.com/julienschmidt/httprouter"
 	"github.com/mxmCherry/openrtb/v15/native1"
 	nativeRequests "github.com/mxmCherry/openrtb/v15/native1/request"
 	"github.com/mxmCherry/openrtb/v15/openrtb2"
+	"golang.org/x/net/publicsuffix"
+	jsonpatch "gopkg.in/evanphx/json-patch.v4"
+
 	accountService "github.com/prebid/prebid-server/account"
 	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/analytics"
@@ -31,6 +30,7 @@ import (
 	"github.com/prebid/prebid-server/currency"
 	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/exchange"
+	"github.com/prebid/prebid-server/firstpartydata"
 	"github.com/prebid/prebid-server/metrics"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/prebid_cache_client"
@@ -43,7 +43,7 @@ import (
 	"github.com/prebid/prebid-server/util/httputil"
 	"github.com/prebid/prebid-server/util/iputil"
 	"github.com/prebid/prebid-server/util/uuidutil"
-	"golang.org/x/net/publicsuffix"
+	"github.com/prebid/prebid-server/version"
 )
 
 const storedRequestTimeoutMillis = 50
@@ -151,7 +151,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 
 	w.Header().Set("X-Prebid", version.BuildXPrebidHeader(version.Ver))
 
-	req, impExtInfoMap, errL := deps.parseRequest(r)
+	req, impExtInfoMap, storedAuctionResponses, errL := deps.parseRequest(r)
 	if errortypes.ContainsFatalError(errL) && writeError(errL, w, &labels) {
 		return
 	}
@@ -225,6 +225,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 		GlobalPrivacyControlHeader: secGPC,
 		ImpExtInfoMap:              impExtInfoMap,
 		FirstPartyData:             resolvedFPD,
+		StoredAuctionResponses:     storedAuctionResponses,
 	}
 
 	response, err := deps.ex.HoldAuction(ctx, auctionRequest, nil)
@@ -266,7 +267,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 // possible, it will return errors with messages that suggest improvements.
 //
 // If the errors list has at least one element, then no guarantees are made about the returned request.
-func (deps *endpointDeps) parseRequest(httpRequest *http.Request) (req *openrtb_ext.RequestWrapper, impExtInfoMap map[string]exchange.ImpExtInfo, errs []error) {
+func (deps *endpointDeps) parseRequest(httpRequest *http.Request) (req *openrtb_ext.RequestWrapper, impExtInfoMap map[string]exchange.ImpExtInfo, storedAuctionResponses map[string]json.RawMessage, errs []error) {
 	req = &openrtb_ext.RequestWrapper{}
 	req.BidRequest = &openrtb2.BidRequest{}
 	errs = nil
@@ -295,7 +296,7 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request) (req *openrtb_
 
 	impInfo, errs := parseImpInfo(requestJson)
 	if len(errs) > 0 {
-		return nil, nil, errs
+		return nil, nil, nil, errs
 	}
 
 	// Fetch the Stored Request data and merge it into the HTTP request.
@@ -303,6 +304,11 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request) (req *openrtb_
 		return
 	}
 
+	storedAuctionResponses, errs = deps.processStoredAuctionResponses(ctx, requestJson)
+	if len(errs) > 0 {
+		return
+	}
+
 	if err := json.Unmarshal(requestJson, req.BidRequest); err != nil {
 		errs = []error{err}
 		return
@@ -323,7 +329,8 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request) (req *openrtb_
 
 	lmt.ModifyForIOS(req.BidRequest)
 
-	errL := deps.validateRequest(req, false)
+	hasStoredResponses := len(storedAuctionResponses) > 0
+	errL := deps.validateRequest(req, false, hasStoredResponses)
 	if len(errL) > 0 {
 		errs = append(errs, errL...)
 	}
@@ -480,7 +487,7 @@ func addMissingReqExtParamsInImpExt(impExtByBidder map[string]json.RawMessage, r
 	return anyModified, nil
 }
 
-func (deps *endpointDeps) validateRequest(req *openrtb_ext.RequestWrapper, isAmp bool) []error {
+func (deps *endpointDeps) validateRequest(req *openrtb_ext.RequestWrapper, isAmp bool, hasStoredResponses bool) []error {
 	errL := []error{}
 	if req.ID == "" {
 		return []error{errors.New("request missing required field: \"id\"")}
@@ -599,7 +606,7 @@ func (deps *endpointDeps) validateRequest(req *openrtb_ext.RequestWrapper, isAmp
 			errL = append(errL, fmt.Errorf(`request.imp[%d].id and request.imp[%d].id are both "%s". Imp IDs must be unique.`, firstIndex, index, imp.ID))
 		}
 		impIDs[imp.ID] = index
-		errs := deps.validateImp(imp, aliases, index)
+		errs := deps.validateImp(imp, aliases, index, hasStoredResponses)
 		if len(errs) > 0 {
 			errL = append(errL, errs...)
 		}
@@ -715,7 +722,7 @@ func validateBidders(bidders []string, knownBidders map[string]openrtb_ext.Bidde
 	return nil
 }
 
-func (deps *endpointDeps) validateImp(imp *openrtb2.Imp, aliases map[string]string, index int) []error {
+func (deps *endpointDeps) validateImp(imp *openrtb2.Imp, aliases map[string]string, index int, hasStoredResponses bool) []error {
 	if imp.ID == "" {
 		return []error{fmt.Errorf("request.imp[%d] missing required field: \"id\"", index)}
 	}
@@ -748,7 +755,7 @@ func (deps *endpointDeps) validateImp(imp *openrtb2.Imp, aliases map[string]stri
 		return []error{err}
 	}
 
-	errL := deps.validateImpExt(imp, aliases, index)
+	errL := deps.validateImpExt(imp, aliases, index, hasStoredResponses)
 	if len(errL) != 0 {
 		return errL
 	}
@@ -1167,7 +1174,7 @@ func validatePmp(pmp *openrtb2.PMP, impIndex int) error {
 	return nil
 }
 
-func (deps *endpointDeps) validateImpExt(imp *openrtb2.Imp, aliases map[string]string, impIndex int) []error {
+func (deps *endpointDeps) validateImpExt(imp *openrtb2.Imp, aliases map[string]string, impIndex int, hasStoredResponses bool) []error {
 	errL := []error{}
 	if len(imp.Ext) == 0 {
 		return []error{fmt.Errorf("request.imp[%d].ext is required", impIndex)}
@@ -1180,8 +1187,8 @@ func (deps *endpointDeps) validateImpExt(imp *openrtb2.Imp, aliases map[string]s
 
 	// Prefer bidder params from request.imp.ext.prebid.bidder.BIDDER over request.imp.ext.BIDDER
 	// to avoid confusion beteween prebid specific adapter config and other ext protocols.
+	var extPrebid openrtb_ext.ExtImpPrebid
 	if extPrebidJSON, ok := bidderExts[openrtb_ext.PrebidExtKey]; ok {
-		var extPrebid openrtb_ext.ExtImpPrebid
 		if err := json.Unmarshal(extPrebidJSON, &extPrebid); err == nil && extPrebid.Bidder != nil {
 			for bidder, ext := range extPrebid.Bidder {
 				if ext == nil {
@@ -1191,6 +1198,9 @@ func (deps *endpointDeps) validateImpExt(imp *openrtb2.Imp, aliases map[string]s
 			}
 		}
 	}
+	if hasStoredResponses && extPrebid.StoredAuctionResponse == nil {
+		return []error{fmt.Errorf("request validation failed. The StoredAuctionResponse.ID field must be completely present with, or completely absent from, all impressions in request. No StoredAuctionResponse data found for request.imp[%d].ext.prebid \n", impIndex)}
+	}
 
 	/* Process all the bidder exts in the request */
 	disabledBidders := []string{}
@@ -1566,6 +1576,52 @@ func getJsonSyntaxError(testJSON []byte) (bool, string) {
 	return false, ""
 }
 
+// processStoredAuctionResponses takes the incoming request as JSON with any
+// stored requests/imps already merged into it, scans it to find any stored auction response ids
+// in the request/imps and produces a map of imp IDs to stored auction responses.
+// Note that processStoredAuctionResponses must be called after processStoredRequests
+// because stored imps and stored requests can contain stored auction responses
+// so the stored requests/imps have to be merged into the incoming request prior to processing stored auction responses.
+func (deps *endpointDeps) processStoredAuctionResponses(ctx context.Context, requestJson []byte) (map[string]json.RawMessage, []error) {
+	impInfo, errs := parseImpInfo(requestJson)
+	if len(errs) > 0 {
+		return nil, errs
+	}
+
+	storedAuctionResponseIds := make([]string, 0, 0) //all stored responses ids from all imps
+	//because of bulk fetch responses we need to map imp id to stored resp body
+	impIdToRespId := make(map[string]string)              //imp id to stored resp id
+	impIdToStoredResp := make(map[string]json.RawMessage) //imp id to stored resp body
+	for index, impData := range impInfo {
+
+		if impData.ImpExtPrebid.StoredAuctionResponse != nil {
+			if len(impData.ImpExtPrebid.StoredAuctionResponse.ID) == 0 {
+				return nil, []error{fmt.Errorf("request.imp[%d] has ext.prebid.storedauctionresponse specified, but \"id\" field is missing ", index)}
+			}
+			storedAuctionResponseIds = append(storedAuctionResponseIds, impData.ImpExtPrebid.StoredAuctionResponse.ID)
+
+			impId, err := jsonparser.GetString(impData.Imp, "id")
+			if err != nil {
+				return nil, []error{err}
+			}
+
+			impIdToRespId[impId] = impData.ImpExtPrebid.StoredAuctionResponse.ID
+
+		}
+	}
+	if len(storedAuctionResponseIds) > 0 {
+		storedAuctionResponses, errs := deps.storedRespFetcher.FetchResponses(ctx, storedAuctionResponseIds)
+		if len(errs) > 0 {
+			return nil, errs
+		}
+		for impId, respId := range impIdToRespId {
+			impIdToStoredResp[impId] = storedAuctionResponses[respId]
+		}
+		return impIdToStoredResp, nil
+	}
+	return nil, nil
+}
+
 func (deps *endpointDeps) processStoredRequests(ctx context.Context, requestJson []byte, impInfo []ImpExtPrebidData) ([]byte, map[string]exchange.ImpExtInfo, []error) {
 	// Parse the Stored Request IDs from the BidRequest and Imps.
 	storedBidRequestId, hasStoredBidRequest, err := getStoredRequestId(requestJson)
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/imp-with-stored-resp.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/imp-with-stored-resp.json
new file mode 100644
index 00000000..315ec934
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/imp-with-stored-resp.json
@@ -0,0 +1,40 @@
+{
+  "description": "Well formed amp request with impression with stored response",
+  "mockBidRequest": {
+    "id": "request-with-stored-resp",
+    "site": {
+      "page": "test.somepage.com"
+    },
+    "imp": [
+      {
+        "id": "my-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          },
+          "prebid": {
+            "storedauctionresponse": {
+              "id": "6d718149"
+            }
+          }
+        }
+      }
+    ],
+    "user": {
+      "yob": 1989
+    }
+  },
+  "expectedBidResponse": {
+      "id":"request-with-stored-resp",
+      "bidid":"bid_id"
+  },
+  "expectedReturnCode": 200
+}
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-imp-stored-response.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-imp-stored-response.json
new file mode 100644
index 00000000..e6fd6687
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-imp-stored-response.json
@@ -0,0 +1,33 @@
+{
+  "description": "request with valid imp with stored response",
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "site": {
+      "page": "test.somepage.com"
+    },
+    "imp": [
+      {
+        "id": "imp-id1",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          },
+          "prebid": {
+            "storedauctionresponse": {
+              "id": "6d718149"
+            }
+          }
+        }
+      }
+    ]
+  },
+  "expectedReturnCode": 200
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-two-imps-missing-stored-response.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-two-imps-missing-stored-response.json
new file mode 100644
index 00000000..fcf8fbe3
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-two-imps-missing-stored-response.json
@@ -0,0 +1,54 @@
+{
+  "description": "request with two valid imp with stored responses",
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "site": {
+      "page": "test.somepage.com"
+    },
+    "imp": [
+      {
+        "id": "imp-id1",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          },
+          "prebid": {
+            "storedauctionresponse": {
+              "id": "6d718149"
+            }
+          }
+        }
+      },
+      {
+        "id": "imp-id2",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          },
+          "prebid": {
+            "storedauctionresponse": {
+              "id": "123"
+            }
+          }
+        }
+      }
+    ]
+  },
+  "expectedReturnCode": 200
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-two-imps-one-stored-response.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-two-imps-one-stored-response.json
new file mode 100644
index 00000000..b5aec656
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-two-imps-one-stored-response.json
@@ -0,0 +1,49 @@
+{
+  "description": "request with two valid imp with stored responses",
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "site": {
+      "page": "test.somepage.com"
+    },
+    "imp": [
+      {
+        "id": "imp-id1",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          },
+          "prebid": {
+            "storedauctionresponse": {
+              "id": "6d718149"
+            }
+          }
+        }
+      },
+      {
+        "id": "imp-id2",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          }
+        }
+      }
+    ]
+  },
+  "expectedReturnCode": 200
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-two-imps-stored-response.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-two-imps-stored-response.json
new file mode 100644
index 00000000..08c39b2f
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-two-imps-stored-response.json
@@ -0,0 +1,54 @@
+{
+  "description": "request with two valid imp with stored responses",
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "site": {
+      "page": "test.somepage.com"
+    },
+    "imp": [
+      {
+        "id": "imp-id1",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          },
+          "prebid": {
+            "storedauctionresponse": {
+              "id": "6d718149"
+            }
+          }
+        }
+      },
+      {
+        "id": "imp-id2",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          },
+          "prebid": {
+            "storedauctionresponse": {
+              "id": "6d715835"
+            }
+          }
+        }
+      }
+    ]
+  },
+  "expectedReturnCode": 200
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go
index 15fe118c..d3324a34 100644
--- a/endpoints/openrtb2/video_auction.go
+++ b/endpoints/openrtb2/video_auction.go
@@ -15,11 +15,11 @@ import (
 	"time"
 
 	"github.com/buger/jsonparser"
-	jsonpatch "github.com/evanphx/json-patch/v5"
 	"github.com/gofrs/uuid"
 	"github.com/golang/glog"
 	"github.com/julienschmidt/httprouter"
 	"github.com/mxmCherry/openrtb/v15/openrtb2"
+	jsonpatch "gopkg.in/evanphx/json-patch.v4"
 
 	accountService "github.com/prebid/prebid-server/account"
 	"github.com/prebid/prebid-server/analytics"
@@ -249,7 +249,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 	// Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers).
 	deps.setFieldsImplicitly(r, bidReq) // move after merge
 
-	errL = deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: bidReq}, false)
+	errL = deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: bidReq}, false, false)
 	if errortypes.ContainsFatalError(errL) {
 		handleError(&labels, w, errL, &vo, &debugLog)
 		return
diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go
index 46fbfe51..7e98d688 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -212,6 +212,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderInvibes:           invibes.Builder,
 		openrtb_ext.BidderIQZone:            iqzone.Builder,
 		openrtb_ext.BidderIx:                ix.Builder,
+		openrtb_ext.BidderJANet:             adtelligent.Builder,
 		openrtb_ext.BidderJixie:             jixie.Builder,
 		openrtb_ext.BidderKayzen:            kayzen.Builder,
 		openrtb_ext.BidderKidoz:             kidoz.Builder,
diff --git a/exchange/events.go b/exchange/events.go
index 28dbddb7..02fe10a9 100644
--- a/exchange/events.go
+++ b/exchange/events.go
@@ -4,7 +4,8 @@ import (
 	"encoding/json"
 	"time"
 
-	jsonpatch "github.com/evanphx/json-patch/v5"
+	jsonpatch "gopkg.in/evanphx/json-patch.v4"
+
 	"github.com/prebid/prebid-server/analytics"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/endpoints/events"
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 6298d88a..08fd3117 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -162,6 +162,8 @@ type AuctionRequest struct {
 	// in HoldAuction until we get to factoring it away. Do not use for anything new.
 	LegacyLabels   metrics.Labels
 	FirstPartyData map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData
+	// map of imp id to stored response
+	StoredAuctionResponses map[string]json.RawMessage
 }
 
 // BidderRequest holds the bidder specific request and all other
@@ -200,9 +202,6 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *
 
 	e.me.RecordRequestPrivacy(privacyLabels)
 
-	// List of bidders we have requests for.
-	liveAdapters := listBiddersWithRequests(bidderRequests)
-
 	// If we need to cache bids, then it will take some time to call prebid cache.
 	// We should reduce the amount of time the bidders have, to compensate.
 	auctionCtx, cancel := e.makeAuctionContext(ctx, cacheInstructions.cacheBids)
@@ -211,7 +210,25 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *
 	// Get currency rates conversions for the auction
 	conversions := e.getAuctionCurrencyRates(requestExt.Prebid.CurrencyConversions)
 
-	adapterBids, adapterExtra, anyBidsReturned := e.getAllBids(auctionCtx, bidderRequests, bidAdjustmentFactors, conversions, accountDebugAllow, r.GlobalPrivacyControlHeader, debugLog.DebugOverride)
+	var adapterBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid
+	var adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra
+	var anyBidsReturned bool
+
+	// List of bidders we have requests for.
+	var liveAdapters []openrtb_ext.BidderName
+
+	if len(r.StoredAuctionResponses) > 0 {
+		adapterBids, liveAdapters, err = buildStoredAuctionResponse(r.StoredAuctionResponses)
+		if err != nil {
+			return nil, err
+		}
+		anyBidsReturned = true
+
+	} else {
+		// List of bidders we have requests for.
+		liveAdapters = listBiddersWithRequests(bidderRequests)
+		adapterBids, adapterExtra, anyBidsReturned = e.getAllBids(auctionCtx, bidderRequests, bidAdjustmentFactors, conversions, accountDebugAllow, r.GlobalPrivacyControlHeader, debugLog.DebugOverride)
+	}
 
 	var auc *auction
 	var cacheErrs []error
@@ -1076,3 +1093,39 @@ func listBiddersWithRequests(bidderRequests []BidderRequest) []openrtb_ext.Bidde
 
 	return liveAdapters
 }
+
+func buildStoredAuctionResponse(storedAuctionResponses map[string]json.RawMessage) (map[openrtb_ext.BidderName]*pbsOrtbSeatBid, []openrtb_ext.BidderName, error) {
+
+	adapterBids := make(map[openrtb_ext.BidderName]*pbsOrtbSeatBid, 0)
+	liveAdapters := make([]openrtb_ext.BidderName, 0)
+	for impId, storedResp := range storedAuctionResponses {
+		var seatBids []openrtb2.SeatBid
+
+		if err := json.Unmarshal(storedResp, &seatBids); err != nil {
+			return nil, nil, err
+		}
+		for _, seat := range seatBids {
+			var bidsToAdd []*pbsOrtbBid
+			//set imp id from request
+			for i := range seat.Bid {
+				seat.Bid[i].ImpID = impId
+				bidsToAdd = append(bidsToAdd, &pbsOrtbBid{bid: &seat.Bid[i]})
+			}
+
+			bidderName := openrtb_ext.BidderName(seat.Seat)
+
+			if _, ok := adapterBids[bidderName]; ok {
+				adapterBids[bidderName].bids = append(adapterBids[bidderName].bids, bidsToAdd...)
+
+			} else {
+				//create new seat bid and add it to live adapters
+				liveAdapters = append(liveAdapters, bidderName)
+				newSeatBid := pbsOrtbSeatBid{bidsToAdd, "", nil}
+				adapterBids[bidderName] = &newSeatBid
+
+			}
+		}
+	}
+
+	return adapterBids, liveAdapters, nil
+}
diff --git a/firstpartydata/first_party_data.go b/firstpartydata/first_party_data.go
index 89498d06..d1940448 100644
--- a/firstpartydata/first_party_data.go
+++ b/firstpartydata/first_party_data.go
@@ -4,8 +4,9 @@ import (
 	"encoding/json"
 	"fmt"
 
-	jsonpatch "github.com/evanphx/json-patch/v5"
 	"github.com/mxmCherry/openrtb/v15/openrtb2"
+	jsonpatch "gopkg.in/evanphx/json-patch.v4"
+
 	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/openrtb_ext"
 )
diff --git a/go.mod b/go.mod
index 83857ff9..cbe88128 100644
--- a/go.mod
+++ b/go.mod
@@ -11,31 +11,32 @@ require (
 	github.com/chasex/glog v0.0.0-20160217080310-c62392af379c
 	github.com/coocood/freecache v1.2.0
 	github.com/docker/go-units v0.4.0
-	github.com/evanphx/json-patch/v5 v5.6.0
 	github.com/gofrs/uuid v4.2.0+incompatible
 	github.com/golang/glog v1.0.0
-	github.com/influxdata/influxdb v1.6.1
+	github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab // indirect
 	github.com/julienschmidt/httprouter v1.3.0
+	github.com/kr/text v0.2.0 // indirect
 	github.com/lib/pq v1.0.0
-	github.com/mitchellh/copystructure v1.1.2
+	github.com/mitchellh/copystructure v1.2.0
 	github.com/mxmCherry/openrtb/v15 v15.0.1
 	github.com/prebid/go-gdpr v1.11.0
-	github.com/prometheus/client_golang v1.11.0
+	github.com/prometheus/client_golang v1.12.1
 	github.com/prometheus/client_model v0.2.0
-	github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165
+	github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475
 	github.com/rs/cors v1.8.2
 	github.com/sergi/go-diff v1.0.0 // indirect
 	github.com/spaolacci/murmur3 v1.1.0 // indirect
 	github.com/spf13/viper v1.10.1
+	github.com/stretchr/objx v0.3.0 // indirect
 	github.com/stretchr/testify v1.7.0
-	github.com/vrischmann/go-metrics-influxdb v0.0.0-20160917065939-43af8332c303
-	github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
-	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
-	github.com/xeipuuv/gojsonschema v0.0.0-20180816142147-da425ebb7609
+	github.com/vrischmann/go-metrics-influxdb v0.1.1
+	github.com/xeipuuv/gojsonschema v1.2.0
 	github.com/yudai/gojsondiff v1.0.0
 	github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
 	github.com/yudai/pp v2.0.1+incompatible // indirect
 	golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd
 	golang.org/x/text v0.3.7
-	gopkg.in/yaml.v2 v2.4.0
+	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
+	gopkg.in/evanphx/json-patch.v4 v4.12.0
+	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
 )
diff --git a/go.sum b/go.sum
index 1346c085..a5d98304 100644
--- a/go.sum
+++ b/go.sum
@@ -104,6 +104,7 @@ github.com/coocood/freecache v1.2.0 h1:p8RhjN6Y4DRBIMzdRlm1y+M7h7YJxye3lGW8/VvzC
 github.com/coocood/freecache v1.2.0/go.mod h1:OKrEjkGVoxZhyWAJoeFi5BMLUJm2Tit0kpGkIr7NGYY=
 github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -120,8 +121,6 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.
 github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
-github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
-github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
 github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
@@ -254,9 +253,9 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
 github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/influxdata/influxdb v1.6.1 h1:OseoBlzI5ftNI/bczyxSWq6PKRCNEeiXvyWP/wS5fB0=
-github.com/influxdata/influxdb v1.6.1/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY=
-github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
+github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab h1:HqW4xhhynfjrtEiiSGcQUd6vrK23iMam1FO8rI7mwig=
+github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
 github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -275,11 +274,13 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
 github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
 github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
@@ -303,16 +304,16 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5
 github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
 github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
 github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
-github.com/mitchellh/copystructure v1.1.2 h1:Th2TIvG1+6ma3e/0/bopBKohOTY7s4dA8V2q4EUcBJ0=
-github.com/mitchellh/copystructure v1.1.2/go.mod h1:EBArHfARyrSWO/+Wyr9zwEkc6XMFB9XyNgFNmRkZZU4=
+github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
+github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
 github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
 github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE=
-github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
+github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
@@ -352,8 +353,9 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
 github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
 github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
 github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
-github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
 github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
+github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk=
+github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -362,16 +364,19 @@ github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6T
 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
 github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
-github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ=
 github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
+github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
+github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
 github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
-github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
 github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
-github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165 h1:nkcn14uNmFEuGCb2mBZbBb24RdNRL08b/wb+xBOYpuk=
-github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
+github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
+github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
+github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U=
@@ -399,8 +404,9 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
 github.com/spf13/viper v1.10.1 h1:nuJZuYpG7gTj/XqiUwg8bA0cp1+M2mC3J4g5luUYBKk=
 github.com/spf13/viper v1.10.1/go.mod h1:IGlFPqhNAPKRxohIzWpI5QEy4kuI7tcl5WvR+8qy1rU=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As=
+github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@@ -411,14 +417,14 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
-github.com/vrischmann/go-metrics-influxdb v0.0.0-20160917065939-43af8332c303 h1:Va10CytCCYRm4xBTses5ZDeDjeIQjhaiC9nRCe/yflI=
-github.com/vrischmann/go-metrics-influxdb v0.0.0-20160917065939-43af8332c303/go.mod h1:Xdcad1nGVhQfhoV0go+/4WaI/RZkWlvfjkVCdpMTxPY=
+github.com/vrischmann/go-metrics-influxdb v0.1.1 h1:xneKFRjsS4BiVYvAKaM/rOlXYd1pGHksnES0ECCJLgo=
+github.com/vrischmann/go-metrics-influxdb v0.1.1/go.mod h1:q7YC8bFETCYopXRMtUvQQdLaoVhpsEwvQS2zZEYCqg8=
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
-github.com/xeipuuv/gojsonschema v0.0.0-20180816142147-da425ebb7609 h1:BcMExZAULPkihVZ7UJXK7t8rwGqisXFw75tILnafhBY=
-github.com/xeipuuv/gojsonschema v0.0.0-20180816142147-da425ebb7609/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
+github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
+github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
 github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
 github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
 github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
@@ -531,6 +537,7 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
 golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
@@ -636,8 +643,9 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0=
+golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -858,9 +866,12 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
+gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/ini.v1 v1.66.2 h1:XfR1dOYubytKy4Shzc2LHrrGhU0lDCfDGG1yLPmpgsI=
 gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
diff --git a/metrics/config/metrics.go b/metrics/config/metrics.go
index 66849db5..d8df48fa 100644
--- a/metrics/config/metrics.go
+++ b/metrics/config/metrics.go
@@ -24,14 +24,17 @@ func NewMetricsEngine(cfg *config.Configuration, adapterList []openrtb_ext.Bidde
 		// Currently use go-metrics as the metrics piece for influx
 		returnEngine.GoMetrics = metrics.NewMetrics(gometrics.NewPrefixedRegistry("prebidserver."), adapterList, cfg.Metrics.Disabled, syncerKeys)
 		engineList = append(engineList, returnEngine.GoMetrics)
+
 		// Set up the Influx logger
 		go influxdb.InfluxDB(
 			returnEngine.GoMetrics.MetricsRegistry,                             // metrics registry
 			time.Second*time.Duration(cfg.Metrics.Influxdb.MetricSendInterval), // Configurable interval
 			cfg.Metrics.Influxdb.Host,                                          // the InfluxDB url
 			cfg.Metrics.Influxdb.Database,                                      // your InfluxDB database
+			cfg.Metrics.Influxdb.Measurement,                                   // your measurement
 			cfg.Metrics.Influxdb.Username,                                      // your InfluxDB user
-			cfg.Metrics.Influxdb.Password,                                      // your InfluxDB password
+			cfg.Metrics.Influxdb.Password,                                      // your InfluxDB password,
+			cfg.Metrics.Influxdb.AlignTimestamps,                               // align timestamps
 		)
 		// Influx is not added to the engine list as goMetrics takes care of it already.
 	}
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index bb244d39..9e51e6e6 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -147,6 +147,7 @@ const (
 	BidderInvibes           BidderName = "invibes"
 	BidderIQZone            BidderName = "iqzone"
 	BidderIx                BidderName = "ix"
+	BidderJANet             BidderName = "janet"
 	BidderJixie             BidderName = "jixie"
 	BidderKayzen            BidderName = "kayzen"
 	BidderKidoz             BidderName = "kidoz"
@@ -293,6 +294,7 @@ func CoreBidderNames() []BidderName {
 		BidderInvibes,
 		BidderIQZone,
 		BidderIx,
+		BidderJANet,
 		BidderJixie,
 		BidderKayzen,
 		BidderKidoz,
diff --git a/openrtb_ext/imp.go b/openrtb_ext/imp.go
index b51f2cde..3331ce21 100644
--- a/openrtb_ext/imp.go
+++ b/openrtb_ext/imp.go
@@ -9,6 +9,9 @@ type ExtImpPrebid struct {
 	// StoredRequest specifies which stored impression to use, if any.
 	StoredRequest *ExtStoredRequest `json:"storedrequest"`
 
+	// StoredResponse specifies which stored impression to use, if any.
+	StoredAuctionResponse *ExtStoredAuctionResponse `json:"storedauctionresponse,omitempty"`
+
 	// IsRewardedInventory is a signal intended for video impressions. Must be 0 or 1.
 	IsRewardedInventory int8 `json:"is_rewarded_inventory"`
 
@@ -23,6 +26,11 @@ type ExtStoredRequest struct {
 	ID string `json:"id"`
 }
 
+// ExtStoredAuctionResponse defines the contract for bidrequest.imp[i].ext.prebid.storedauctionresponse
+type ExtStoredAuctionResponse struct {
+	ID string `json:"id"`
+}
+
 type Options struct {
 	EchoVideoAttrs bool `json:"echovideoattrs"`
 }
diff --git a/openrtb_ext/imp_adot.go b/openrtb_ext/imp_adot.go
index 5d741589..e959c7c3 100644
--- a/openrtb_ext/imp_adot.go
+++ b/openrtb_ext/imp_adot.go
@@ -1,6 +1,7 @@
 package openrtb_ext
 
 type ExtImpAdot struct {
-	Parallax    bool   `json:"parallax,omitempty"`
-	PlacementId string `json:"placementId,omitempty"`
+	Parallax      bool   `json:"parallax,omitempty"`
+	PlacementId   string `json:"placementId,omitempty"`
+	PublisherPath string `json:"publisherPath,omitempty"`
 }
diff --git a/openrtb_ext/imp_sharethrough.go b/openrtb_ext/imp_sharethrough.go
index 7c3f1f67..abcbe11e 100644
--- a/openrtb_ext/imp_sharethrough.go
+++ b/openrtb_ext/imp_sharethrough.go
@@ -1,31 +1,7 @@
 package openrtb_ext
 
-type ExtData struct {
-	PBAdSlot string `json:"pbadslot"`
-}
-
-// ExtImpSharethrough defines the contract for bidrequest.imp[i].ext.sharethrough
 type ExtImpSharethrough struct {
-	Pkey       string   `json:"pkey"`
-	Iframe     bool     `json:"iframe"`
-	IframeSize []int    `json:"iframeSize"`
-	BidFloor   float64  `json:"bidfloor"`
-	Data       *ExtData `json:"data,omitempty"`
-}
-
-type ExtImpSharethroughResponse struct {
-	AdServerRequestID string                       `json:"adserverRequestId"`
-	BidID             string                       `json:"bidId"`
-	Creatives         []ExtImpSharethroughCreative `json:"creatives"`
-}
-type ExtImpSharethroughCreative struct {
-	AuctionWinID string                             `json:"auctionWinId"`
-	CPM          float64                            `json:"cpm"`
-	Metadata     ExtImpSharethroughCreativeMetadata `json:"creative"`
-}
-
-type ExtImpSharethroughCreativeMetadata struct {
-	CampaignKey string `json:"campaign_key"`
-	CreativeKey string `json:"creative_key"`
-	DealID      string `json:"deal_id"`
+	Pkey string   `json:"pkey"`
+	BAdv []string `json:"badv,omitempty"`
+	BCat []string `json:"bcat,omitempty"`
 }
diff --git a/router/router.go b/router/router.go
index 44314409..e49f88b1 100644
--- a/router/router.go
+++ b/router/router.go
@@ -214,7 +214,7 @@ func New(cfg *config.Configuration, rateConvertor *currency.RateConverter) (r *R
 		glog.Fatalf("Failed to create the openrtb2 endpoint handler. %v", err)
 	}
 
-	ampEndpoint, err := openrtb2.NewAmpEndpoint(uuidGenerator, theExchange, paramsValidator, ampFetcher, accounts, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, activeBidders)
+	ampEndpoint, err := openrtb2.NewAmpEndpoint(uuidGenerator, theExchange, paramsValidator, ampFetcher, accounts, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, activeBidders, storedRespFetcher)
 	if err != nil {
 		glog.Fatalf("Failed to create the amp endpoint handler. %v", err)
 	}
diff --git a/static/bidder-info/janet.yaml b/static/bidder-info/janet.yaml
new file mode 100644
index 00000000..a6e98f02
--- /dev/null
+++ b/static/bidder-info/janet.yaml
@@ -0,0 +1,16 @@
+maintainer:
+  email: "info@thejmg.com"
+capabilities:
+  app:
+    mediaTypes:
+      - banner
+      - video
+  site:
+    mediaTypes:
+      - banner
+      - video
+userSync:
+  # JANet supports user syncing, but requires configuration by the host. contact this
+  # bidder directly at the email address in this file to ask about enabling user sync.
+  supports:
+    - iframe
diff --git a/static/bidder-info/sharethrough.yaml b/static/bidder-info/sharethrough.yaml
index 12c9c4bc..51cca6d0 100644
--- a/static/bidder-info/sharethrough.yaml
+++ b/static/bidder-info/sharethrough.yaml
@@ -1,16 +1,16 @@
 maintainer:
-  email: "pubgrowth.engineering@sharethrough.com"
+  email: pubgrowth.engineering@sharethrough.com
 gvlVendorID: 80
 capabilities:
   app:
     mediaTypes:
-      - native
       - banner
+      - video
   site:
     mediaTypes:
-      - native
       - banner
+      - video
 userSync:
   redirect:
-    url: "https://match.sharethrough.com/FGMrCMMc/v1?redirectUri={{.RedirectURL}}"
-    userMacro: "$UID"
+    url: https://match.sharethrough.com/FGMrCMMc/v1?redirectUri={{.RedirectURL}}
+    userMacro: $UID
diff --git a/static/bidder-info/synacormedia.yaml b/static/bidder-info/synacormedia.yaml
index 589fce9c..1769ab82 100644
--- a/static/bidder-info/synacormedia.yaml
+++ b/static/bidder-info/synacormedia.yaml
@@ -13,8 +13,3 @@ userSync:
   iframe:
     url: "https://ad-cdn.technoratimedia.com/html/usersync.html?cb={{.RedirectURL}}"
     userMacro: "[USER_ID]"
-userSync:
-  # synacormedia supports user syncing, but requires configuration by the host. contact this
-  # bidder directly at the email address in this file to ask about enabling user sync.
-  supports:
-    - iframe
\ No newline at end of file
diff --git a/static/bidder-params/adot.json b/static/bidder-params/adot.json
index fa2b8533..e82b17be 100644
--- a/static/bidder-params/adot.json
+++ b/static/bidder-params/adot.json
@@ -1,17 +1,21 @@
 {
-    "$schema": "http://json-schema.org/draft-04/schema#",
-    "title": "The Adot Adapter Params",
-    "description": "A schema which validates params accepted by Adot adapter",
-    "type": "object",
-    "properties": {
-        "placementId": {
-            "type": "string",
-            "description": "An ID which identifies this placement of the impression"
-        },
-        "parallax": {
-            "type": "boolean",
-            "description": "It determines if the wanted advertising format is a parallax."
-        }
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "The Adot Adapter Params",
+  "description": "A schema which validates params accepted by Adot adapter",
+  "type": "object",
+  "properties": {
+    "placementId": {
+      "type": "string",
+      "description": "An ID which identifies this placement of the impression"
     },
-    "required": []
-}
\ No newline at end of file
+    "parallax": {
+      "type": "boolean",
+      "description": "It determines if the wanted advertising format is a parallax."
+    },
+    "publisherPath": {
+      "type": "string",
+      "description": "An optional path used in the bid endpoint"
+    }
+  },
+  "required": []
+}
diff --git a/static/bidder-params/janet.json b/static/bidder-params/janet.json
new file mode 100644
index 00000000..38c2c2eb
--- /dev/null
+++ b/static/bidder-params/janet.json
@@ -0,0 +1,26 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "JANet Adapter Params",
+  "description": "A schema which validates params accepted by the JANet adapter",
+
+  "type": "object",
+  "properties": {
+    "placementId": {
+      "type": "integer",
+      "description": "An ID which identifies this placement of the impression"
+    },
+    "siteId": {
+      "type": "integer",
+      "description": "An ID which identifies the site selling the impression"
+    },
+    "aid": {
+      "type": "integer",
+      "description": "An ID which identifies the channel"
+    },
+    "bidFloor": {
+      "type": "number",
+      "description": "BidFloor, US Dollars"
+    }
+  },
+  "required": ["aid"]
+}
diff --git a/static/bidder-params/sharethrough.json b/static/bidder-params/sharethrough.json
index 1fe2949b..f1357e91 100644
--- a/static/bidder-params/sharethrough.json
+++ b/static/bidder-params/sharethrough.json
@@ -6,36 +6,23 @@
   "properties": {
     "pkey": {
       "type": "string",
-      "description": "placement key to use."
+      "description": "Placement Key"
     },
-    "iframe": {
-      "type": "boolean",
-      "description": "whether or not to stay in iframe",
-      "default": false
-    },
-    "iframeSize": {
+    "badv": {
       "type": "array",
-      "minItems": 2,
-      "maxItems": 2,
+      "minItems": 0,
       "items": {
-        "type": "integer"
+        "type": "string"
       },
-      "description": "iframe dimensions",
-      "default": [0, 0]
-    },
-    "bidfloor": {
-      "type": "number",
-      "description": "The floor price, or minimum amount, a publisher will accept for an impression, given in CPM in USD"
+      "description": "Blocked Advertiser Domains"
     },
-    "data": {
-      "type": "object",
-      "description": "Ad-specific first party data",
-      "properties": {
-        "pbadslot": {
-          "type": "string",
-          "description": "Prebid Ad Slot, see: https://docs.prebid.org/features/pbAdSlot.html"
-        }
-      }
+    "bcat": {
+      "type": "array",
+      "minItems": 0,
+      "items": {
+        "type": "string"
+      },
+      "description": "Blocked IAB Categories"
     }
   },
   "required": ["pkey"]


Test patch:
diff --git a/adapters/adot/adot_test.go b/adapters/adot/adot_test.go
index 3e0944b6..99bf9bd9 100644
--- a/adapters/adot/adot_test.go
+++ b/adapters/adot/adot_test.go
@@ -12,7 +12,7 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-const testsBidderEndpoint = "https://dsp.adotmob.com/headerbidding/bidrequest"
+const testsBidderEndpoint = "https://dsp.adotmob.com/headerbidding{PUBLISHER_PATH}/bidrequest"
 
 func TestJsonSamples(t *testing.T) {
 	bidder, buildErr := Builder(openrtb_ext.BidderAdot, config.Adapter{
@@ -38,7 +38,7 @@ func TestMediaTypeError(t *testing.T) {
 
 func TestBidResponseNoContent(t *testing.T) {
 	bidder, buildErr := Builder(openrtb_ext.BidderAdot, config.Adapter{
-		Endpoint: "https://dsp.adotmob.com/headerbidding/bidrequest"})
+		Endpoint: "https://dsp.adotmob.com/headerbidding{PUBLISHER_PATH}/bidrequest"})
 
 	if buildErr != nil {
 		t.Fatalf("Builder returned unexpected error %v", buildErr)
@@ -79,3 +79,18 @@ func TestResolveMacros(t *testing.T) {
 	assert.Equal(t, "adm:imp_123.45 amd:creativeview_123.45", bid.AdM)
 	assert.Equal(t, "nurl_123.45", bid.NURL)
 }
+
+func TestGetImpAdotExt(t *testing.T) {
+	ext := &openrtb2.Imp{Ext: json.RawMessage(`{"bidder":{"publisherPath": "/hubvisor"}}`)}
+	adotExt := getImpAdotExt(ext)
+	assert.Equal(t, adotExt.PublisherPath, "/hubvisor")
+
+	emptyBidderExt := &openrtb2.Imp{Ext: json.RawMessage(`{"bidder":{}}`)}
+	emptyAdotBidderExt := getImpAdotExt(emptyBidderExt)
+	assert.NotNil(t, emptyAdotBidderExt)
+	assert.Equal(t, emptyAdotBidderExt.PublisherPath, "")
+
+	emptyExt := &openrtb2.Imp{Ext: json.RawMessage(`{}`)}
+	emptyAdotExt := getImpAdotExt(emptyExt)
+	assert.Nil(t, emptyAdotExt)
+}
diff --git a/adapters/adot/adottest/exemplary/simple-native.json b/adapters/adot/adottest/exemplary/simple-native.json
index ad7bccff..9ad26b26 100644
--- a/adapters/adot/adottest/exemplary/simple-native.json
+++ b/adapters/adot/adottest/exemplary/simple-native.json
@@ -41,20 +41,22 @@
           "seatbid": [
             {
               "seat": "adot",
-              "bid": [{
-                "id": "test-request-native-id",
-                "impid": "test-imp-native-id",
-                "price": 1.16346,
-                "adm": "some-test-ad imp_${AUCTION_PRICE} creativeview_${AUCTION_PRICE}",
-                "nurl": "nurl.link/win?p=${AUCTION_PRICE}",
-                "w": 300,
-                "h": 250,
-                "ext": {
-                  "adot": {
-                    "media_type": "native"
+              "bid": [
+                {
+                  "id": "test-request-native-id",
+                  "impid": "test-imp-native-id",
+                  "price": 1.16346,
+                  "adm": "some-test-ad imp_${AUCTION_PRICE} creativeview_${AUCTION_PRICE}",
+                  "nurl": "nurl.link/win?p=${AUCTION_PRICE}",
+                  "w": 300,
+                  "h": 250,
+                  "ext": {
+                    "adot": {
+                      "media_type": "native"
+                    }
                   }
                 }
-              }]
+              ]
             }
           ],
           "cur": "USD"
@@ -87,4 +89,3 @@
     }
   ]
 }
-
diff --git a/adapters/adot/adottest/exemplary/simple-video.json b/adapters/adot/adottest/exemplary/simple-video.json
index 4c850318..f4620a9e 100644
--- a/adapters/adot/adottest/exemplary/simple-video.json
+++ b/adapters/adot/adottest/exemplary/simple-video.json
@@ -9,20 +9,10 @@
           "h": 250,
           "maxduration": 60,
           "minduration": 1,
-          "api": [
-            1,
-            2,
-            5,
-            6,
-            7
-          ],
-          "mimes": [
-            "video\/mp4"
-          ],
+          "api": [1, 2, 5, 6, 7],
+          "mimes": ["video/mp4"],
           "placement": 4,
-          "protocols": [
-            2
-          ]
+          "protocols": [2]
         },
         "ext": {
           "adot": {}
@@ -44,20 +34,10 @@
                 "h": 250,
                 "maxduration": 60,
                 "minduration": 1,
-                "api": [
-                  1,
-                  2,
-                  5,
-                  6,
-                  7
-                ],
-                "mimes": [
-                  "video\/mp4"
-                ],
+                "api": [1, 2, 5, 6, 7],
+                "mimes": ["video/mp4"],
                 "placement": 4,
-                "protocols": [
-                  2
-                ]
+                "protocols": [2]
               },
               "ext": {
                 "adot": {}
@@ -73,20 +53,22 @@
           "seatbid": [
             {
               "seat": "adot",
-              "bid": [{
-                "id": "test-request-video-id",
-                "impid": "test-imp-video-id",
-                "price": 1.16346,
-                "adm": "some-test-ad imp_${AUCTION_PRICE} creativeview_${AUCTION_PRICE}",
-                "nurl": "nurl.link/win?p=${AUCTION_PRICE}",
-                "w": 300,
-                "h": 250,
-                "ext": {
-                  "adot": {
-                    "media_type": "video"
+              "bid": [
+                {
+                  "id": "test-request-video-id",
+                  "impid": "test-imp-video-id",
+                  "price": 1.16346,
+                  "adm": "some-test-ad imp_${AUCTION_PRICE} creativeview_${AUCTION_PRICE}",
+                  "nurl": "nurl.link/win?p=${AUCTION_PRICE}",
+                  "w": 300,
+                  "h": 250,
+                  "ext": {
+                    "adot": {
+                      "media_type": "video"
+                    }
                   }
                 }
-              }]
+              ]
             }
           ],
           "cur": "USD"
@@ -119,4 +101,3 @@
     }
   ]
 }
-
diff --git a/adapters/adot/adottest/supplemental/ext-bidder-empty.json b/adapters/adot/adottest/supplemental/ext-bidder-empty.json
new file mode 100644
index 00000000..b3fc956f
--- /dev/null
+++ b/adapters/adot/adottest/supplemental/ext-bidder-empty.json
@@ -0,0 +1,107 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-publishPath-id",
+    "imp": [
+      {
+        "id": "test-imp-publishPath-id",
+        "banner": {
+          "format": [
+            {
+              "w": 320,
+              "h": 250
+            }
+          ],
+          "w": 320,
+          "h": 250
+        },
+        "ext": {
+          "bidder": {},
+          "adot": {}
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://dsp.adotmob.com/headerbidding/bidrequest",
+        "body": {
+          "id": "test-request-publishPath-id",
+          "imp": [
+            {
+              "id": "test-imp-publishPath-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 320,
+                    "h": 250
+                  }
+                ],
+                "w": 320,
+                "h": 250
+              },
+              "ext": {
+                "bidder": {
+                },
+                "adot": {}
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-publishPath-id",
+          "seatbid": [
+            {
+              "seat": "adot",
+              "bid": [
+                {
+                  "id": "test-request-publishPath-id",
+                  "impid": "test-imp-publishPath-id",
+                  "price": 1.16346,
+                  "adm": "some-test-ad imp_${AUCTION_PRICE} creativeview_${AUCTION_PRICE}",
+                  "nurl": "nurl.link/win?p=${AUCTION_PRICE}",
+                  "w": 320,
+                  "h": 50,
+                  "ext": {
+                    "adot": {
+                      "media_type": "banner"
+                    }
+                  }
+                }
+              ]
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "test-request-publishPath-id",
+            "impid": "test-imp-publishPath-id",
+            "price": 1.16346,
+            "adm": "some-test-ad imp_1.16346 creativeview_1.16346",
+            "nurl": "nurl.link/win?p=1.16346",
+            "w": 320,
+            "h": 50,
+            "ext": {
+              "adot": {
+                "media_type": "banner"
+              }
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/adot/adottest/supplemental/ext-bidder-publisher-path.json b/adapters/adot/adottest/supplemental/ext-bidder-publisher-path.json
new file mode 100644
index 00000000..bf69d12a
--- /dev/null
+++ b/adapters/adot/adottest/supplemental/ext-bidder-publisher-path.json
@@ -0,0 +1,110 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-publishPath-id",
+    "imp": [
+      {
+        "id": "test-imp-publishPath-id",
+        "banner": {
+          "format": [
+            {
+              "w": 320,
+              "h": 250
+            }
+          ],
+          "w": 320,
+          "h": 250
+        },
+        "ext": {
+          "bidder": {
+            "publisherPath": "/hubvisor"
+          },
+          "adot": {}
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://dsp.adotmob.com/headerbidding/hubvisor/bidrequest",
+        "body": {
+          "id": "test-request-publishPath-id",
+          "imp": [
+            {
+              "id": "test-imp-publishPath-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 320,
+                    "h": 250
+                  }
+                ],
+                "w": 320,
+                "h": 250
+              },
+              "ext": {
+                "bidder": {
+                  "publisherPath": "/hubvisor"
+                },
+                "adot": {}
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-publishPath-id",
+          "seatbid": [
+            {
+              "seat": "adot",
+              "bid": [
+                {
+                  "id": "test-request-publishPath-id",
+                  "impid": "test-imp-publishPath-id",
+                  "price": 1.16346,
+                  "adm": "some-test-ad imp_${AUCTION_PRICE} creativeview_${AUCTION_PRICE}",
+                  "nurl": "nurl.link/win?p=${AUCTION_PRICE}",
+                  "w": 320,
+                  "h": 50,
+                  "ext": {
+                    "adot": {
+                      "media_type": "banner"
+                    }
+                  }
+                }
+              ]
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "test-request-publishPath-id",
+            "impid": "test-imp-publishPath-id",
+            "price": 1.16346,
+            "adm": "some-test-ad imp_1.16346 creativeview_1.16346",
+            "nurl": "nurl.link/win?p=1.16346",
+            "w": 320,
+            "h": 50,
+            "ext": {
+              "adot": {
+                "media_type": "banner"
+              }
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/adot/adottest/supplemental/simple-audio.json b/adapters/adot/adottest/supplemental/simple-audio.json
index 9be53c4c..cd19d140 100644
--- a/adapters/adot/adottest/supplemental/simple-audio.json
+++ b/adapters/adot/adottest/supplemental/simple-audio.json
@@ -43,17 +43,19 @@
           "seatbid": [
             {
               "seat": "adot",
-              "bid": [{
-                "id": "test-request-audio-id",
-                "impid": "test-imp-audio-id",
-                "price": 1.16346,
-                "adm": "some-audio-ad",
-                "ext": {
-                  "adot": {
-                    "media_type": "audio"
+              "bid": [
+                {
+                  "id": "test-request-audio-id",
+                  "impid": "test-imp-audio-id",
+                  "price": 1.16346,
+                  "adm": "some-audio-ad",
+                  "ext": {
+                    "adot": {
+                      "media_type": "audio"
+                    }
                   }
                 }
-              }]
+              ]
             }
           ],
           "cur": "USD"
diff --git a/adapters/adot/adottest/supplemental/status_500.json b/adapters/adot/adottest/supplemental/status_500.json
index 879bb8c5..f749ecc8 100644
--- a/adapters/adot/adottest/supplemental/status_500.json
+++ b/adapters/adot/adottest/supplemental/status_500.json
@@ -59,4 +59,3 @@
     }
   ]
 }
-
diff --git a/adapters/adot/adottest/supplemental/unmarshal_error.json b/adapters/adot/adottest/supplemental/unmarshal_error.json
index 3094fa86..557fb16c 100644
--- a/adapters/adot/adottest/supplemental/unmarshal_error.json
+++ b/adapters/adot/adottest/supplemental/unmarshal_error.json
@@ -49,8 +49,9 @@
       "mockResponse": {
         "status": 200,
         "body": "fail for unmarshal"
-        }
-    }],
+      }
+    }
+  ],
 
   "expectedMakeBidsErrors": [
     {
@@ -59,4 +60,3 @@
     }
   ]
 }
-
diff --git a/adapters/adot/params_test.go b/adapters/adot/params_test.go
index 2f7b4b9a..bc458641 100644
--- a/adapters/adot/params_test.go
+++ b/adapters/adot/params_test.go
@@ -2,8 +2,9 @@ package adot
 
 import (
 	"encoding/json"
-	"github.com/prebid/prebid-server/openrtb_ext"
 	"testing"
+
+	"github.com/prebid/prebid-server/openrtb_ext"
 )
 
 // This file actually intends to test static/bidder-params/adot.json
@@ -42,12 +43,16 @@ var validParams = []string{
 	`{"placementId": "test-114"}`,
 	`{"placementId": "test-113", "parallax": true}`,
 	`{"placementId": "test-113", "parallax": false}`,
+	`{"placementId": "test-113", "parallax": false, "publisherPath": "/hubvisor"}`,
+	`{"placementId": "test-113", "parallax": false, "publisherPath": ""}`,
 }
 
 var invalidParams = []string{
 	`{"parallax": 1}`,
 	`{"placementId": 135123}`,
+	`{"publisherPath": 111}`,
 	`{"placementId": 113, "parallax": 1}`,
 	`{"placementId": 142, "parallax": true}`,
 	`{"placementId": "test-114", "parallax": 1}`,
+	`{"placementId": "test-114", "parallax": true, "publisherPath": 111}`,
 }
diff --git a/adapters/sharethrough/butler_test.go b/adapters/sharethrough/butler_test.go
deleted file mode 100644
index 3d19eea9..00000000
--- a/adapters/sharethrough/butler_test.go
+++ /dev/null
@@ -1,569 +0,0 @@
-package sharethrough
-
-import (
-	"fmt"
-	"net/http"
-	"regexp"
-	"strconv"
-	"strings"
-	"testing"
-	"time"
-
-	"github.com/mxmCherry/openrtb/v15/openrtb2"
-	"github.com/prebid/prebid-server/adapters"
-	"github.com/prebid/prebid-server/errortypes"
-	"github.com/prebid/prebid-server/openrtb_ext"
-	"github.com/stretchr/testify/assert"
-)
-
-type MockUtil struct {
-	mockCanAutoPlayVideo func() bool
-	mockGdprApplies      func() bool
-	mockGetPlacementSize func() (int64, int64)
-	mockParseUserInfo    func() userInfo
-	UtilityInterface
-}
-
-func (m MockUtil) canAutoPlayVideo(userAgent string, parsers UserAgentParsers) bool {
-	return m.mockCanAutoPlayVideo()
-}
-
-func (m MockUtil) gdprApplies(request *openrtb2.BidRequest) bool {
-	return m.mockGdprApplies()
-}
-
-func (m MockUtil) getPlacementSize(imp openrtb2.Imp, strImpParams openrtb_ext.ExtImpSharethrough) (height, width int64) {
-	return m.mockGetPlacementSize()
-}
-
-func (m MockUtil) parseUserInfo(user *openrtb2.User) (ui userInfo) {
-	return m.mockParseUserInfo()
-}
-
-func (m MockUtil) getClock() ClockInterface {
-	return MockClock{}
-}
-
-type MockClock struct {
-	ClockInterface
-}
-
-func (m MockClock) now() time.Time {
-	return time.Date(2019, 9, 12, 11, 29, 0, 123456, time.UTC)
-}
-
-func assertRequestDataEquals(t *testing.T, testName string, expected *adapters.RequestData, actual *adapters.RequestData) {
-	t.Logf("Test case: %s\n", testName)
-	if expected.Method != actual.Method {
-		t.Errorf("Method mismatch: expected %s got %s\n", expected.Method, actual.Method)
-	}
-	if expected.Uri != actual.Uri {
-		t.Errorf("Uri mismatch: expected %s got %s\n", expected.Uri, actual.Uri)
-	}
-	if len(expected.Body) != len(actual.Body) {
-		t.Errorf("Body mismatch: expected %s got %s\n", expected.Body, actual.Body)
-	}
-	if len(expected.Headers) != len(actual.Headers) {
-		t.Errorf("Number of headers mismatch: expected %d got %d\n", len(expected.Headers), len(actual.Headers))
-	}
-	for headerIndex, expectedHeader := range expected.Headers {
-		if expectedHeader[0] != actual.Headers[headerIndex][0] {
-			t.Errorf("Header %s mismatch: expected %s got %s\n", headerIndex, expectedHeader[0], actual.Headers[headerIndex][0])
-		}
-	}
-}
-
-func TestSuccessRequestFromOpenRTB(t *testing.T) {
-	tests := map[string]struct {
-		inputImp openrtb2.Imp
-		inputReq *openrtb2.BidRequest
-		inputDom string
-		expected *adapters.RequestData
-	}{
-		"Generates the correct AdServer request from Imp (no user provided)": {
-			inputImp: openrtb2.Imp{
-				ID:  "abc",
-				Ext: []byte(`{ "bidder": {"pkey": "pkey", "iframe": true, "iframeSize": [10, 20], "bidfloor": 1.0, "data": { "pbadslot": "adslot" } } }`),
-				Banner: &openrtb2.Banner{
-					Format: []openrtb2.Format{{H: 30, W: 40}},
-				},
-			},
-			inputReq: &openrtb2.BidRequest{
-				App: &openrtb2.App{Ext: []byte(`{}`)},
-				Device: &openrtb2.Device{
-					UA: "Android Chome/60",
-					IP: "127.0.0.1",
-				},
-				Site: &openrtb2.Site{Page: "http://a.domain.com/page"},
-				BAdv: []string{"domain1.com", "domain2.com"},
-				TMax: 700,
-			},
-			inputDom: "http://a.domain.com",
-			expected: &adapters.RequestData{
-				Method: "POST",
-				Uri:    "http://abc.com",
-				Body:   []byte(`{"badv":["domain1.com","domain2.com"],"tmax":700,"deadline":"2019-09-12T11:29:00.700123456Z","bidfloor":1}`),
-				Headers: http.Header{
-					"Content-Type":    []string{"application/json;charset=utf-8"},
-					"Accept":          []string{"application/json"},
-					"Origin":          []string{"http://a.domain.com"},
-					"Referer":         []string{"http://a.domain.com/page"},
-					"User-Agent":      []string{"Android Chome/60"},
-					"X-Forwarded-For": []string{"127.0.0.1"},
-				},
-			},
-		},
-		"Generates width/height if not provided": {
-			inputImp: openrtb2.Imp{
-				ID:  "abc",
-				Ext: []byte(`{ "bidder": {"pkey": "pkey", "iframe": true} }`),
-				Banner: &openrtb2.Banner{
-					Format: []openrtb2.Format{{H: 30, W: 40}},
-				},
-			},
-			inputReq: &openrtb2.BidRequest{
-				App: &openrtb2.App{Ext: []byte(`{}`)},
-				Device: &openrtb2.Device{
-					UA: "Android Chome/60",
-					IP: "127.0.0.1",
-				},
-				Site: &openrtb2.Site{Page: "http://a.domain.com/page"},
-				BAdv: []string{"domain1.com", "domain2.com"},
-				TMax: 700,
-			},
-			inputDom: "http://a.domain.com",
-			expected: &adapters.RequestData{
-				Method: "POST",
-				Uri:    "http://abc.com",
-				Body:   []byte(`{"badv":["domain1.com","domain2.com"],"tmax":700,"deadline":"2019-09-12T11:29:00.700123456Z"}`),
-				Headers: http.Header{
-					"Content-Type":    []string{"application/json;charset=utf-8"},
-					"Accept":          []string{"application/json"},
-					"Origin":          []string{"http://a.domain.com"},
-					"Referer":         []string{"http://a.domain.com/page"},
-					"User-Agent":      []string{"Android Chome/60"},
-					"X-Forwarded-For": []string{"127.0.0.1"},
-				},
-			},
-		},
-	}
-
-	mockUriHelper := MockStrUriHelper{
-		mockBuildUri: func() string {
-			return "http://abc.com"
-		},
-	}
-
-	mockUtil := MockUtil{
-		mockCanAutoPlayVideo: func() bool { return true },
-		mockGdprApplies:      func() bool { return true },
-		mockGetPlacementSize: func() (int64, int64) { return 100, 200 },
-		mockParseUserInfo:    func() userInfo { return userInfo{Consent: "ok", TtdUid: "ttduid", StxUid: "stxuid"} },
-	}
-
-	adServer := StrOpenRTBTranslator{UriHelper: mockUriHelper, Util: mockUtil, UserAgentParsers: UserAgentParsers{
-		ChromeVersion:    regexp.MustCompile(`Chrome\/(?P<ChromeVersion>\d+)`),
-		ChromeiOSVersion: regexp.MustCompile(`CriOS\/(?P<chromeiOSVersion>\d+)`),
-		SafariVersion:    regexp.MustCompile(`Version\/(?P<safariVersion>\d+)`),
-	}}
-	for testName, test := range tests {
-		outputSuccess, outputError := adServer.requestFromOpenRTB(test.inputImp, test.inputReq, test.inputDom)
-		assertRequestDataEquals(t, testName, test.expected, outputSuccess)
-		if outputError != nil {
-			t.Errorf("Expected no errors, got %s\n", outputError)
-		}
-	}
-}
-
-func TestFailureRequestFromOpenRTB(t *testing.T) {
-	tests := map[string]struct {
-		inputImp      openrtb2.Imp
-		inputReq      *openrtb2.BidRequest
-		expectedError string
-	}{
-		"Fails when unable to parse imp.Ext": {
-			inputImp: openrtb2.Imp{
-				Ext: []byte(`{"abc`),
-			},
-			inputReq: &openrtb2.BidRequest{
-				Device: &openrtb2.Device{UA: "A", IP: "ip"},
-				Site:   &openrtb2.Site{Page: "page"},
-			},
-			expectedError: `unexpected end of JSON input`,
-		},
-		"Fails when unable to parse imp.Ext.Bidder": {
-			inputImp: openrtb2.Imp{
-				Ext: []byte(`{ "bidder": "{ abc" }`),
-			},
-			inputReq: &openrtb2.BidRequest{
-				Device: &openrtb2.Device{UA: "A", IP: "ip"},
-				Site:   &openrtb2.Site{Page: "page"},
-			},
-			expectedError: `json: cannot unmarshal string into Go value of type openrtb_ext.ExtImpSharethrough`,
-		},
-	}
-
-	mockUriHelper := MockStrUriHelper{
-		mockBuildUri: func() string {
-			return "http://abc.com"
-		},
-	}
-
-	mockUtil := MockUtil{
-		mockCanAutoPlayVideo: func() bool { return true },
-		mockGdprApplies:      func() bool { return true },
-		mockGetPlacementSize: func() (int64, int64) { return 100, 200 },
-		mockParseUserInfo:    func() userInfo { return userInfo{Consent: "ok", TtdUid: "ttduid", StxUid: "stxuid"} },
-	}
-
-	adServer := StrOpenRTBTranslator{UriHelper: mockUriHelper, Util: mockUtil, UserAgentParsers: UserAgentParsers{
-		ChromeVersion:    regexp.MustCompile(`Chrome\/(?P<ChromeVersion>\d+)`),
-		ChromeiOSVersion: regexp.MustCompile(`CriOS\/(?P<chromeiOSVersion>\d+)`),
-		SafariVersion:    regexp.MustCompile(`Version\/(?P<safariVersion>\d+)`),
-	}}
-
-	assert := assert.New(t)
-	for testName, test := range tests {
-		t.Logf("Test case: %s\n", testName)
-		output, outputError := adServer.requestFromOpenRTB(test.inputImp, test.inputReq, "anything")
-
-		assert.Nil(output)
-		assert.NotNil(outputError)
-		assert.Equal(test.expectedError, outputError.Error())
-	}
-}
-
-func assertBidderResponseEquals(t *testing.T, testName string, expected adapters.BidderResponse, actual adapters.BidderResponse) {
-	t.Logf("Test case: %s\n", testName)
-	if len(expected.Bids) != len(actual.Bids) {
-		t.Errorf("Expected %d bids in BidResponse, got %d\n", len(expected.Bids), len(actual.Bids))
-		return
-	}
-	for index, expectedTypedBid := range expected.Bids {
-		if expectedTypedBid.BidType != actual.Bids[index].BidType {
-			t.Errorf("Bid[%d]: Type mismatch, expected %s got %s\n", index, expectedTypedBid.BidType, actual.Bids[index].BidType)
-		}
-		if expectedTypedBid.Bid.AdID != actual.Bids[index].Bid.AdID {
-			t.Errorf("Bid[%d]: AdID mismatch, expected %s got %s\n", index, expectedTypedBid.Bid.AdID, actual.Bids[index].Bid.AdID)
-		}
-		if expectedTypedBid.Bid.ID != actual.Bids[index].Bid.ID {
-			t.Errorf("Bid[%d]: ID mismatch, expected %s got %s\n", index, expectedTypedBid.Bid.ID, actual.Bids[index].Bid.ID)
-		}
-		if expectedTypedBid.Bid.ImpID != actual.Bids[index].Bid.ImpID {
-			t.Errorf("Bid[%d]: ImpID mismatch, expected %s got %s\n", index, expectedTypedBid.Bid.ImpID, actual.Bids[index].Bid.ImpID)
-		}
-		if expectedTypedBid.Bid.Price != actual.Bids[index].Bid.Price {
-			t.Errorf("Bid[%d]: Price mismatch, expected %f got %f\n", index, expectedTypedBid.Bid.Price, actual.Bids[index].Bid.Price)
-		}
-		if expectedTypedBid.Bid.CID != actual.Bids[index].Bid.CID {
-			t.Errorf("Bid[%d]: CID mismatch, expected %s got %s\n", index, expectedTypedBid.Bid.CID, actual.Bids[index].Bid.CID)
-		}
-		if expectedTypedBid.Bid.CrID != actual.Bids[index].Bid.CrID {
-			t.Errorf("Bid[%d]: CrID mismatch, expected %s got %s\n", index, expectedTypedBid.Bid.CrID, actual.Bids[index].Bid.CrID)
-		}
-		if expectedTypedBid.Bid.DealID != actual.Bids[index].Bid.DealID {
-			t.Errorf("Bid[%d]: DealID mismatch, expected %s got %s\n", index, expectedTypedBid.Bid.DealID, actual.Bids[index].Bid.DealID)
-		}
-		if expectedTypedBid.Bid.H != actual.Bids[index].Bid.H {
-			t.Errorf("Bid[%d]: H mismatch, expected %d got %d\n", index, expectedTypedBid.Bid.H, actual.Bids[index].Bid.H)
-		}
-		if expectedTypedBid.Bid.W != actual.Bids[index].Bid.W {
-			t.Errorf("Bid[%d]: W mismatch, expected %d got %d\n", index, expectedTypedBid.Bid.W, actual.Bids[index].Bid.W)
-		}
-	}
-}
-
-func TestSuccessResponseToOpenRTB(t *testing.T) {
-	tests := map[string]struct {
-		inputButlerReq  *adapters.RequestData
-		inputStrResp    []byte
-		expectedSuccess *adapters.BidderResponse
-		expectedErrors  []error
-	}{
-		"Generates expected openRTB bid response": {
-			inputButlerReq: &adapters.RequestData{
-				Uri: "http://uri.com?placement_key=pkey&bidId=bidid&height=20&width=30",
-			},
-			inputStrResp: []byte(`{ "adserverRequestId": "arid", "bidId": "bid", "creatives": [{"cpm": 10, "creative": {"campaign_key": "cmpKey", "creative_key": "creaKey", "deal_id": "dealId"}}] }`),
-			expectedSuccess: &adapters.BidderResponse{
-				Bids: []*adapters.TypedBid{{
-					BidType: openrtb_ext.BidTypeBanner,
-					Bid: &openrtb2.Bid{
-						AdID:   "arid",
-						ID:     "bid",
-						ImpID:  "bidid",
-						Price:  10,
-						CID:    "cmpKey",
-						CrID:   "creaKey",
-						DealID: "dealId",
-						H:      20,
-						W:      30,
-					},
-				}},
-			},
-			expectedErrors: []error{},
-		},
-	}
-
-	adServer := StrOpenRTBTranslator{Util: Util{Clock: MockClock{}}, UriHelper: StrUriHelper{}}
-	for testName, test := range tests {
-		outputSuccess, outputErrors := adServer.responseToOpenRTB(test.inputStrResp, test.inputButlerReq)
-		assertBidderResponseEquals(t, testName, *test.expectedSuccess, *outputSuccess)
-		if len(outputErrors) != len(test.expectedErrors) {
-			t.Errorf("Expected %d errors, got %d\n", len(test.expectedErrors), len(outputErrors))
-		}
-	}
-}
-
-func TestFailResponseToOpenRTB(t *testing.T) {
-	tests := map[string]struct {
-		inputButlerReq  *adapters.RequestData
-		inputStrResp    []byte
-		expectedSuccess *adapters.BidderResponse
-		expectedErrors  []error
-	}{
-		"Returns nil if no creatives provided": {
-			inputButlerReq:  &adapters.RequestData{},
-			inputStrResp:    []byte(`{}`),
-			expectedSuccess: nil,
-			expectedErrors: []error{
-				&errortypes.BadInput{Message: "No creative provided"},
-			},
-		},
-		"Returns nil if failed to parse Uri": {
-			inputButlerReq: &adapters.RequestData{
-				Uri: "wrong format url",
-			},
-			inputStrResp:    []byte(`{ "creatives": [{"creative": {}}] }`),
-			expectedSuccess: nil,
-			expectedErrors: []error{
-				&errortypes.BadInput{Message: `strconv.ParseUint: parsing "": invalid syntax`},
-			},
-		},
-		"Returns error if failed parsing body": {
-			inputButlerReq:  &adapters.RequestData{},
-			inputStrResp:    []byte(`{ wrong json`),
-			expectedSuccess: nil,
-			expectedErrors: []error{
-				&errortypes.BadInput{Message: "Unable to parse response JSON"},
-			},
-		},
-	}
-
-	adServer := StrOpenRTBTranslator{UriHelper: StrUriHelper{}}
-	for testName, test := range tests {
-		t.Logf("Test case: %s\n", testName)
-		outputSuccess, outputErrors := adServer.responseToOpenRTB(test.inputStrResp, test.inputButlerReq)
-
-		if test.expectedSuccess != outputSuccess {
-			t.Errorf("Expected result %+v, got %+v\n", test.expectedSuccess, outputSuccess)
-		}
-
-		if len(outputErrors) != len(test.expectedErrors) {
-			t.Errorf("Expected %d errors, got %d\n", len(test.expectedErrors), len(outputErrors))
-		}
-
-		for index, expectedError := range test.expectedErrors {
-			if fmt.Sprintf("%T", expectedError) != fmt.Sprintf("%T", outputErrors[index]) {
-				t.Errorf("Error type mismatch, expected %T, got %T\n", expectedError, outputErrors[index])
-			}
-			if expectedError.Error() != outputErrors[index].Error() {
-				t.Errorf("Expected error %s, got %s\n", expectedError.Error(), outputErrors[index].Error())
-			}
-		}
-	}
-}
-
-func TestBuildBody(t *testing.T) {
-	tests := map[string]struct {
-		inputRequest  *openrtb2.BidRequest
-		inputImp      openrtb_ext.ExtImpSharethrough
-		expectedJson  []byte
-		expectedError error
-	}{
-		"Empty input: skips badomains, tmax default to 10 sec and sets deadline accordingly": {
-			inputRequest:  &openrtb2.BidRequest{},
-			inputImp:      openrtb_ext.ExtImpSharethrough{},
-			expectedJson:  []byte(`{"tmax":10000, "deadline":"2019-09-12T11:29:10.000123456Z"}`),
-			expectedError: nil,
-		},
-		"Sets badv as list of domains according to Badv (tmax default to 10 sec and sets deadline accordingly)": {
-			inputRequest: &openrtb2.BidRequest{
-				BAdv: []string{"dom1.com", "dom2.com"},
-			},
-			inputImp:      openrtb_ext.ExtImpSharethrough{},
-			expectedJson:  []byte(`{"badv": ["dom1.com", "dom2.com"], "tmax":10000, "deadline":"2019-09-12T11:29:10.000123456Z"}`),
-			expectedError: nil,
-		},
-		"Sets tmax and deadline according to Tmax": {
-			inputRequest: &openrtb2.BidRequest{
-				TMax: 500,
-			},
-			inputImp:      openrtb_ext.ExtImpSharethrough{},
-			expectedJson:  []byte(`{"tmax": 500, "deadline":"2019-09-12T11:29:00.500123456Z"}`),
-			expectedError: nil,
-		},
-		"Sets bidfloor according to the Imp object": {
-			inputRequest: &openrtb2.BidRequest{},
-			inputImp: openrtb_ext.ExtImpSharethrough{
-				BidFloor: 1.23,
-			},
-			expectedJson:  []byte(`{"tmax":10000, "deadline":"2019-09-12T11:29:10.000123456Z", "bidfloor":1.23}`),
-			expectedError: nil,
-		},
-	}
-
-	assert := assert.New(t)
-	helper := StrBodyHelper{Clock: MockClock{}}
-
-	for testName, test := range tests {
-		t.Logf("Test case: %s\n", testName)
-		outputJson, outputError := helper.buildBody(test.inputRequest, test.inputImp)
-
-		assert.JSONEq(string(test.expectedJson), string(outputJson))
-		assert.Equal(test.expectedError, outputError)
-	}
-}
-
-func TestBuildUri(t *testing.T) {
-	tests := map[string]struct {
-		inputParams StrAdSeverParams
-		inputApp    *openrtb2.App
-		expected    []string
-	}{
-		"Generates expected URL, appending all params": {
-			inputParams: StrAdSeverParams{
-				Pkey:               "pkey",
-				BidID:              "bid",
-				GPID:               "gpid",
-				ConsentRequired:    true,
-				ConsentString:      "consent",
-				USPrivacySignal:    "ccpa",
-				InstantPlayCapable: true,
-				Iframe:             false,
-				Height:             20,
-				Width:              30,
-				TheTradeDeskUserId: "ttd123",
-				SharethroughUserId: "stx123",
-			},
-			expected: []string{
-				"http://abc.com?",
-				"placement_key=pkey",
-				"bidId=bid",
-				"gpid=gpid",
-				"consent_required=true",
-				"consent_string=consent",
-				"us_privacy=ccpa",
-				"instant_play_capable=true",
-				"stayInIframe=false",
-				"height=20",
-				"width=30",
-				"supplyId=FGMrCMMc",
-				"strVersion=" + strconv.FormatInt(strVersion, 10),
-				"ttduid=ttd123",
-				"stxuid=stx123",
-				"adRequestAt=2019-09-12T11%3A29%3A00.000123456Z",
-			},
-		},
-	}
-
-	uriHelper := StrUriHelper{BaseURI: "http://abc.com", Clock: MockClock{}}
-	for testName, test := range tests {
-		t.Logf("Test case: %s\n", testName)
-		output := uriHelper.buildUri(test.inputParams)
-
-		for _, uriParam := range test.expected {
-			if !strings.Contains(output, uriParam) {
-				t.Errorf("Expected %s to be found in URL, got %s\n", uriParam, output)
-			}
-		}
-	}
-}
-
-func assertStrAdServerParamsEquals(t *testing.T, testName string, expected *StrAdSeverParams, actual *StrAdSeverParams) {
-	t.Logf("Test case: %s\n", testName)
-	if expected.Pkey != actual.Pkey {
-		t.Errorf("Expected Pkey to be %s, got %s\n", expected.Pkey, actual.Pkey)
-	}
-	if expected.BidID != actual.BidID {
-		t.Errorf("Expected BidID to be %s, got %s\n", expected.BidID, actual.BidID)
-	}
-	if expected.Iframe != actual.Iframe {
-		t.Errorf("Expected Iframe to be %t, got %t\n", expected.Iframe, actual.Iframe)
-	}
-	if expected.Height != actual.Height {
-		t.Errorf("Expected Height to be %d, got %d\n", expected.Height, actual.Height)
-	}
-	if expected.Width != actual.Width {
-		t.Errorf("Expected Width to be %d, got %d\n", expected.Width, actual.Width)
-	}
-	if expected.ConsentRequired != actual.ConsentRequired {
-		t.Errorf("Expected ConsentRequired to be %t, got %t\n", expected.ConsentRequired, actual.ConsentRequired)
-	}
-	if expected.ConsentString != actual.ConsentString {
-		t.Errorf("Expected ConsentString to be %s, got %s\n", expected.ConsentString, actual.ConsentString)
-	}
-}
-
-func TestSuccessParseUri(t *testing.T) {
-	tests := map[string]struct {
-		input           string
-		expectedSuccess *StrAdSeverParams
-	}{
-		"Decodes URI successfully": {
-			input: "http://abc.com?placement_key=pkey&bidId=bid&consent_required=true&consent_string=consent&instant_play_capable=true&stayInIframe=false&height=20&width=30&hbVersion=1&supplyId=FGMrCMMc&strVersion=1.0.0",
-			expectedSuccess: &StrAdSeverParams{
-				Pkey:            "pkey",
-				BidID:           "bid",
-				Iframe:          false,
-				Height:          20,
-				Width:           30,
-				ConsentRequired: true,
-				ConsentString:   "consent",
-			},
-		},
-	}
-
-	uriHelper := StrUriHelper{}
-	for testName, test := range tests {
-		t.Logf("Test case: %s\n", testName)
-		output, actualError := uriHelper.parseUri(test.input)
-
-		assertStrAdServerParamsEquals(t, testName, test.expectedSuccess, output)
-		if actualError != nil {
-			t.Errorf("Expected no errors, got %s\n", actualError)
-		}
-	}
-}
-
-func TestFailParseUri(t *testing.T) {
-	tests := map[string]struct {
-		input         string
-		expectedError string
-	}{
-		"Fails decoding if unable to parse URI": {
-			input:         "test:/#$%?#",
-			expectedError: `parse (\")?test:/#\$%\?#(\")?: invalid URL escape \"%\?#\"`,
-		},
-		"Fails decoding if height not provided": {
-			input:         "http://abc.com?width=10",
-			expectedError: `strconv.ParseUint: parsing \"\": invalid syntax`,
-		},
-		"Fails decoding if width not provided": {
-			input:         "http://abc.com?height=10",
-			expectedError: `strconv.ParseUint: parsing \"\": invalid syntax`,
-		},
-	}
-
-	assert := assert.New(t)
-
-	uriHelper := StrUriHelper{}
-	for testName, test := range tests {
-		t.Logf("Test case: %s\n", testName)
-		output, actualError := uriHelper.parseUri(test.input)
-
-		assert.Nil(output)
-		assert.NotNil(actualError)
-		assert.Regexp(test.expectedError, actualError.Error())
-	}
-}
diff --git a/adapters/sharethrough/params_test.go b/adapters/sharethrough/params_test.go
index 416f4593..4edae124 100644
--- a/adapters/sharethrough/params_test.go
+++ b/adapters/sharethrough/params_test.go
@@ -10,12 +10,12 @@ import (
 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)
+		t.Fatalf("Failed to fetch the JSON schema. %v", err)
 	}
 
-	for _, validParam := range validParams {
-		if err := validator.Validate(openrtb_ext.BidderSharethrough, json.RawMessage(validParam)); err != nil {
-			t.Errorf("Schema rejected Sharethrough params: %s", validParam)
+	for _, p := range validParams {
+		if err := validator.Validate(openrtb_ext.BidderSharethrough, json.RawMessage(p)); err != nil {
+			t.Errorf("Schema rejected valid params: %s", p)
 		}
 	}
 }
@@ -23,21 +23,24 @@ func TestValidParams(t *testing.T) {
 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)
+		t.Fatalf("Failed to fetch the JSON schema. %v", err)
 	}
 
-	for _, invalidParam := range invalidParams {
-		if err := validator.Validate(openrtb_ext.BidderSharethrough, json.RawMessage(invalidParam)); err == nil {
-			t.Errorf("Schema allowed unexpected params: %s", invalidParam)
+	for _, p := range invalidParams {
+		if err := validator.Validate(openrtb_ext.BidderSharethrough, json.RawMessage(p)); err == nil {
+			t.Errorf("Schema allowed invalid params: %s", p)
 		}
 	}
 }
 
 var validParams = []string{
 	`{"pkey": "123"}`,
-	`{"pkey": "123", "iframe": true}`,
-	`{"pkey": "abc", "iframe": false}`,
-	`{"pkey": "abc123", "iframe": true, "iframeSize": [20, 20]}`,
+	`{"pkey": "123", "bcat": []}`,
+	`{"pkey": "123", "bcat": ["IAB-1"]}`,
+	`{"pkey": "abc", "badv": []}`,
+	`{"pkey": "abc", "badv": ["advertiser.com"]}`,
+	`{"pkey": "abc123", "bcat": [], "badv": []}`,
+	`{"pkey": "abc123", "bcat": ["IAB-1", "IAB-2"], "badv": ["other.advertiser.com"]}`,
 }
 
 var invalidParams = []string{
@@ -49,10 +52,10 @@ var invalidParams = []string{
 	`[]`,
 	`{}`,
 	`{"pkey": 123}`,
-	`{"iframe": 123}`,
-	`{"iframeSize": [20, 20]}`,
-	`{"pkey": 123, "iframe": 123}`,
-	`{"pkey": 123, "iframe": true, "iframeSize": [20]}`,
-	`{"pkey": 123, "iframe": true, "iframeSize": []}`,
-	`{"pkey": 123, "iframe": true, "iframeSize": 123}`,
+	`{"bcat": 123}`,
+	`{"badv": 123}`,
+	`{"bcat": ["IAB-1", "IAB-2"]}`,
+	`{"badv": ["other.advertiser.com"]}`,
+	`{"bcat": ["IAB-1", "IAB-2"], "badv": ["other.advertiser.com"]}`,
+	`{"pkey": 123, "bcat": ["IAB-1", "IAB-2"], "badv": ["other.advertiser.com"]}`,
 }
diff --git a/adapters/sharethrough/sharethrough_test.go b/adapters/sharethrough/sharethrough_test.go
index 1cf45d1f..d1ecadc4 100644
--- a/adapters/sharethrough/sharethrough_test.go
+++ b/adapters/sharethrough/sharethrough_test.go
@@ -1,278 +1,22 @@
 package sharethrough
 
 import (
-	"fmt"
-	"net/http"
-	"regexp"
-	"testing"
-
-	"github.com/mxmCherry/openrtb/v15/openrtb2"
-	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/adapters/adapterstest"
 	"github.com/prebid/prebid-server/config"
-	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/openrtb_ext"
-	"github.com/stretchr/testify/assert"
+	"testing"
 )
 
-type MockStrAdServer struct {
-	mockRequestFromOpenRTB func() (*adapters.RequestData, error)
-	mockResponseToOpenRTB  func() (*adapters.BidderResponse, []error)
-
-	StrOpenRTBInterface
-}
-
-func (m MockStrAdServer) requestFromOpenRTB(imp openrtb2.Imp, request *openrtb2.BidRequest, domain string) (*adapters.RequestData, error) {
-	return m.mockRequestFromOpenRTB()
-}
-
-func (m MockStrAdServer) responseToOpenRTB(strRawResp []byte, btlrReq *adapters.RequestData) (*adapters.BidderResponse, []error) {
-	return m.mockResponseToOpenRTB()
-}
-
-type MockStrUriHelper struct {
-	mockBuildUri func() string
-	mockParseUri func() (*StrAdSeverParams, error)
-
-	StrAdServerUriInterface
-}
-
-func (m MockStrUriHelper) buildUri(params StrAdSeverParams) string {
-	return m.mockBuildUri()
-}
-
-func (m MockStrUriHelper) parseUri(uri string) (*StrAdSeverParams, error) {
-	return m.mockParseUri()
-}
-
-func TestNewSharethroughBidder(t *testing.T) {
-	tests := map[string]struct {
-		input  config.Adapter
-		output SharethroughAdapter
-	}{
-		"Creates Sharethrough adapter": {
-			input: config.Adapter{Endpoint: "test endpoint"},
-			output: SharethroughAdapter{
-				AdServer: StrOpenRTBTranslator{
-					UriHelper: StrUriHelper{BaseURI: "test endpoint", Clock: Clock{}},
-					Util:      Util{Clock: Clock{}},
-					UserAgentParsers: UserAgentParsers{
-						ChromeVersion:    regexp.MustCompile(`Chrome\/(?P<ChromeVersion>\d+)`),
-						ChromeiOSVersion: regexp.MustCompile(`CriOS\/(?P<chromeiOSVersion>\d+)`),
-						SafariVersion:    regexp.MustCompile(`Version\/(?P<safariVersion>\d+)`),
-					},
-				},
-			},
-		},
-	}
-
-	assert := assert.New(t)
-	for testName, test := range tests {
-		t.Logf("Test case: %s\n", testName)
-
-		bidder, buildErr := Builder(openrtb_ext.BidderSharethrough, test.input)
-
-		assert.NoError(buildErr)
-		assert.Equal(bidder, &test.output)
-	}
-}
-
-func TestSuccessMakeRequests(t *testing.T) {
-	stubReq := &adapters.RequestData{
-		Method: "POST",
-		Uri:    "http://test.com",
-		Body:   nil,
-		Headers: http.Header{
-			"Content-Type": []string{"text/plain;charset=utf-8"},
-			"Accept":       []string{"application/json"},
-		},
-	}
-
-	tests := map[string]struct {
-		input    *openrtb2.BidRequest
-		expected []*adapters.RequestData
-	}{
-		"Generates expected Request": {
-			input: &openrtb2.BidRequest{
-				Site: &openrtb2.Site{
-					Page: "test.com",
-				},
-				Device: &openrtb2.Device{
-					UA: "Android Chome/60",
-				},
-				Imp: []openrtb2.Imp{{
-					ID:  "abc",
-					Ext: []byte(`{"pkey": "pkey", "iframe": true, "iframeSize": [10, 20]}`),
-					Banner: &openrtb2.Banner{
-						Format: []openrtb2.Format{{H: 30, W: 40}},
-					},
-				}},
-			},
-			expected: []*adapters.RequestData{stubReq},
-		},
-	}
-
-	mockAdServer := MockStrAdServer{
-		mockRequestFromOpenRTB: func() (*adapters.RequestData, error) {
-			return stubReq, nil
-		},
-	}
-
-	adapter := SharethroughAdapter{AdServer: mockAdServer}
-	for testName, test := range tests {
-		t.Logf("Test case: %s\n", testName)
-
-		output, actualErrors := adapter.MakeRequests(test.input, &adapters.ExtraRequestInfo{})
-
-		if len(output) != 1 {
-			t.Errorf("Expected one request in result, got %d\n", len(output))
-			return
-		}
-
-		assertRequestDataEquals(t, testName, test.expected[0], output[0])
-		if len(actualErrors) != 0 {
-			t.Errorf("Expected no errors, got %d\n", len(actualErrors))
-		}
-	}
-}
-
-func TestFailureMakeRequests(t *testing.T) {
-	tests := map[string]struct {
-		input    *openrtb2.BidRequest
-		expected string
-	}{
-		"Returns nil if failed to generate request": {
-			input: &openrtb2.BidRequest{
-				Site: &openrtb2.Site{
-					Page: "test.com",
-				},
-				Device: &openrtb2.Device{
-					UA: "Android Chome/60",
-				},
-				Imp: []openrtb2.Imp{{
-					ID:  "abc",
-					Ext: []byte(`{"pkey": "pkey", "iframe": true, "iframeSize": [10, 20]}`),
-					Banner: &openrtb2.Banner{
-						Format: []openrtb2.Format{{H: 30, W: 40}},
-					},
-				}},
-			},
-			expected: "error generating request",
-		},
-	}
-
-	mockAdServer := MockStrAdServer{
-		mockRequestFromOpenRTB: func() (*adapters.RequestData, error) {
-			return nil, fmt.Errorf("error generating request")
-		},
-	}
-
-	adapter := SharethroughAdapter{AdServer: mockAdServer}
-	for testName, test := range tests {
-		t.Logf("Test case: %s\n", testName)
-
-		output, actualErrors := adapter.MakeRequests(test.input, &adapters.ExtraRequestInfo{})
-
-		if output != nil {
-			t.Errorf("Expected result to be nil, got %d elements\n", len(output))
-		}
-		if len(actualErrors) != 1 {
-			t.Errorf("Expected one error, got %d\n", len(actualErrors))
-		}
-		if actualErrors[0].Error() != test.expected {
-			t.Errorf("Error mismatch: expected '%s' got '%s'\n", test.expected, actualErrors[0].Error())
-		}
-	}
-}
-
-func TestSuccessMakeBids(t *testing.T) {
-	stubBidderResponse := adapters.BidderResponse{}
-
-	tests := map[string]struct {
-		inputResponse *adapters.ResponseData
-		expected      *adapters.BidderResponse
-	}{
-		"Returns nil,nil if ad server responded with no content": {
-			inputResponse: &adapters.ResponseData{
-				StatusCode: http.StatusNoContent,
-			},
-			expected: nil,
-		},
-		"Generates response if ad server responded with 200": {
-			inputResponse: &adapters.ResponseData{
-				StatusCode: http.StatusOK,
-				Body:       []byte(`{}`),
-			},
-			expected: &stubBidderResponse,
-		},
-	}
-
-	mockAdServer := MockStrAdServer{
-		mockResponseToOpenRTB: func() (*adapters.BidderResponse, []error) {
-			return &stubBidderResponse, []error{}
-		},
-	}
+func TestJsonSamples(t *testing.T) {
+	adapterVersion = "10.0"
 
-	adapter := SharethroughAdapter{AdServer: mockAdServer}
-	for testName, test := range tests {
-		t.Logf("Test case: %s\n", testName)
+	bidder, buildErr := Builder(openrtb_ext.BidderSharethrough, config.Adapter{
+		Endpoint: "http://whatever.url",
+	})
 
-		response, errors := adapter.MakeBids(&openrtb2.BidRequest{}, &adapters.RequestData{}, test.inputResponse)
-		if len(errors) > 0 {
-			t.Errorf("Expected no errors, got %d\n", len(errors))
-		}
-		if response != test.expected {
-			t.Errorf("Response mismatch: expected '%+v' got '%+v'\n", test.expected, response)
-		}
-	}
-}
-
-func TestFailureMakeBids(t *testing.T) {
-	tests := map[string]struct {
-		inputResponse *adapters.ResponseData
-		expected      []error
-	}{
-		"Returns BadInput error if ad server responds with BadRequest": {
-			inputResponse: &adapters.ResponseData{
-				StatusCode: http.StatusBadRequest,
-			},
-			expected: []error{&errortypes.BadInput{
-				Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", http.StatusBadRequest),
-			}},
-		},
-		"Returns default error if ad server does not respond with Status OK": {
-			inputResponse: &adapters.ResponseData{
-				StatusCode: http.StatusInternalServerError,
-			},
-			expected: []error{fmt.Errorf("unexpected status code: %d. Run with request.debug = 1 for more info", http.StatusInternalServerError)},
-		},
-		"Passes by errors from responseToOpenRTB": {
-			inputResponse: &adapters.ResponseData{
-				StatusCode: http.StatusOK,
-				Body:       []byte(`{}`),
-			},
-			expected: []error{fmt.Errorf("failed in responseToOpenRTB")},
-		},
+	if buildErr != nil {
+		t.Fatalf("Builder returned unexpected error %v", buildErr)
 	}
 
-	mockAdServer := MockStrAdServer{
-		mockResponseToOpenRTB: func() (*adapters.BidderResponse, []error) {
-			return nil, []error{fmt.Errorf("failed in responseToOpenRTB")}
-		},
-	}
-
-	adapter := SharethroughAdapter{AdServer: mockAdServer}
-	for testName, test := range tests {
-		t.Logf("Test case: %s\n", testName)
-
-		response, errors := adapter.MakeBids(&openrtb2.BidRequest{}, &adapters.RequestData{}, test.inputResponse)
-		if response != nil {
-			t.Errorf("Expected response to be nil, got %+v\n", response)
-		}
-		if len(errors) != 1 {
-			t.Errorf("Expected no errors, got %d\n", len(errors))
-		}
-		if errors[0].Error() != test.expected[0].Error() {
-			t.Errorf("Error mismatch: expected '%s' got '%s'\n", test.expected[0].Error(), errors[0].Error())
-		}
-	}
+	adapterstest.RunJSONBidderTest(t, "sharethroughtest", bidder)
 }
diff --git a/adapters/sharethrough/sharethroughtest/exemplary/app-banner.json b/adapters/sharethrough/sharethroughtest/exemplary/app-banner.json
new file mode 100644
index 00000000..746c72f7
--- /dev/null
+++ b/adapters/sharethrough/sharethroughtest/exemplary/app-banner.json
@@ -0,0 +1,131 @@
+{
+  "mockBidRequest": {
+    "id": "web-banner",
+    "tmax": 3000,
+    "imp": [
+      {
+        "id": "banner-imp-id",
+        "ext": {
+          "bidder": {
+            "pkey": "pkey"
+          }
+        },
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        }
+      }
+    ],
+    "app": {
+      "bundle": "com.fake.app",
+      "publisher": {
+        "id": "42",
+        "name": "whatever.pub"
+      }
+    },
+    "device": {
+      "w": 1200,
+      "h": 900
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://whatever.url",
+        "headers": {
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "Accept": [
+            "application/json"
+          ]
+        },
+        "body": {
+          "id": "web-banner",
+          "tmax": 3000,
+          "imp": [
+            {
+              "id": "banner-imp-id",
+              "tagid": "pkey",
+              "ext": {
+                "bidder": {
+                  "pkey": "pkey"
+                }
+              },
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ]
+              }
+            }
+          ],
+          "device": {
+            "w": 1200,
+            "h": 900
+          },
+          "app": {
+            "bundle": "com.fake.app",
+            "publisher": {
+              "id": "42",
+              "name": "whatever.pub"
+            }
+          },
+          "source": {
+            "ext": {
+              "version": "",
+              "str": "10.0"
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "web-banner",
+          "cur": "USD",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "web-banner",
+                  "impid": "banner-imp-id",
+                  "crid": "some-creative-id",
+                  "adm": "<div>Ad</div>",
+                  "price": 20,
+                  "w": 300,
+                  "h": 250
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "web-banner",
+            "impid": "banner-imp-id",
+            "crid": "some-creative-id",
+            "adm": "<div>Ad</div>",
+            "price": 20,
+            "w": 300,
+            "h": 250
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/sharethrough/sharethroughtest/exemplary/app-video.json b/adapters/sharethrough/sharethroughtest/exemplary/app-video.json
new file mode 100644
index 00000000..240b94aa
--- /dev/null
+++ b/adapters/sharethrough/sharethroughtest/exemplary/app-video.json
@@ -0,0 +1,153 @@
+{
+  "mockBidRequest": {
+    "id": "web-video",
+    "tmax": 3000,
+    "imp": [
+      {
+        "id": "video-imp-id",
+        "ext": {
+          "data": {
+            "pbadslot": "video1"
+          },
+          "bidder": {
+            "pkey": "pkey",
+            "badv": [
+              "ford.com"
+            ],
+            "bcat": [
+              "IAB-1",
+              "IAB-2"
+            ]
+          }
+        },
+        "video": {
+          "w": 640,
+          "h": 480,
+          "mimes": [
+            "video/mp4"
+          ],
+          "placement": 1
+        }
+      }
+    ],
+    "app": {
+      "bundle": "com.fake.app",
+      "publisher": {
+        "id": "42",
+        "name": "whatever.pub"
+      }
+    },
+    "device": {
+      "w": 1200,
+      "h": 900
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://whatever.url",
+        "headers": {
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "Accept": [
+            "application/json"
+          ]
+        },
+        "body": {
+          "id": "web-video",
+          "tmax": 3000,
+          "imp": [
+            {
+              "id": "video-imp-id",
+              "tagid": "pkey",
+              "ext": {
+                "data": {
+                  "pbadslot": "video1"
+                },
+                "bidder": {
+                  "pkey": "pkey",
+                  "badv": [
+                    "ford.com"
+                  ],
+                  "bcat": [
+                    "IAB-1",
+                    "IAB-2"
+                  ]
+                }
+              },
+              "video": {
+                "w": 640,
+                "h": 480,
+                "mimes": [
+                  "video/mp4"
+                ],
+                "placement": 1
+              }
+            }
+          ],
+          "app": {
+            "bundle": "com.fake.app",
+            "publisher": {
+              "id": "42",
+              "name": "whatever.pub"
+            }
+          },
+          "device": {
+            "w": 1200,
+            "h": 900
+          },
+          "source": {
+            "ext": {
+              "version": "",
+              "str": "10.0"
+            }
+          },
+          "badv": ["ford.com"],
+          "bcat": ["IAB-1", "IAB-2"]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "web-video",
+          "cur": "USD",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "web-video",
+                  "impid": "video-imp-id",
+                  "crid": "some-creative-id",
+                  "adm": "<VAST>TAG</VAST>",
+                  "price": 20,
+                  "w": 640,
+                  "h": 480
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "web-video",
+            "impid": "video-imp-id",
+            "crid": "some-creative-id",
+            "adm": "<VAST>TAG</VAST>",
+            "price": 20,
+            "w": 640,
+            "h": 480
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/sharethrough/sharethroughtest/exemplary/web-banner.json b/adapters/sharethrough/sharethroughtest/exemplary/web-banner.json
new file mode 100644
index 00000000..b3574965
--- /dev/null
+++ b/adapters/sharethrough/sharethroughtest/exemplary/web-banner.json
@@ -0,0 +1,132 @@
+{
+  "mockBidRequest": {
+    "id": "web-banner",
+    "tmax": 3000,
+    "imp": [
+      {
+        "id": "baner-imp-id",
+        "ext": {
+          "bidder": {
+            "pkey": "pkey"
+          }
+        },
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        }
+      }
+    ],
+    "test": 0,
+    "site": {
+      "publisher": {
+        "id": "1"
+      },
+      "page": "https://some-site.com",
+      "ref": "https://some-site.com"
+    },
+    "device": {
+      "w": 1200,
+      "h": 900
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://whatever.url",
+        "headers": {
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "Accept": [
+            "application/json"
+          ]
+        },
+        "body": {
+          "id": "web-banner",
+          "tmax": 3000,
+          "imp": [
+            {
+              "id": "baner-imp-id",
+              "tagid": "pkey",
+              "ext": {
+                "bidder": {
+                  "pkey": "pkey"
+                }
+              },
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ]
+              }
+            }
+          ],
+          "site": {
+            "publisher": {
+              "id": "1"
+            },
+            "page": "https://some-site.com",
+            "ref": "https://some-site.com"
+          },
+          "device": {
+            "w": 1200,
+            "h": 900
+          },
+          "source": {
+            "ext": {
+              "version": "",
+              "str": "10.0"
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "web-banner",
+          "cur": "USD",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "web-banner",
+                  "impid": "baner-imp-id",
+                  "crid": "some-creative-id",
+                  "adm": "<div>Ad</div>",
+                  "price": 20,
+                  "w": 300,
+                  "h": 250
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "web-banner",
+            "impid": "baner-imp-id",
+            "crid": "some-creative-id",
+            "adm": "<div>Ad</div>",
+            "price": 20,
+            "w": 300,
+            "h": 250
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/sharethrough/sharethroughtest/exemplary/web-video.json b/adapters/sharethrough/sharethroughtest/exemplary/web-video.json
new file mode 100644
index 00000000..02de1780
--- /dev/null
+++ b/adapters/sharethrough/sharethroughtest/exemplary/web-video.json
@@ -0,0 +1,154 @@
+{
+  "mockBidRequest": {
+    "id": "web-video",
+    "tmax": 3000,
+    "imp": [
+      {
+        "id": "video-imp-id",
+        "ext": {
+          "data": {
+            "pbadslot": "video1"
+          },
+          "bidder": {
+            "pkey": "pkey",
+            "badv": [
+              "ford.com"
+            ],
+            "bcat": [
+              "IAB-1",
+              "IAB-2"
+            ]
+          }
+        },
+        "video": {
+          "w": 640,
+          "h": 480,
+          "mimes": [
+            "video/mp4"
+          ],
+          "placement": 1
+        }
+      }
+    ],
+    "test": 0,
+    "site": {
+      "publisher": {
+        "id": "1"
+      },
+      "page": "https://some-site.com",
+      "ref": "https://some-site.com"
+    },
+    "device": {
+      "w": 1200,
+      "h": 900
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://whatever.url",
+        "headers": {
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "Accept": [
+            "application/json"
+          ]
+        },
+        "body": {
+          "id": "web-video",
+          "tmax": 3000,
+          "imp": [
+            {
+              "id": "video-imp-id",
+              "tagid": "pkey",
+              "ext": {
+                "data": {
+                  "pbadslot": "video1"
+                },
+                "bidder": {
+                  "pkey": "pkey",
+                  "badv": [
+                    "ford.com"
+                  ],
+                  "bcat": [
+                    "IAB-1",
+                    "IAB-2"
+                  ]
+                }
+              },
+              "video": {
+                "w": 640,
+                "h": 480,
+                "mimes": [
+                  "video/mp4"
+                ],
+                "placement": 1
+              }
+            }
+          ],
+          "site": {
+            "publisher": {
+              "id": "1"
+            },
+            "page": "https://some-site.com",
+            "ref": "https://some-site.com"
+          },
+          "device": {
+            "w": 1200,
+            "h": 900
+          },
+          "source": {
+            "ext": {
+              "version": "",
+              "str": "10.0"
+            }
+          },
+          "badv": ["ford.com"],
+          "bcat": ["IAB-1", "IAB-2"]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "web-video",
+          "cur": "USD",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "web-video",
+                  "impid": "video-imp-id",
+                  "crid": "some-creative-id",
+                  "adm": "<VAST>TAG</VAST>",
+                  "price": 20,
+                  "w": 640,
+                  "h": 480
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "web-video",
+            "impid": "video-imp-id",
+            "crid": "some-creative-id",
+            "adm": "<VAST>TAG</VAST>",
+            "price": 20,
+            "w": 640,
+            "h": 480
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/sharethrough/sharethroughtest/supplemental/error-bad-request.json b/adapters/sharethrough/sharethroughtest/supplemental/error-bad-request.json
new file mode 100644
index 00000000..82908095
--- /dev/null
+++ b/adapters/sharethrough/sharethroughtest/supplemental/error-bad-request.json
@@ -0,0 +1,102 @@
+{
+  "mockBidRequest": {
+    "id": "web-banner",
+    "tmax": 3000,
+    "imp": [
+      {
+        "id": "banner-imp-id",
+        "ext": {
+          "bidder": {
+            "pkey": "pkey"
+          }
+        },
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        }
+      }
+    ],
+    "app": {
+      "bundle": "com.fake.app",
+      "publisher": {
+        "id": "42",
+        "name": "whatever.pub"
+      }
+    },
+    "device": {
+      "w": 1200,
+      "h": 900
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://whatever.url",
+        "headers": {
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "Accept": [
+            "application/json"
+          ]
+        },
+        "body": {
+          "id": "web-banner",
+          "tmax": 3000,
+          "imp": [
+            {
+              "id": "banner-imp-id",
+              "tagid": "pkey",
+              "ext": {
+                "bidder": {
+                  "pkey": "pkey"
+                }
+              },
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ]
+              }
+            }
+          ],
+          "device": {
+            "w": 1200,
+            "h": 900
+          },
+          "app": {
+            "bundle": "com.fake.app",
+            "publisher": {
+              "id": "42",
+              "name": "whatever.pub"
+            }
+          },
+          "source": {
+            "ext": {
+              "version": "",
+              "str": "10.0"
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 400
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    null
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Unexpected status code: 400.",
+      "comparison": "regex"
+    }
+  ]
+}
diff --git a/adapters/sharethrough/sharethroughtest/supplemental/error-internal-server.json b/adapters/sharethrough/sharethroughtest/supplemental/error-internal-server.json
new file mode 100644
index 00000000..11cbc3a4
--- /dev/null
+++ b/adapters/sharethrough/sharethroughtest/supplemental/error-internal-server.json
@@ -0,0 +1,102 @@
+{
+  "mockBidRequest": {
+    "id": "web-banner",
+    "tmax": 3000,
+    "imp": [
+      {
+        "id": "banner-imp-id",
+        "ext": {
+          "bidder": {
+            "pkey": "pkey"
+          }
+        },
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        }
+      }
+    ],
+    "app": {
+      "bundle": "com.fake.app",
+      "publisher": {
+        "id": "42",
+        "name": "whatever.pub"
+      }
+    },
+    "device": {
+      "w": 1200,
+      "h": 900
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://whatever.url",
+        "headers": {
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "Accept": [
+            "application/json"
+          ]
+        },
+        "body": {
+          "id": "web-banner",
+          "tmax": 3000,
+          "imp": [
+            {
+              "id": "banner-imp-id",
+              "tagid": "pkey",
+              "ext": {
+                "bidder": {
+                  "pkey": "pkey"
+                }
+              },
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ]
+              }
+            }
+          ],
+          "device": {
+            "w": 1200,
+            "h": 900
+          },
+          "app": {
+            "bundle": "com.fake.app",
+            "publisher": {
+              "id": "42",
+              "name": "whatever.pub"
+            }
+          },
+          "source": {
+            "ext": {
+              "version": "",
+              "str": "10.0"
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 500
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    null
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "unexpected status code: 500.",
+      "comparison": "regex"
+    }
+  ]
+}
diff --git a/adapters/sharethrough/sharethroughtest/supplemental/multi-imp.json b/adapters/sharethrough/sharethroughtest/supplemental/multi-imp.json
new file mode 100644
index 00000000..e083c798
--- /dev/null
+++ b/adapters/sharethrough/sharethroughtest/supplemental/multi-imp.json
@@ -0,0 +1,239 @@
+{
+  "mockBidRequest": {
+    "id": "web-banner",
+    "tmax": 3000,
+    "imp": [
+      {
+        "id": "banner-imp-id-1",
+        "ext": {
+          "bidder": {
+            "pkey": "pkey1"
+          }
+        },
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        }
+      },
+      {
+        "id": "banner-imp-id-2",
+        "ext": {
+          "bidder": {
+            "pkey": "pkey2"
+          }
+        },
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        }
+      }
+    ],
+    "site": {
+      "publisher": {
+        "id": "1"
+      },
+      "page": "https://some-site.com",
+      "ref": "https://some-site.com"
+    },
+    "device": {
+      "w": 1200,
+      "h": 900
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://whatever.url",
+        "headers": {
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "Accept": [
+            "application/json"
+          ]
+        },
+        "body": {
+          "id": "web-banner",
+          "tmax": 3000,
+          "imp": [
+            {
+              "id": "banner-imp-id-1",
+              "tagid": "pkey1",
+              "ext": {
+                "bidder": {
+                  "pkey": "pkey1"
+                }
+              },
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ]
+              }
+            }
+          ],
+          "site": {
+            "publisher": {
+              "id": "1"
+            },
+            "page": "https://some-site.com",
+            "ref": "https://some-site.com"
+          },
+          "device": {
+            "w": 1200,
+            "h": 900
+          },
+          "source": {
+            "ext": {
+              "version": "",
+              "str": "10.0"
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "web-banner",
+          "cur": "USD",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "web-banner",
+                  "impid": "banner-imp-id-1",
+                  "crid": "some-creative-id",
+                  "adm": "<div>Ad</div>",
+                  "price": 20,
+                  "w": 300,
+                  "h": 250
+                }
+              ]
+            }
+          ]
+        }
+      }
+    },
+    {
+      "expectedRequest": {
+        "uri": "http://whatever.url",
+        "headers": {
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "Accept": [
+            "application/json"
+          ]
+        },
+        "body": {
+          "id": "web-banner",
+          "tmax": 3000,
+          "imp": [
+            {
+              "id": "banner-imp-id-2",
+              "tagid": "pkey2",
+              "ext": {
+                "bidder": {
+                  "pkey": "pkey2"
+                }
+              },
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ]
+              }
+            }
+          ],
+          "site": {
+            "publisher": {
+              "id": "1"
+            },
+            "page": "https://some-site.com",
+            "ref": "https://some-site.com"
+          },
+          "device": {
+            "w": 1200,
+            "h": 900
+          },
+          "source": {
+            "ext": {
+              "version": "",
+              "str": "10.0"
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "web-banner",
+          "cur": "USD",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "web-banner",
+                  "impid": "banner-imp-id-2",
+                  "crid": "some-creative-id",
+                  "adm": "<div>Ad</div>",
+                  "price": 20,
+                  "w": 300,
+                  "h": 600
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "web-banner",
+            "impid": "banner-imp-id-1",
+            "crid": "some-creative-id",
+            "adm": "<div>Ad</div>",
+            "price": 20,
+            "w": 300,
+            "h": 250
+          },
+          "type": "banner"
+        }
+      ]
+    },
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "web-banner",
+            "impid": "banner-imp-id-2",
+            "crid": "some-creative-id",
+            "adm": "<div>Ad</div>",
+            "price": 20,
+            "w": 300,
+            "h": 600
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/sharethrough/sharethroughtest/supplemental/no-fill.json b/adapters/sharethrough/sharethroughtest/supplemental/no-fill.json
new file mode 100644
index 00000000..6d7dd939
--- /dev/null
+++ b/adapters/sharethrough/sharethroughtest/supplemental/no-fill.json
@@ -0,0 +1,97 @@
+{
+  "mockBidRequest": {
+    "id": "web-banner",
+    "tmax": 3000,
+    "imp": [
+      {
+        "id": "banner-imp-id",
+        "ext": {
+          "bidder": {
+            "pkey": "pkey"
+          }
+        },
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        }
+      }
+    ],
+    "app": {
+      "bundle": "com.fake.app",
+      "publisher": {
+        "id": "42",
+        "name": "whatever.pub"
+      }
+    },
+    "device": {
+      "w": 1200,
+      "h": 900
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://whatever.url",
+        "headers": {
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "Accept": [
+            "application/json"
+          ]
+        },
+        "body": {
+          "id": "web-banner",
+          "tmax": 3000,
+          "imp": [
+            {
+              "id": "banner-imp-id",
+              "tagid": "pkey",
+              "ext": {
+                "bidder": {
+                  "pkey": "pkey"
+                }
+              },
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ]
+              }
+            }
+          ],
+          "device": {
+            "w": 1200,
+            "h": 900
+          },
+          "app": {
+            "bundle": "com.fake.app",
+            "publisher": {
+              "id": "42",
+              "name": "whatever.pub"
+            }
+          },
+          "source": {
+            "ext": {
+              "version": "",
+              "str": "10.0"
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 204
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    null
+  ],
+  "expectedMakeBidsErrors": []
+}
diff --git a/adapters/sharethrough/sharethroughtest/supplemental/schain.json b/adapters/sharethrough/sharethroughtest/supplemental/schain.json
new file mode 100644
index 00000000..62b0e43c
--- /dev/null
+++ b/adapters/sharethrough/sharethroughtest/supplemental/schain.json
@@ -0,0 +1,185 @@
+{
+  "mockBidRequest": {
+    "id": "web-video-schain",
+    "tmax": 3000,
+    "source": {
+      "ext": {
+        "schain": {
+          "ver": "1.0",
+          "complete": 1,
+          "nodes": [
+            {
+              "asi": "indirectseller.com",
+              "sid": "00001",
+              "hp": 1
+            }
+          ]
+        }
+      }
+    },
+    "imp": [
+      {
+        "id": "video-imp-schain-id",
+        "ext": {
+          "data": {
+            "pbadslot": "video1-schain"
+          },
+          "bidder": {
+            "pkey": "pkey",
+            "badv": [
+              "ford.com"
+            ],
+            "bcat": [
+              "IAB-1",
+              "IAB-2"
+            ]
+          }
+        },
+        "video": {
+          "w": 640,
+          "h": 480,
+          "mimes": [
+            "video/mp4"
+          ],
+          "placement": 1
+        }
+      }
+    ],
+    "test": 0,
+    "site": {
+      "publisher": {
+        "id": "1"
+      },
+      "page": "https://some-site.com",
+      "ref": "https://some-site.com"
+    },
+    "device": {
+      "w": 1200,
+      "h": 900
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://whatever.url",
+        "headers": {
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "Accept": [
+            "application/json"
+          ]
+        },
+        "body": {
+          "id": "web-video-schain",
+          "tmax": 3000,
+          "imp": [
+            {
+              "id": "video-imp-schain-id",
+              "tagid": "pkey",
+              "ext": {
+                "data": {
+                  "pbadslot": "video1-schain"
+                },
+                "bidder": {
+                  "pkey": "pkey",
+                  "badv": [
+                    "ford.com"
+                  ],
+                  "bcat": [
+                    "IAB-1",
+                    "IAB-2"
+                  ]
+                }
+              },
+              "video": {
+                "w": 640,
+                "h": 480,
+                "mimes": [
+                  "video/mp4"
+                ],
+                "placement": 1
+              }
+            }
+          ],
+          "site": {
+            "publisher": {
+              "id": "1"
+            },
+            "page": "https://some-site.com",
+            "ref": "https://some-site.com"
+          },
+          "device": {
+            "w": 1200,
+            "h": 900
+          },
+          "source": {
+            "ext": {
+              "schain": {
+                "ver": "1.0",
+                "complete": 1,
+                "nodes": [
+                  {
+                    "asi": "indirectseller.com",
+                    "sid": "00001",
+                    "hp": 1
+                  }
+                ]
+              },
+              "version": "",
+              "str": "10.0"
+            }
+          },
+          "badv": [
+            "ford.com"
+          ],
+          "bcat": [
+            "IAB-1",
+            "IAB-2"
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "web-video-schain",
+          "cur": "USD",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "web-video-schain",
+                  "impid": "video-imp-schain-id",
+                  "crid": "some-creative-id",
+                  "adm": "<VAST>TAG</VAST>",
+                  "price": 20,
+                  "w": 640,
+                  "h": 480
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "web-video-schain",
+            "impid": "video-imp-schain-id",
+            "crid": "some-creative-id",
+            "adm": "<VAST>TAG</VAST>",
+            "price": 20,
+            "w": 640,
+            "h": 480
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/sharethrough/utils_test.go b/adapters/sharethrough/utils_test.go
deleted file mode 100644
index 70e97947..00000000
--- a/adapters/sharethrough/utils_test.go
+++ /dev/null
@@ -1,490 +0,0 @@
-package sharethrough
-
-import (
-	"encoding/base64"
-	"encoding/json"
-	"fmt"
-	"regexp"
-	"testing"
-
-	"github.com/mxmCherry/openrtb/v15/openrtb2"
-	"github.com/prebid/prebid-server/openrtb_ext"
-	"github.com/stretchr/testify/assert"
-)
-
-func TestGetAdMarkup(t *testing.T) {
-	tests := map[string]struct {
-		inputResponse   []byte
-		inputParams     *StrAdSeverParams
-		expectedSuccess []string
-		expectedError   error
-	}{
-		"Sets template variables": {
-			inputResponse: []byte(`{"bidId": "bid", "adserverRequestId": "arid"}`),
-			inputParams:   &StrAdSeverParams{Pkey: "pkey"},
-			expectedSuccess: []string{
-				`<img src="//b.sharethrough.com/butler?type=s2s-win&arid=arid&adReceivedAt=2019-09-12T11%3a29%3a00.000123456Z" />`,
-				`<div data-str-native-key="pkey" data-stx-response-name="str_response_bid"></div>`,
-				fmt.Sprintf(`<script>var str_response_bid = "%s"</script>`, base64.StdEncoding.EncodeToString([]byte(`{"bidId": "bid", "adserverRequestId": "arid"}`))),
-			},
-			expectedError: nil,
-		},
-		"Includes sfp.js without iFrame busting logic if iFrame param is true": {
-			inputResponse: []byte(`{"bidId": "bid", "adserverRequestId": "arid"}`),
-			inputParams:   &StrAdSeverParams{Pkey: "pkey", Iframe: true},
-			expectedSuccess: []string{
-				`<script src="//native.sharethrough.com/assets/sfp.js"></script>`,
-			},
-			expectedError: nil,
-		},
-		"Includes sfp.js with iFrame busting logic if iFrame param is false": {
-			inputResponse: []byte(`{"bidId": "bid", "adserverRequestId": "arid"}`),
-			inputParams:   &StrAdSeverParams{Pkey: "pkey", Iframe: false},
-			expectedSuccess: []string{
-				`<script src="//native.sharethrough.com/assets/sfp-set-targeting.js"></script>`,
-			},
-			expectedError: nil,
-		},
-		"Includes sfp.js with iFrame busting logic if iFrame param is not provided": {
-			inputResponse: []byte(`{"bidId": "bid", "adserverRequestId": "arid"}`),
-			inputParams:   &StrAdSeverParams{Pkey: "pkey"},
-			expectedSuccess: []string{
-				`<script src="//native.sharethrough.com/assets/sfp-set-targeting.js"></script>`,
-			},
-			expectedError: nil,
-		},
-	}
-
-	for testName, test := range tests {
-		t.Logf("Test case: %s\n", testName)
-		assert := assert.New(t)
-
-		var strResp openrtb_ext.ExtImpSharethroughResponse
-		_ = json.Unmarshal(test.inputResponse, &strResp)
-
-		outputSuccess, outputError := Util{Clock: MockClock{}}.getAdMarkup(test.inputResponse, strResp, test.inputParams)
-		for _, markup := range test.expectedSuccess {
-			assert.Contains(outputSuccess, markup)
-		}
-		assert.Equal(test.expectedError, outputError)
-	}
-}
-
-func TestGetPlacementSize(t *testing.T) {
-	tests := map[string]struct {
-		imp            openrtb2.Imp
-		strImpParams   openrtb_ext.ExtImpSharethrough
-		expectedHeight int64
-		expectedWidth  int64
-	}{
-		"Returns size from STR params if provided": {
-			imp:            openrtb2.Imp{},
-			strImpParams:   openrtb_ext.ExtImpSharethrough{IframeSize: []int{100, 200}},
-			expectedHeight: 100,
-			expectedWidth:  200,
-		},
-		"Skips size from STR params if malformed": {
-			imp:            openrtb2.Imp{},
-			strImpParams:   openrtb_ext.ExtImpSharethrough{IframeSize: []int{100}},
-			expectedHeight: 1,
-			expectedWidth:  1,
-		},
-		"Returns size from banner format if provided": {
-			imp:            openrtb2.Imp{Banner: &openrtb2.Banner{Format: []openrtb2.Format{{H: 100, W: 200}}}},
-			strImpParams:   openrtb_ext.ExtImpSharethrough{},
-			expectedHeight: 100,
-			expectedWidth:  200,
-		},
-		"Defaults to 1x1": {
-			imp:            openrtb2.Imp{},
-			strImpParams:   openrtb_ext.ExtImpSharethrough{},
-			expectedHeight: 1,
-			expectedWidth:  1,
-		},
-	}
-
-	for testName, test := range tests {
-		t.Logf("Test case: %s\n", testName)
-		assert := assert.New(t)
-
-		outputHeight, outputWidth := Util{}.getPlacementSize(test.imp, test.strImpParams)
-		assert.Equal(test.expectedHeight, outputHeight)
-		assert.Equal(test.expectedWidth, outputWidth)
-	}
-}
-
-func TestGetBestFormat(t *testing.T) {
-	tests := map[string]struct {
-		input          []openrtb2.Format
-		expectedHeight int64
-		expectedWidth  int64
-	}{
-		"Returns default size if empty input": {
-			input:          []openrtb2.Format{},
-			expectedHeight: 1,
-			expectedWidth:  1,
-		},
-		"Returns size if only one is passed": {
-			input:          []openrtb2.Format{{H: 100, W: 100}},
-			expectedHeight: 100,
-			expectedWidth:  100,
-		},
-		"Returns biggest size if multiple are passed": {
-			input:          []openrtb2.Format{{H: 100, W: 100}, {H: 200, W: 200}, {H: 50, W: 50}},
-			expectedHeight: 200,
-			expectedWidth:  200,
-		},
-	}
-
-	for testName, test := range tests {
-		t.Logf("Test case: %s\n", testName)
-		assert := assert.New(t)
-
-		outputHeight, outputWidth := Util{}.getBestFormat(test.input)
-		assert.Equal(test.expectedHeight, outputHeight)
-		assert.Equal(test.expectedWidth, outputWidth)
-	}
-}
-
-type userAgentTest struct {
-	input    string
-	expected bool
-}
-
-func runUserAgentTests(tests map[string]userAgentTest, fn func(string) bool, t *testing.T) {
-	for testName, test := range tests {
-		t.Logf("Test case: %s\n", testName)
-		assert := assert.New(t)
-
-		output := fn(test.input)
-		assert.Equal(test.expected, output)
-	}
-}
-
-func TestCanAutoPlayVideo(t *testing.T) {
-	uaParsers := UserAgentParsers{
-		ChromeVersion:    regexp.MustCompile(`Chrome\/(?P<ChromeVersion>\d+)`),
-		ChromeiOSVersion: regexp.MustCompile(`CriOS\/(?P<chromeiOSVersion>\d+)`),
-		SafariVersion:    regexp.MustCompile(`Version\/(?P<safariVersion>\d+)`),
-	}
-
-	ableAgents := map[string]string{
-		"Android at min Chrome version": "Android Chrome/60.0",
-		"iOS at min Chrome version":     "iPhone CriOS/60.0",
-		"iOS at min Safari version":     "iPad Version/14.0",
-		"Neither Android or iOS":        "Some User Agent",
-	}
-	unableAgents := map[string]string{
-		"Android not at min Chrome version": "Android Chrome/12",
-		"iOS not at min Chrome version":     "iPod Chrome/12",
-		"iOS not at min Safari version":     "iPod Version/8",
-	}
-
-	tests := map[string]userAgentTest{}
-	for testName, agent := range ableAgents {
-		tests[testName] = userAgentTest{
-			input:    agent,
-			expected: true,
-		}
-	}
-	for testName, agent := range unableAgents {
-		tests[testName] = userAgentTest{
-			input:    agent,
-			expected: false,
-		}
-	}
-
-	for testName, test := range tests {
-		t.Logf("Test case: %s\n", testName)
-		assert := assert.New(t)
-
-		output := Util{}.canAutoPlayVideo(test.input, uaParsers)
-		assert.Equal(test.expected, output)
-	}
-}
-
-func TestIsAndroid(t *testing.T) {
-	goodUserAgent := "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 6P Build/MMB29P)"
-	badUserAgent := "fake user agent"
-
-	// This is an alternate way to do testing if you have many test cases that only change the input and output
-	tests := map[string]userAgentTest{
-		"Match the Android user agent": {
-			input:    goodUserAgent,
-			expected: true,
-		},
-		"Does not match Android user agent": {
-			input:    badUserAgent,
-			expected: false,
-		},
-	}
-
-	runUserAgentTests(tests, Util{}.isAndroid, t)
-}
-
-func TestIsiOS(t *testing.T) {
-	iPhoneUserAgent := "Some string containing iPhone"
-	iPadUserAgent := "Some string containing iPad"
-	iPodUserAgent := "Some string containing iPOD"
-	badUserAgent := "Fake User Agent"
-
-	tests := map[string]userAgentTest{
-		"Match the iPhone user agent": {
-			input:    iPhoneUserAgent,
-			expected: true,
-		},
-		"Match the iPad user agent": {
-			input:    iPadUserAgent,
-			expected: true,
-		},
-		"Match the iPod user agent": {
-			input:    iPodUserAgent,
-			expected: true,
-		},
-		"Does not match Android user agent": {
-			input:    badUserAgent,
-			expected: false,
-		},
-	}
-
-	runUserAgentTests(tests, Util{}.isiOS, t)
-}
-
-func TestIsAtMinChromeVersion(t *testing.T) {
-	regex := regexp.MustCompile(`Chrome\/(?P<ChromeVersion>\d+)`)
-	v60ChromeUA := "Mozilla/5.0 Chrome/60.0.3112.113"
-	v12ChromeUA := "Mozilla/5.0 Chrome/12.0.3112.113"
-	badUA := "Fake User Agent"
-
-	tests := map[string]userAgentTest{
-		"Return true if greater than min (53)": {
-			input:    v60ChromeUA,
-			expected: true,
-		},
-		"Return false if lower than min (53)": {
-			input:    v12ChromeUA,
-			expected: false,
-		},
-		"Return false if no version found": {
-			input:    badUA,
-			expected: false,
-		},
-	}
-
-	for testName, test := range tests {
-		t.Logf("Test case: %s\n", testName)
-		assert := assert.New(t)
-
-		output := Util{}.isAtMinChromeVersion(test.input, regex)
-		assert.Equal(test.expected, output)
-	}
-}
-
-func TestIsAtMinChromeIosVersion(t *testing.T) {
-	regex := regexp.MustCompile(`CriOS\/(?P<chromeiOSVersion>\d+)`)
-	v60ChrIosUA := "Mozilla/5.0 CriOS/60.0.3112.113"
-	v12ChrIosUA := "Mozilla/5.0 CriOS/12.0.3112.113"
-	badUA := "Fake User Agent"
-
-	tests := map[string]userAgentTest{
-		"Return true if greater than min (53)": {
-			input:    v60ChrIosUA,
-			expected: true,
-		},
-		"Return false if lower than min (53)": {
-			input:    v12ChrIosUA,
-			expected: false,
-		},
-		"Return false if no version found": {
-			input:    badUA,
-			expected: false,
-		},
-	}
-
-	for testName, test := range tests {
-		t.Logf("Test case: %s\n", testName)
-		assert := assert.New(t)
-
-		output := Util{}.isAtMinChromeVersion(test.input, regex)
-		assert.Equal(test.expected, output)
-	}
-}
-
-func TestIsAtMinSafariVersion(t *testing.T) {
-	regex := regexp.MustCompile(`Version\/(?P<safariVersion>\d+)`)
-	v12SafariUA := "Mozilla/5.0 Version/12.0.3112.113"
-	v07SafariUA := "Mozilla/5.0 Version/07.0.3112.113"
-	badUA := "Fake User Agent"
-
-	tests := map[string]userAgentTest{
-		"Return true if greater than min (10)": {
-			input:    v12SafariUA,
-			expected: true,
-		},
-		"Return false if lower than min (10)": {
-			input:    v07SafariUA,
-			expected: false,
-		},
-		"Return false if no version found": {
-			input:    badUA,
-			expected: false,
-		},
-	}
-
-	for testName, test := range tests {
-		t.Logf("Test case: %s\n", testName)
-		assert := assert.New(t)
-
-		output := Util{}.isAtMinSafariVersion(test.input, regex)
-		assert.Equal(test.expected, output)
-	}
-}
-
-func TestGdprApplies(t *testing.T) {
-	bidRequestGdpr := openrtb2.BidRequest{
-		Regs: &openrtb2.Regs{
-			Ext: []byte(`{"gdpr": 1}`),
-		},
-	}
-	bidRequestNonGdpr := openrtb2.BidRequest{
-		Regs: &openrtb2.Regs{
-			Ext: []byte(`{"gdpr": 0}`),
-		},
-	}
-	bidRequestEmptyGdpr := openrtb2.BidRequest{
-		Regs: &openrtb2.Regs{
-			Ext: []byte(``),
-		},
-	}
-	bidRequestEmptyRegs := openrtb2.BidRequest{
-		Regs: &openrtb2.Regs{},
-	}
-
-	tests := map[string]struct {
-		input    *openrtb2.BidRequest
-		expected bool
-	}{
-		"Return true if gdpr set to 1": {
-			input:    &bidRequestGdpr,
-			expected: true,
-		},
-		"Return false if gdpr set to 0": {
-			input:    &bidRequestNonGdpr,
-			expected: false,
-		},
-		"Return false if no gdpr set": {
-			input:    &bidRequestEmptyGdpr,
-			expected: false,
-		},
-		"Return false if no Regs set": {
-			input:    &bidRequestEmptyRegs,
-			expected: false,
-		},
-	}
-
-	for testName, test := range tests {
-		t.Logf("Test case: %s\n", testName)
-		assert := assert.New(t)
-
-		output := Util{}.gdprApplies(test.input)
-		assert.Equal(test.expected, output)
-	}
-}
-
-func TestParseUserInfo(t *testing.T) {
-	tests := map[string]struct {
-		input    *openrtb2.User
-		expected userInfo
-	}{
-		"Return empty strings if no User": {
-			input:    nil,
-			expected: userInfo{Consent: "", TtdUid: "", StxUid: ""},
-		},
-		"Return empty strings if no uids": {
-			input:    &openrtb2.User{Ext: []byte(`{ "eids": [{"source": "adserver.org", "uids": []}] }`)},
-			expected: userInfo{Consent: "", TtdUid: "", StxUid: ""},
-		},
-		"Return empty strings if ID is not defined or empty string": {
-			input:    &openrtb2.User{Ext: []byte(`{ "eids": [{"source": "adserver.org", "uids": [{"id": null}]}, {"source": "adserver.org", "uids": [{"id": ""}]}] }`)},
-			expected: userInfo{Consent: "", TtdUid: "", StxUid: ""},
-		},
-		"Return consent correctly": {
-			input:    &openrtb2.User{Ext: []byte(`{ "consent": "abc" }`)},
-			expected: userInfo{Consent: "abc", TtdUid: "", StxUid: ""},
-		},
-		"Return ttd uid correctly": {
-			input:    &openrtb2.User{Ext: []byte(`{ "eids": [{"source": "adserver.org", "uids": [{"id": "abc123"}]}] }`)},
-			expected: userInfo{Consent: "", TtdUid: "abc123", StxUid: ""},
-		},
-		"Ignore non-trade-desk uid": {
-			input:    &openrtb2.User{Ext: []byte(`{ "eids": [{"source": "something", "uids": [{"id": "xyz"}]}] }`)},
-			expected: userInfo{Consent: "", TtdUid: "", StxUid: ""},
-		},
-		"Returns STX user id from buyer id": {
-			input:    &openrtb2.User{BuyerUID: "myid"},
-			expected: userInfo{Consent: "", TtdUid: "", StxUid: "myid"},
-		},
-		"Full test": {
-			input:    &openrtb2.User{BuyerUID: "myid", Ext: []byte(`{ "consent": "abc", "eids": [{"source": "something", "uids": [{"id": "xyz"}]}, {"source": "adserver.org", "uids": [{"id": "abc123"}]}] }`)},
-			expected: userInfo{Consent: "abc", TtdUid: "abc123", StxUid: "myid"},
-		},
-	}
-
-	for testName, test := range tests {
-		t.Logf("Test case: %s\n", testName)
-		assert := assert.New(t)
-
-		output := Util{}.parseUserInfo(test.input)
-		assert.Equal(test.expected.Consent, output.Consent)
-		assert.Equal(test.expected.TtdUid, output.TtdUid)
-		assert.Equal(test.expected.StxUid, output.StxUid)
-	}
-}
-
-func TestParseDomain(t *testing.T) {
-	tests := map[string]struct {
-		input    string
-		expected string
-	}{
-		"Parses domain without port": {
-			input:    "http://a.domain.com/page?param=value",
-			expected: "http://a.domain.com",
-		},
-		"Parses domain with port": {
-			input:    "https://a.domain.com:8000/page?param=value",
-			expected: "https://a.domain.com",
-		},
-		"Returns empty string if cannot parse the domain": {
-			input:    "abc",
-			expected: "",
-		},
-	}
-
-	for testName, test := range tests {
-		t.Logf("Test case: %s\n", testName)
-		assert := assert.New(t)
-
-		output := Util{}.parseDomain(test.input)
-		assert.Equal(test.expected, output)
-	}
-}
-
-func TestGetClock(t *testing.T) {
-	tests := map[string]struct {
-		input    Util
-		expected Clock
-	}{
-		"returns Clock from Utility": {
-			input:    Util{Clock: Clock{}},
-			expected: Clock{},
-		},
-	}
-
-	for testName, test := range tests {
-		t.Logf("Test case: %s\n", testName)
-		assert := assert.New(t)
-
-		output := test.input.getClock()
-		assert.Equal(test.expected, output)
-	}
-}
diff --git a/adapters/vidoomy/vidoomy_test.go b/adapters/vidoomy/vidoomy_test.go
index c9181395..11654a0c 100644
--- a/adapters/vidoomy/vidoomy_test.go
+++ b/adapters/vidoomy/vidoomy_test.go
@@ -3,7 +3,8 @@ package vidoomy
 import (
 	"testing"
 
-	"github.com/influxdata/influxdb/pkg/testing/assert"
+	"github.com/stretchr/testify/assert"
+
 	"github.com/prebid/prebid-server/adapters/adapterstest"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/openrtb_ext"
diff --git a/adapters/yahoossp/yahoossp_test.go b/adapters/yahoossp/yahoossp_test.go
index 214457c0..a7702f3f 100644
--- a/adapters/yahoossp/yahoossp_test.go
+++ b/adapters/yahoossp/yahoossp_test.go
@@ -3,7 +3,8 @@ package yahoossp
 import (
 	"testing"
 
-	"github.com/influxdata/influxdb/pkg/testing/assert"
+	"github.com/stretchr/testify/assert"
+
 	"github.com/prebid/prebid-server/adapters/adapterstest"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/openrtb_ext"
diff --git a/config/bidderinfo_validate_test.go b/config/bidderinfo_validate_test.go
index 6a1b18ab..c33c829f 100644
--- a/config/bidderinfo_validate_test.go
+++ b/config/bidderinfo_validate_test.go
@@ -9,11 +9,12 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
+	"gopkg.in/yaml.v3"
+
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/usersync"
-	"github.com/stretchr/testify/assert"
-	"gopkg.in/yaml.v2"
 )
 
 const bidderInfoRelativePath = "../static/bidder-info"
diff --git a/config/config_test.go b/config/config_test.go
index 766244c4..d06a9fd8 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -319,8 +319,10 @@ metrics:
   influxdb:
     host: upstream:8232
     database: metricsdb
+    measurement: anyMeasurement
     username: admin
     password: admin1324
+    align_timestamps: true
     metric_send_interval: 30
   disabled_metrics:
     account_adapter_details: true
@@ -557,8 +559,10 @@ func TestFullConfig(t *testing.T) {
 	cmpStrings(t, "recaptcha_secret", cfg.RecaptchaSecret, "asdfasdfasdfasdf")
 	cmpStrings(t, "metrics.influxdb.host", cfg.Metrics.Influxdb.Host, "upstream:8232")
 	cmpStrings(t, "metrics.influxdb.database", cfg.Metrics.Influxdb.Database, "metricsdb")
+	cmpStrings(t, "metrics.influxdb.measurement", cfg.Metrics.Influxdb.Measurement, "anyMeasurement")
 	cmpStrings(t, "metrics.influxdb.username", cfg.Metrics.Influxdb.Username, "admin")
 	cmpStrings(t, "metrics.influxdb.password", cfg.Metrics.Influxdb.Password, "admin1324")
+	cmpBools(t, "metrics.influxdb.align_timestamps", cfg.Metrics.Influxdb.AlignTimestamps, true)
 	cmpInts(t, "metrics.influxdb.metric_send_interval", cfg.Metrics.Influxdb.MetricSendInterval, 30)
 	cmpStrings(t, "", cfg.CacheURL.GetBaseURL(), "http://prebidcache.net")
 	cmpStrings(t, "", cfg.GetCachedAssetURL("a0eebc99-9c0b-4ef8-bb00-6bb9bd380a11"), "http://prebidcache.net/cache?uuid=a0eebc99-9c0b-4ef8-bb00-6bb9bd380a11")
diff --git a/config/events_test.go b/config/events_test.go
new file mode 100644
index 00000000..b4c196e9
--- /dev/null
+++ b/config/events_test.go
@@ -0,0 +1,329 @@
+package config
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestIsValidCreateElement(t *testing.T) {
+	testCases := []struct {
+		description   string
+		createElement VASTEventElement
+		valid         bool
+	}{
+		{
+			description:   "Empty create element",
+			createElement: "",
+			valid:         false,
+		},
+		{
+			description:   "Invalid create element",
+			createElement: "invalid_element",
+			valid:         false,
+		},
+		{
+			description:   "Valid create element",
+			createElement: "tracking",
+			valid:         true,
+		},
+		{
+			description:   "Case sensitivity of create element",
+			createElement: "Tracking",
+			valid:         false,
+		},
+	}
+	for _, test := range testCases {
+		isValid := test.createElement.isValid()
+		assert.Equal(t, test.valid, isValid, test.description)
+	}
+
+}
+
+func TestIsValidTrackingEventType(t *testing.T) {
+	testCases := []struct {
+		description string
+		vastEvent   VASTEvent
+		valid       bool
+	}{
+		{
+			description: "Empty type",
+			vastEvent:   VASTEvent{},
+			valid:       false,
+		},
+		{
+			description: "Empty type for tracking event",
+			vastEvent: VASTEvent{
+				CreateElement: TrackingVASTElement,
+			},
+			valid: false,
+		},
+		{
+			description: "Invalid type for tracking event",
+			vastEvent: VASTEvent{
+				CreateElement: TrackingVASTElement,
+				Type:          "invalid_type",
+			},
+			valid: false,
+		},
+		{
+			description: "Valid type for tracking event",
+			vastEvent: VASTEvent{
+				CreateElement: TrackingVASTElement,
+				Type:          MidPoint,
+			},
+			valid: true,
+		},
+		{
+			description: "Case sensitivity of type for tracking event",
+			vastEvent: VASTEvent{
+				CreateElement: TrackingVASTElement,
+				Type:          "COMplete",
+			},
+			valid: false,
+		},
+	}
+	for _, test := range testCases {
+		isValid := test.vastEvent.Type.isValid()
+		assert.Equal(t, test.valid, isValid, test.description)
+	}
+}
+
+func TestIsValidURL(t *testing.T) {
+	testCases := []struct {
+		description string
+		url         string
+		valid       bool
+	}{
+		{
+			description: "Empty Url",
+			url:         "",
+			valid:       false,
+		},
+		{
+			description: "Capital Domain name",
+			url:         "http://PREBID.ORG",
+			valid:       true,
+		},
+		{
+			description: "Url with Macros",
+			url:         "https://PBS_HOST/event?t=##PBS-EVENTTYPE##&vtype=##PBS-VASTEVENT##&b=##PBS-BIDID##&f=i&a=##PBS-ACCOUNTID##&ts=##PBS-TIMESTAMP##&bidder=##PBS-BIDDER##&int=##PBS-INTEGRATION##&mt=##PBS-MEDIATYPE##&ch=##PBS-CHANNEL##&aid=##PBS-AUCTIONID##&l=##PBS-LINEID##",
+			valid:       true,
+		},
+		{
+			description: "Invalid http syntax",
+			url:         "HTTP://PREBID.ORG",
+			valid:       false,
+		},
+		{
+			description: "Invalid protocol",
+			url:         "HTTPE://PREBID.ORG",
+			valid:       false,
+		},
+		{
+			description: "Double colon after domain name",
+			url:         "https://prebid.org::8000",
+			valid:       false,
+		},
+		{
+			description: "Url with Port",
+			url:         "https://prebid.org:8000",
+			valid:       true,
+		},
+		{
+			description: "Url with invalid Port",
+			url:         "https://prebid.org:100000",
+			valid:       false,
+		},
+	}
+	for _, test := range testCases {
+		isValid := isValidURL(test.url)
+		assert.Equal(t, test.valid, isValid, test.description)
+	}
+}
+
+func TestIsTrackingEvent(t *testing.T) {
+	testCases := []struct {
+		description string
+		vastEvent   VASTEvent
+		valid       bool
+	}{
+		{
+			description: "Empty Tracking Event",
+			vastEvent:   VASTEvent{},
+			valid:       false,
+		},
+		{
+			description: "Valid Tracking Event",
+			vastEvent: VASTEvent{
+				CreateElement: "tracking",
+			},
+			valid: true,
+		},
+		{
+			description: "Case Sensitivity in Tracking Event",
+			vastEvent: VASTEvent{
+				CreateElement: "Tracking",
+			},
+			valid: false,
+		},
+	}
+	for _, test := range testCases {
+		isValid := test.vastEvent.isTrackingEvent()
+		assert.Equal(t, test.valid, isValid, test.description)
+	}
+}
+
+func TestValidateVASTEvent(t *testing.T) {
+	testCases := []struct {
+		description string
+		vastEvent   VASTEvent
+		index       int
+		expectErr   bool
+	}{
+		{
+			description: "At least one URL - Default URL",
+			vastEvent: VASTEvent{
+				CreateElement:     "tracking",
+				Type:              "start",
+				ExcludeDefaultURL: false,
+				URLs:              nil,
+			},
+			index:     0,
+			expectErr: false,
+		},
+		{
+			description: "URLs has at least one URL, default URL is excluded",
+			vastEvent: VASTEvent{
+				CreateElement:     "impression",
+				ExcludeDefaultURL: true,
+				URLs:              []string{"http://mytracker.comm"},
+			},
+			index:     0,
+			expectErr: false,
+		},
+		{
+			description: "No URLs and default URL Excluded",
+			vastEvent: VASTEvent{
+				CreateElement:     "impression",
+				ExcludeDefaultURL: true,
+				URLs:              nil,
+			},
+			index:     0,
+			expectErr: true,
+		},
+		{
+			description: "Invalid Create Elemment",
+			vastEvent:   VASTEvent{},
+			index:       0,
+			expectErr:   true,
+		},
+		{
+			description: "Invalid Type",
+			vastEvent: VASTEvent{
+				CreateElement: "tracking",
+				Type:          "invalid_type",
+			},
+			index:     0,
+			expectErr: true,
+		},
+		{
+			description: "Invalid URLs",
+			vastEvent: VASTEvent{
+				CreateElement: "impression",
+				URLs:          []string{"http://url.com?k1=v1&k2=###PBS-MACRO##", "httpE://invalid.url"},
+			},
+			index:     0,
+			expectErr: true,
+		},
+		{
+			description: "Valid type but create element is other than tracking",
+			vastEvent: VASTEvent{
+				CreateElement: "impression",
+				Type:          "start",
+			},
+			index:     0,
+			expectErr: true,
+		},
+	}
+	for _, test := range testCases {
+		err := test.vastEvent.validate()
+		assert.Equal(t, !test.expectErr, err == nil, test.description)
+	}
+}
+
+func TestValidate(t *testing.T) {
+	testCases := []struct {
+		description string
+		events      Events
+		expectErr   bool
+	}{
+		{
+			description: "Empty default URL",
+			events: Events{
+				Enabled: true,
+			},
+			expectErr: true,
+		},
+		{
+			description: "Events are disabled. Skips validations",
+			events: Events{
+				Enabled:    false,
+				DefaultURL: "",
+			},
+			expectErr: false,
+		},
+		{
+			description: "No VAST Events and default URL present",
+			events: Events{
+				Enabled:    true,
+				DefaultURL: "http://prebid.org",
+			},
+			expectErr: false,
+		},
+		{
+			description: "Invalid VAST Event",
+			events: Events{
+				Enabled:    true,
+				DefaultURL: "http://prebid.org",
+				VASTEvents: []VASTEvent{
+					{},
+				},
+			},
+			expectErr: true,
+		},
+	}
+	for _, test := range testCases {
+		errs := test.events.validate(make([]error, 0))
+		assert.Equal(t, !test.expectErr, len(errs) == 0, test.description)
+	}
+}
+
+func TestValidateVASTEvents(t *testing.T) {
+	testCases := []struct {
+		description string
+		events      []VASTEvent
+		expectErr   bool
+	}{
+		{
+			description: "No Vast Events",
+			events:      nil,
+			expectErr:   false,
+		},
+		{
+
+			description: "Invalid Event Object",
+			events: []VASTEvent{
+				{
+					CreateElement: "impression",
+				},
+				{},
+			},
+			expectErr: true,
+		},
+	}
+	for _, test := range testCases {
+		err := validateVASTEvents(test.events)
+		assert.Equal(t, !test.expectErr, err == nil, test.description)
+	}
+}
diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go
index d266bc38..1e32727b 100644
--- a/endpoints/openrtb2/amp_auction_test.go
+++ b/endpoints/openrtb2/amp_auction_test.go
@@ -32,12 +32,17 @@ func TestGoodAmpRequests(t *testing.T) {
 	goodRequests := map[string]json.RawMessage{
 		"1": json.RawMessage(validRequest(t, "aliased-buyeruids.json")),
 		"2": json.RawMessage(validRequest(t, "aliases.json")),
+		"3": json.RawMessage(validRequest(t, "imp-with-stored-resp.json")),
 		"5": json.RawMessage(validRequest(t, "gdpr-no-consentstring.json")),
 		"6": json.RawMessage(validRequest(t, "gdpr.json")),
 		"7": json.RawMessage(validRequest(t, "site.json")),
 		"9": json.RawMessage(validRequest(t, "user.json")),
 	}
 
+	goodStoredResponses := map[string]json.RawMessage{
+		"6d718149": json.RawMessage(`[{"bid": [{"id": "bid_id", "ext": {"prebid": {"targeting": { "hb_pb": "1.20", "hb_cat": "IAB-20", "hb_cache_id": "some_id"}}}}],"seat": "appnexus"}]`),
+	}
+
 	endpoint, _ := NewAmpEndpoint(
 		fakeUUIDGenerator{},
 		&mockAmpExchange{},
@@ -50,6 +55,7 @@ func TestGoodAmpRequests(t *testing.T) {
 		map[string]string{},
 		[]byte{},
 		openrtb_ext.BuildBidderMap(),
+		&mockAmpStoredResponseFetcher{goodStoredResponses},
 	)
 
 	for requestID := range goodRequests {
@@ -104,6 +110,7 @@ func TestAMPPageInfo(t *testing.T) {
 		map[string]string{},
 		[]byte{},
 		openrtb_ext.BuildBidderMap(),
+		empty_fetcher.EmptyFetcher{},
 	)
 	request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=1&curl=%s", url.QueryEscape(page)), nil)
 	recorder := httptest.NewRecorder()
@@ -202,6 +209,7 @@ func TestGDPRConsent(t *testing.T) {
 			map[string]string{},
 			[]byte{},
 			openrtb_ext.BuildBidderMap(),
+			empty_fetcher.EmptyFetcher{},
 		)
 
 		// Invoke Endpoint
@@ -355,6 +363,7 @@ func TestCCPAConsent(t *testing.T) {
 			map[string]string{},
 			[]byte{},
 			openrtb_ext.BuildBidderMap(),
+			empty_fetcher.EmptyFetcher{},
 		)
 
 		// Invoke Endpoint
@@ -466,6 +475,7 @@ func TestConsentWarnings(t *testing.T) {
 			map[string]string{},
 			[]byte{},
 			openrtb_ext.BuildBidderMap(),
+			empty_fetcher.EmptyFetcher{},
 		)
 
 		// Invoke Endpoint
@@ -559,6 +569,7 @@ func TestNewAndLegacyConsentBothProvided(t *testing.T) {
 			map[string]string{},
 			[]byte{},
 			openrtb_ext.BuildBidderMap(),
+			empty_fetcher.EmptyFetcher{},
 		)
 
 		// Invoke Endpoint
@@ -611,6 +622,7 @@ func TestAMPSiteExt(t *testing.T) {
 		nil,
 		nil,
 		openrtb_ext.BuildBidderMap(),
+		empty_fetcher.EmptyFetcher{},
 	)
 	request, err := http.NewRequest("GET", "/openrtb2/auction/amp?tag_id=1", nil)
 	if !assert.NoError(t, err) {
@@ -648,6 +660,7 @@ func TestAmpBadRequests(t *testing.T) {
 		map[string]string{},
 		[]byte{},
 		openrtb_ext.BuildBidderMap(),
+		empty_fetcher.EmptyFetcher{},
 	)
 	for requestID := range badRequests {
 		request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=%s", requestID), nil)
@@ -679,6 +692,7 @@ func TestAmpDebug(t *testing.T) {
 		map[string]string{},
 		[]byte{},
 		openrtb_ext.BuildBidderMap(),
+		empty_fetcher.EmptyFetcher{},
 	)
 
 	for requestID := range requests {
@@ -752,6 +766,7 @@ func TestQueryParamOverrides(t *testing.T) {
 		map[string]string{},
 		[]byte{},
 		openrtb_ext.BuildBidderMap(),
+		empty_fetcher.EmptyFetcher{},
 	)
 
 	requestID := "1"
@@ -905,6 +920,7 @@ func (s formatOverrideSpec) execute(t *testing.T) {
 		map[string]string{},
 		[]byte{},
 		openrtb_ext.BuildBidderMap(),
+		empty_fetcher.EmptyFetcher{},
 	)
 
 	url := fmt.Sprintf("/openrtb2/auction/amp?tag_id=1&debug=1&w=%d&h=%d&ow=%d&oh=%d&ms=%s&account=%s", s.width, s.height, s.overrideWidth, s.overrideHeight, s.multisize, s.account)
@@ -947,6 +963,18 @@ func (cf *mockAmpStoredReqFetcher) FetchResponses(ctx context.Context, ids []str
 	return nil, nil
 }
 
+type mockAmpStoredResponseFetcher struct {
+	data map[string]json.RawMessage
+}
+
+func (cf *mockAmpStoredResponseFetcher) FetchRequests(ctx context.Context, requestIDs []string, impIDs []string) (requestData map[string]json.RawMessage, impData map[string]json.RawMessage, errs []error) {
+	return nil, nil, nil
+}
+
+func (cf *mockAmpStoredResponseFetcher) FetchResponses(ctx context.Context, ids []string) (data map[string]json.RawMessage, errs []error) {
+	return cf.data, nil
+}
+
 type mockAmpExchange struct {
 	lastRequest *openrtb2.BidRequest
 }
@@ -972,6 +1000,14 @@ func (m *mockAmpExchange) HoldAuction(ctx context.Context, r exchange.AuctionReq
 		}},
 		Ext: json.RawMessage(`{ "errors": {"openx":[ { "code": 1, "message": "The request exceeded the timeout allocated" } ] } }`),
 	}
+	if len(r.StoredAuctionResponses) > 0 {
+		var seatBids []openrtb2.SeatBid
+
+		if err := json.Unmarshal(r.StoredAuctionResponses[r.BidRequest.Imp[0].ID], &seatBids); err != nil {
+			return nil, err
+		}
+		response.SeatBid = seatBids
+	}
 
 	if r.BidRequest.Test == 1 {
 		resolvedRequest, err := json.Marshal(r.BidRequest)
@@ -1360,6 +1396,7 @@ func ampObjectTestSetup(t *testing.T, inTagId string, inStoredRequest json.RawMe
 		map[string]string{},
 		[]byte{},
 		openrtb_ext.BuildBidderMap(),
+		empty_fetcher.EmptyFetcher{},
 	)
 	return &actualAmpObject, endpoint
 }
@@ -1410,6 +1447,7 @@ func TestAmpAuctionResponseHeaders(t *testing.T) {
 		map[string]string{},
 		[]byte{},
 		openrtb_ext.BuildBidderMap(),
+		empty_fetcher.EmptyFetcher{},
 	)
 
 	for _, test := range testCases {
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index dd9183c3..190405db 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -1635,7 +1635,7 @@ func TestValidateRequest(t *testing.T) {
 	}
 
 	for _, test := range testCases {
-		errorList := deps.validateRequest(test.givenRequestWrapper, test.givenIsAmp)
+		errorList := deps.validateRequest(test.givenRequestWrapper, test.givenIsAmp, false)
 		assert.Equalf(t, test.expectedErrorList, errorList, "Error doesn't match: %s\n", test.description)
 
 		if len(errorList) == 0 {
@@ -2314,7 +2314,7 @@ func TestValidateImpExt(t *testing.T) {
 		for _, test := range group.testCases {
 			imp := &openrtb2.Imp{Ext: test.impExt}
 
-			errs := deps.validateImpExt(imp, nil, 0)
+			errs := deps.validateImpExt(imp, nil, 0, false)
 
 			if len(test.expectedImpExt) > 0 {
 				assert.JSONEq(t, test.expectedImpExt, string(imp.Ext), "imp.ext JSON does not match expected. Test: %s. %s\n", group.description, test.description)
@@ -2377,7 +2377,7 @@ func TestCurrencyTrunc(t *testing.T) {
 		Cur: []string{"USD", "EUR"},
 	}
 
-	errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false)
+	errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false, false)
 
 	expectedError := errortypes.Warning{Message: "A prebid request can only process one currency. Taking the first currency in the list, USD, as the active currency"}
 	assert.ElementsMatch(t, errL, []error{&expectedError})
@@ -2425,7 +2425,7 @@ func TestCCPAInvalid(t *testing.T) {
 		},
 	}
 
-	errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false)
+	errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false, false)
 
 	expectedWarning := errortypes.Warning{
 		Message:     "CCPA consent is invalid and will be ignored. (request.regs.ext.us_privacy must contain 4 characters)",
@@ -2476,7 +2476,7 @@ func TestNoSaleInvalid(t *testing.T) {
 		Ext: json.RawMessage(`{"prebid": {"nosale": ["*", "appnexus"]} }`),
 	}
 
-	errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false)
+	errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false, false)
 
 	expectedError := errors.New("request.ext.prebid.nosale is invalid: can only specify all bidders if no other bidders are provided")
 	assert.ElementsMatch(t, errL, []error{expectedError})
@@ -2525,7 +2525,7 @@ func TestValidateSourceTID(t *testing.T) {
 		},
 	}
 
-	deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false)
+	deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false, false)
 	assert.NotEmpty(t, req.Source.TID, "Expected req.Source.TID to be filled with a randomly generated UID")
 }
 
@@ -2569,7 +2569,7 @@ func TestSChainInvalid(t *testing.T) {
 		Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller1.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller2.com","sid":"00002","rid":"BidRequest2","hp":1}],"ver":"1.0"}}]}}`),
 	}
 
-	errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false)
+	errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false, false)
 
 	expectedError := errors.New("request.ext.prebid.schains contains multiple schains for bidder appnexus; it must contain no more than one per bidder.")
 	assert.ElementsMatch(t, errL, []error{expectedError})
@@ -2918,7 +2918,7 @@ func TestEidPermissionsInvalid(t *testing.T) {
 		Ext: json.RawMessage(`{"prebid": {"data": {"eidpermissions": [{"source":"a", "bidders":[]}]} } }`),
 	}
 
-	errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false)
+	errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false, false)
 
 	expectedError := errors.New(`request.ext.prebid.data.eidpermissions[0] missing or empty required field: "bidders"`)
 	assert.ElementsMatch(t, errL, []error{expectedError})
@@ -3224,7 +3224,7 @@ func TestParseRequestParseImpInfoError(t *testing.T) {
 
 	req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(reqBody))
 
-	resReq, impExtInfoMap, errL := deps.parseRequest(req)
+	resReq, impExtInfoMap, _, errL := deps.parseRequest(req)
 
 	assert.Nil(t, resReq, "Result request should be nil due to incorrect imp")
 	assert.Nil(t, impExtInfoMap, "Impression info map should be nil due to incorrect imp")
@@ -4735,7 +4735,7 @@ func TestParseRequestMergeBidderParams(t *testing.T) {
 
 			req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(tt.givenRequestBody))
 
-			resReq, _, errL := deps.parseRequest(req)
+			resReq, _, _, errL := deps.parseRequest(req)
 
 			var expIExt, iExt map[string]interface{}
 			err := json.Unmarshal(tt.expectedImpExt, &expIExt)
@@ -4761,6 +4761,332 @@ func TestParseRequestMergeBidderParams(t *testing.T) {
 	}
 }
 
+func TestParseRequestStoredResponses(t *testing.T) {
+	mockStoredResponses := map[string]json.RawMessage{
+		"6d718149": json.RawMessage(`[{"bid": [{"id": "bid_id1"],"seat": "appnexus"}]`),
+		"6d715835": json.RawMessage(`[{"bid": [{"id": "bid_id2"],"seat": "appnexus"}]`),
+	}
+
+	tests := []struct {
+		name                    string
+		givenRequestBody        string
+		expectedStoredResponses map[string]json.RawMessage
+		expectedErrorCount      int
+		expectedError           string
+	}{
+		{
+			name:             "reqimp has valid stored response",
+			givenRequestBody: validRequest(t, "req-imp-stored-response.json"),
+			expectedStoredResponses: map[string]json.RawMessage{
+				"imp-id1": json.RawMessage(`[{"bid": [{"id": "bid_id1"],"seat": "appnexus"}]`),
+			},
+			expectedErrorCount: 0,
+		},
+		{
+			name:             "req has two imps valid stored responses",
+			givenRequestBody: validRequest(t, "req-two-imps-stored-response.json"),
+			expectedStoredResponses: map[string]json.RawMessage{
+				"imp-id1": json.RawMessage(`[{"bid": [{"id": "bid_id1"],"seat": "appnexus"}]`),
+				"imp-id2": json.RawMessage(`[{"bid": [{"id": "bid_id2"],"seat": "appnexus"}]`),
+			},
+			expectedErrorCount: 0,
+		},
+		{
+			name:             "req has two imps with missing stored responses",
+			givenRequestBody: validRequest(t, "req-two-imps-missing-stored-response.json"),
+			expectedStoredResponses: map[string]json.RawMessage{
+				"imp-id1": json.RawMessage(`[{"bid": [{"id": "bid_id1"],"seat": "appnexus"}]`),
+				"imp-id2": json.RawMessage(nil),
+			},
+			expectedErrorCount: 0,
+		},
+		{
+			name:             "req has two imps: one with stored response and another imp without stored resp",
+			givenRequestBody: validRequest(t, "req-two-imps-one-stored-response.json"),
+			expectedStoredResponses: map[string]json.RawMessage{
+				"imp-id1": json.RawMessage(`[{"bid": [{"id": "bid_id1"],"seat": "appnexus"}]`),
+			},
+			expectedErrorCount: 1,
+			expectedError:      `request validation failed. The StoredAuctionResponse.ID field must be completely present with, or completely absent from, all impressions in request. No StoredAuctionResponse data found for request.imp[1].ext.prebid`,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+
+			deps := &endpointDeps{
+				fakeUUIDGenerator{},
+				&warningsCheckExchange{},
+				newParamsValidator(t),
+				&mockStoredReqFetcher{},
+				empty_fetcher.EmptyFetcher{},
+				empty_fetcher.EmptyFetcher{},
+				&config.Configuration{MaxRequestSize: int64(len(tt.givenRequestBody))},
+				&metricsConfig.NilMetricsEngine{},
+				analyticsConf.NewPBSAnalytics(&config.Analytics{}),
+				map[string]string{},
+				false,
+				[]byte{},
+				openrtb_ext.BuildBidderMap(),
+				nil,
+				nil,
+				hardcodedResponseIPValidator{response: true},
+				&mockStoredResponseFetcher{mockStoredResponses},
+			}
+
+			req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(tt.givenRequestBody))
+
+			_, _, storedResponses, errL := deps.parseRequest(req)
+
+			if tt.expectedErrorCount == 0 {
+				assert.Equal(t, tt.expectedStoredResponses, storedResponses, "stored responses should match")
+			} else {
+				assert.Contains(t, errL[0].Error(), tt.expectedError, "error should match")
+			}
+
+		})
+	}
+}
+
+func TestValidateStoredResp(t *testing.T) {
+	deps := &endpointDeps{
+		fakeUUIDGenerator{},
+		&nobidExchange{},
+		newParamsValidator(t),
+		&mockStoredReqFetcher{},
+		empty_fetcher.EmptyFetcher{},
+		empty_fetcher.EmptyFetcher{},
+		&config.Configuration{MaxRequestSize: maxSize},
+		&metricsConfig.NilMetricsEngine{},
+		analyticsConf.NewPBSAnalytics(&config.Analytics{}),
+		map[string]string{},
+		false,
+		[]byte{},
+		openrtb_ext.BuildBidderMap(),
+		nil,
+		nil,
+		hardcodedResponseIPValidator{response: true},
+		&mockStoredResponseFetcher{},
+	}
+
+	testCases := []struct {
+		description         string
+		givenRequestWrapper *openrtb_ext.RequestWrapper
+		expectedErrorList   []error
+		hasStoredResponses  bool
+	}{
+		{
+			description: "One imp with stored response, expect validate request to throw no errors",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					ID:  "Some-ID",
+					App: &openrtb2.App{},
+					Imp: []openrtb2.Imp{
+						{
+							ID: "Some-Imp-ID",
+							Banner: &openrtb2.Banner{
+								Format: []openrtb2.Format{
+									{
+										W: 600,
+										H: 500,
+									},
+									{
+										W: 300,
+										H: 600,
+									},
+								},
+							},
+							Ext: []byte(`{"appnexus":{"placementId": 12345678}, "prebid": {"storedAuctionResponse": {"id": "6d718149-6dfe-25ae-a7d6-305399f77f04"}}}`),
+						},
+					},
+				},
+			},
+			expectedErrorList:  []error{},
+			hasStoredResponses: true,
+		},
+		{
+			description: "Two imps with stored responses, expect validate request to throw no errors",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					ID:  "Some-ID",
+					App: &openrtb2.App{},
+					Imp: []openrtb2.Imp{
+						{
+							ID: "Some-Imp-ID",
+							Banner: &openrtb2.Banner{
+								Format: []openrtb2.Format{
+									{
+										W: 600,
+										H: 500,
+									},
+									{
+										W: 300,
+										H: 600,
+									},
+								},
+							},
+							Ext: []byte(`{"appnexus":{"placementId": 12345678}, "prebid": {"storedAuctionResponse": {"id": "6d718149-6dfe-25ae-a7d6-305399f77f04"}}}`),
+						},
+						{
+							ID: "Some-Imp-ID2",
+							Banner: &openrtb2.Banner{
+								Format: []openrtb2.Format{
+									{
+										W: 600,
+										H: 500,
+									},
+									{
+										W: 300,
+										H: 600,
+									},
+								},
+							},
+							Ext: []byte(`{"appnexus":{"placementId": 12345678}, "prebid": {"storedAuctionResponse": {"id": "6d718149-6dfe-25ae-a7d6-305399f77f04"}}}`),
+						},
+					},
+				},
+			},
+			expectedErrorList:  []error{},
+			hasStoredResponses: true,
+		},
+		{
+			description: "Two imps, one with stored response, expect validate request to throw validation error",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					ID:  "Some-ID",
+					App: &openrtb2.App{},
+					Imp: []openrtb2.Imp{
+						{
+							ID: "Some-Imp-ID",
+							Banner: &openrtb2.Banner{
+								Format: []openrtb2.Format{
+									{
+										W: 600,
+										H: 500,
+									},
+									{
+										W: 300,
+										H: 600,
+									},
+								},
+							},
+							Ext: []byte(`{"appnexus":{"placementId": 12345678}, "prebid": {"storedAuctionResponse": {"id": "6d718149-6dfe-25ae-a7d6-305399f77f04"}}}`),
+						},
+						{
+							ID: "Some-Imp-ID2",
+							Banner: &openrtb2.Banner{
+								Format: []openrtb2.Format{
+									{
+										W: 600,
+										H: 500,
+									},
+									{
+										W: 300,
+										H: 600,
+									},
+								},
+							},
+							Ext: []byte(`{"appnexus":{"placementId": 12345678}}`),
+						},
+					},
+				},
+			},
+			expectedErrorList:  []error{errors.New("request validation failed. The StoredAuctionResponse.ID field must be completely present with, or completely absent from, all impressions in request. No StoredAuctionResponse data found for request.imp[1].ext.prebid \n")},
+			hasStoredResponses: true,
+		},
+	}
+
+	for _, test := range testCases {
+		errorList := deps.validateRequest(test.givenRequestWrapper, false, test.hasStoredResponses)
+		assert.Equalf(t, test.expectedErrorList, errorList, "Error doesn't match: %s\n", test.description)
+	}
+}
+
+func TestProcessStoredAuctionResponsesErrors(t *testing.T) {
+	deps := &endpointDeps{}
+
+	testCases := []struct {
+		description       string
+		requestJson       []byte
+		expectedErrorList []error
+	}{
+		{
+			description: "Invalid stored auction response format: empty stored Auction Response Id",
+			requestJson: []byte(`{"imp": [
+    			  {
+    			    "id": "imp-id1",
+    			    "ext": {
+    			      "prebid": {
+    			        "storedauctionresponse": {
+    			        }
+    			      }
+    			    }
+    			  }
+    			]}`),
+			expectedErrorList: []error{errors.New("request.imp[0] has ext.prebid.storedauctionresponse specified, but \"id\" field is missing ")},
+		},
+		{
+			description: "Invalid stored auction response format: empty stored Auction Response Id in second imp",
+			requestJson: []byte(`{"imp": [
+    			  {
+    			    "id": "imp-id1",
+    			    "ext": {
+    			      "prebid": {
+    			        "storedauctionresponse": {
+							"id":"123"
+    			        }
+    			      }
+    			    }
+    			  },
+			      {
+    			    "id": "imp-id2",
+    			    "ext": {
+    			      "prebid": {
+    			        "storedauctionresponse": {
+							"id":""
+    			        }
+    			      }
+    			    }
+    			  }
+    			]}`),
+			expectedErrorList: []error{errors.New("request.imp[1] has ext.prebid.storedauctionresponse specified, but \"id\" field is missing ")},
+		},
+		{
+			description: "Invalid stored auction response format: integer imp Id",
+			requestJson: []byte(`{"imp": [
+    			  {
+    			    "id": 123,
+    			    "ext": {
+    			      "prebid": {
+    			        "storedauctionresponse": {
+							"id":"123"
+    			        }
+    			      }
+    			    }
+    			  }
+    			]}`),
+			expectedErrorList: []error{errors.New("Value is not a string: 123")},
+		},
+	}
+
+	for _, test := range testCases {
+		_, errorList := deps.processStoredAuctionResponses(nil, test.requestJson)
+		assert.Equalf(t, test.expectedErrorList, errorList, "Error doesn't match: %s\n", test.description)
+	}
+
+}
+
+type mockStoredResponseFetcher struct {
+	data map[string]json.RawMessage
+}
+
+func (cf *mockStoredResponseFetcher) FetchRequests(ctx context.Context, requestIDs []string, impIDs []string) (requestData map[string]json.RawMessage, impData map[string]json.RawMessage, errs []error) {
+	return nil, nil, nil
+}
+
+func (cf *mockStoredResponseFetcher) FetchResponses(ctx context.Context, ids []string) (data map[string]json.RawMessage, errs []error) {
+	return cf.data, nil
+}
+
 func getObject(t *testing.T, filename, key string) json.RawMessage {
 	requestData, err := ioutil.ReadFile("sample-requests/valid-whole/supplementary/" + filename)
 	if err != nil {
diff --git a/errortypes/aggregate_test.go b/errortypes/aggregate_test.go
index 2d4ce21b..86a1d15e 100644
--- a/errortypes/aggregate_test.go
+++ b/errortypes/aggregate_test.go
@@ -4,7 +4,7 @@ import (
 	"errors"
 	"testing"
 
-	"github.com/influxdata/influxdb/pkg/testing/assert"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestAggregateError(t *testing.T) {
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index c7429dff..474c0284 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -6,7 +6,6 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"github.com/prebid/prebid-server/firstpartydata"
 	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
@@ -16,7 +15,13 @@ import (
 	"testing"
 	"time"
 
+	"github.com/buger/jsonparser"
 	"github.com/mxmCherry/openrtb/v15/openrtb2"
+	"github.com/prebid/prebid-server/firstpartydata"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+	jsonpatch "gopkg.in/evanphx/json-patch.v4"
+
 	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/currency"
@@ -30,11 +35,6 @@ import (
 	"github.com/prebid/prebid-server/stored_requests"
 	"github.com/prebid/prebid-server/stored_requests/backends/file_fetcher"
 	"github.com/prebid/prebid-server/usersync"
-
-	"github.com/buger/jsonparser"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/mock"
-	"github.com/yudai/gojsondiff"
 )
 
 func TestNewExchange(t *testing.T) {
@@ -2177,7 +2177,7 @@ func extractResponseTimes(t *testing.T, context string, bid *openrtb2.BidRespons
 		// Delete the response times so that they don't appear in the JSON, because they can't be tested reliably anyway.
 		// If there's no other ext, just delete it altogether.
 		bid.Ext = jsonparser.Delete(bid.Ext, "responsetimemillis")
-		if diff, err := gojsondiff.New().Compare(bid.Ext, []byte("{}")); err == nil && !diff.Modified() {
+		if jsonpatch.Equal(bid.Ext, []byte("{}")) {
 			bid.Ext = nil
 		}
 		return responseTimes
@@ -3667,6 +3667,236 @@ func TestFPD(t *testing.T) {
 
 }
 
+func TestStoredAuctionResponses(t *testing.T) {
+	categoriesFetcher, error := newCategoryFetcher("./test/category-mapping")
+	if error != nil {
+		t.Errorf("Failed to create a category Fetcher: %v", error)
+	}
+
+	e := new(exchange)
+	e.cache = &wellBehavedCache{}
+	e.me = &metricsConf.NilMetricsEngine{}
+	e.gDPR = gdpr.AlwaysAllow{}
+	e.categoriesFetcher = categoriesFetcher
+	e.bidIDGenerator = &mockBidIDGenerator{false, false}
+	e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0))
+
+	// Define mock incoming bid requeset
+	mockBidRequest := &openrtb2.BidRequest{
+		ID: "request-id",
+		Imp: []openrtb2.Imp{{
+			ID:    "impression-id",
+			Video: &openrtb2.Video{W: 400, H: 300},
+		}},
+	}
+
+	expectedBidResponse := &openrtb2.BidResponse{
+		ID: "request-id",
+		SeatBid: []openrtb2.SeatBid{
+			{
+				Bid: []openrtb2.Bid{
+					{ID: "bid_id", ImpID: "impression-id", Ext: json.RawMessage(`{"prebid":{"type":""}}`)},
+				},
+				Seat: "appnexus",
+			},
+		},
+	}
+
+	testCases := []struct {
+		desc              string
+		storedAuctionResp map[string]json.RawMessage
+		errorExpected     bool
+	}{
+		{
+			desc: "Single imp with valid stored response",
+			storedAuctionResp: map[string]json.RawMessage{
+				"impression-id": json.RawMessage(`[{"bid": [{"id": "bid_id"}],"seat": "appnexus"}]`),
+			},
+			errorExpected: false,
+		},
+		{
+			desc: "Single imp with invalid stored response",
+			storedAuctionResp: map[string]json.RawMessage{
+				"impression-id": json.RawMessage(`[}]`),
+			},
+			errorExpected: true,
+		},
+	}
+
+	for _, test := range testCases {
+
+		auctionRequest := AuctionRequest{
+			BidRequest:             mockBidRequest,
+			Account:                config.Account{},
+			UserSyncs:              &emptyUsersync{},
+			StoredAuctionResponses: test.storedAuctionResp,
+		}
+
+		// Run test
+		outBidResponse, err := e.HoldAuction(context.Background(), auctionRequest, &DebugLog{})
+		if test.errorExpected {
+			assert.Error(t, err, "Error should be returned")
+		} else {
+			assert.NoErrorf(t, err, "%s. HoldAuction error: %v \n", test.desc, err)
+			outBidResponse.Ext = nil
+			assert.Equal(t, expectedBidResponse, outBidResponse, "Incorrect stored auction response")
+		}
+
+	}
+}
+
+func TestBuildStoredAuctionResponses(t *testing.T) {
+
+	type testIn struct {
+		StoredAuctionResponses map[string]json.RawMessage
+	}
+	type testResults struct {
+		adapterBids  map[openrtb_ext.BidderName]*pbsOrtbSeatBid
+		liveAdapters []openrtb_ext.BidderName
+	}
+
+	testCases := []struct {
+		desc     string
+		in       testIn
+		expected testResults
+	}{
+		{
+			desc: "Single imp with single stored response bid",
+			in: testIn{
+				StoredAuctionResponses: map[string]json.RawMessage{
+					"impression-id": json.RawMessage(`[{"bid": [{"id": "bid_id"}],"seat": "appnexus"}]`),
+				},
+			},
+			expected: testResults{
+				adapterBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{
+					openrtb_ext.BidderName("appnexus"): {
+						bids: []*pbsOrtbBid{
+							{
+								bid: &openrtb2.Bid{ID: "bid_id", ImpID: "impression-id"},
+							},
+						},
+					},
+				},
+				liveAdapters: []openrtb_ext.BidderName{openrtb_ext.BidderName("appnexus")},
+			},
+		},
+		{
+			desc: "Single imp with multiple bids in stored response one bidder",
+			in: testIn{
+				StoredAuctionResponses: map[string]json.RawMessage{
+					"impression-id": json.RawMessage(`[{"bid": [{"id": "bid_id1"}, {"id": "bid_id2"}],"seat": "appnexus"}]`),
+				},
+			},
+			expected: testResults{
+				adapterBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{
+					openrtb_ext.BidderName("appnexus"): {
+						bids: []*pbsOrtbBid{
+							{bid: &openrtb2.Bid{ID: "bid_id1", ImpID: "impression-id"}},
+							{bid: &openrtb2.Bid{ID: "bid_id2", ImpID: "impression-id"}},
+						},
+					},
+				},
+				liveAdapters: []openrtb_ext.BidderName{openrtb_ext.BidderName("appnexus")},
+			},
+		},
+		{
+			desc: "Single imp with multiple bids in stored response two bidders",
+			in: testIn{
+				StoredAuctionResponses: map[string]json.RawMessage{
+					"impression-id": json.RawMessage(`[{"bid": [{"id": "apn_id1"}, {"id": "apn_id2"}],"seat": "appnexus"}, {"bid": [{"id": "rubicon_id1"}, {"id": "rubicon_id2"}],"seat": "rubicon"}]`),
+				},
+			},
+			expected: testResults{
+				adapterBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{
+					openrtb_ext.BidderName("appnexus"): {
+						bids: []*pbsOrtbBid{
+							{bid: &openrtb2.Bid{ID: "apn_id1", ImpID: "impression-id"}},
+							{bid: &openrtb2.Bid{ID: "apn_id2", ImpID: "impression-id"}},
+						},
+					},
+					openrtb_ext.BidderName("rubicon"): {
+						bids: []*pbsOrtbBid{
+							{bid: &openrtb2.Bid{ID: "rubicon_id1", ImpID: "impression-id"}},
+							{bid: &openrtb2.Bid{ID: "rubicon_id2", ImpID: "impression-id"}},
+						},
+					},
+				},
+				liveAdapters: []openrtb_ext.BidderName{openrtb_ext.BidderName("appnexus"), openrtb_ext.BidderName("rubicon")},
+			},
+		},
+		{
+			desc: "Two imps with two bids in stored response two bidders, different bids number",
+			in: testIn{
+				StoredAuctionResponses: map[string]json.RawMessage{
+					"impression-id1": json.RawMessage(`[{"bid": [{"id": "apn_id1"}, {"id": "apn_id2"}],"seat": "appnexus"}]`),
+					"impression-id2": json.RawMessage(`[{"bid": [{"id": "apn_id1"}, {"id": "apn_id2"}],"seat": "appnexus"}, {"bid": [{"id": "rubicon_id1"}, {"id": "rubicon_id2"}],"seat": "rubicon"}]`),
+				},
+			},
+			expected: testResults{
+				adapterBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{
+					openrtb_ext.BidderName("appnexus"): {
+						bids: []*pbsOrtbBid{
+							{bid: &openrtb2.Bid{ID: "apn_id1", ImpID: "impression-id1"}},
+							{bid: &openrtb2.Bid{ID: "apn_id2", ImpID: "impression-id1"}},
+							{bid: &openrtb2.Bid{ID: "apn_id1", ImpID: "impression-id2"}},
+							{bid: &openrtb2.Bid{ID: "apn_id2", ImpID: "impression-id2"}},
+						},
+					},
+					openrtb_ext.BidderName("rubicon"): {
+						bids: []*pbsOrtbBid{
+							{bid: &openrtb2.Bid{ID: "rubicon_id1", ImpID: "impression-id2"}},
+							{bid: &openrtb2.Bid{ID: "rubicon_id2", ImpID: "impression-id2"}},
+						},
+					},
+				},
+				liveAdapters: []openrtb_ext.BidderName{openrtb_ext.BidderName("appnexus"), openrtb_ext.BidderName("rubicon")},
+			},
+		},
+		{
+			desc: "Two imps with two bids in stored response two bidders",
+			in: testIn{
+				StoredAuctionResponses: map[string]json.RawMessage{
+					"impression-id1": json.RawMessage(`[{"bid": [{"id": "apn_id1"}, {"id": "apn_id2"}],"seat": "appnexus"}, {"bid": [{"id": "rubicon_id1"}, {"id": "rubicon_id2"}],"seat": "rubicon"}]`),
+					"impression-id2": json.RawMessage(`[{"bid": [{"id": "apn_id1"}, {"id": "apn_id2"}],"seat": "appnexus"}, {"bid": [{"id": "rubicon_id1"}, {"id": "rubicon_id2"}],"seat": "rubicon"}]`),
+				},
+			},
+			expected: testResults{
+				adapterBids: map[openrtb_ext.BidderName]*pbsOrtbSeatBid{
+					openrtb_ext.BidderName("appnexus"): {
+						bids: []*pbsOrtbBid{
+							{bid: &openrtb2.Bid{ID: "apn_id1", ImpID: "impression-id1"}},
+							{bid: &openrtb2.Bid{ID: "apn_id2", ImpID: "impression-id1"}},
+							{bid: &openrtb2.Bid{ID: "apn_id1", ImpID: "impression-id2"}},
+							{bid: &openrtb2.Bid{ID: "apn_id2", ImpID: "impression-id2"}},
+						},
+					},
+					openrtb_ext.BidderName("rubicon"): {
+						bids: []*pbsOrtbBid{
+							{bid: &openrtb2.Bid{ID: "rubicon_id1", ImpID: "impression-id1"}},
+							{bid: &openrtb2.Bid{ID: "rubicon_id2", ImpID: "impression-id1"}},
+							{bid: &openrtb2.Bid{ID: "rubicon_id1", ImpID: "impression-id2"}},
+							{bid: &openrtb2.Bid{ID: "rubicon_id2", ImpID: "impression-id2"}},
+						},
+					},
+				},
+				liveAdapters: []openrtb_ext.BidderName{openrtb_ext.BidderName("appnexus"), openrtb_ext.BidderName("rubicon")},
+			},
+		},
+	}
+	for _, test := range testCases {
+
+		bids, adapters, err := buildStoredAuctionResponse(test.in.StoredAuctionResponses)
+		assert.NoErrorf(t, err, "%s. HoldAuction error: %v \n", test.desc, err)
+
+		assert.ElementsMatch(t, test.expected.liveAdapters, adapters, "Incorrect adapter list")
+
+		for _, bidderName := range test.expected.liveAdapters {
+			assert.ElementsMatch(t, test.expected.adapterBids[bidderName].bids, bids[bidderName].bids, "Incorrect bids")
+		}
+
+	}
+}
+
 type exchangeSpec struct {
 	GDPREnabled       bool                   `json:"gdpr_enabled"`
 	IncomingRequest   exchangeRequest        `json:"incomingRequest"`
diff --git a/openrtb_ext/bidders_test.go b/openrtb_ext/bidders_test.go
index cde79f70..056223e2 100644
--- a/openrtb_ext/bidders_test.go
+++ b/openrtb_ext/bidders_test.go
@@ -55,7 +55,7 @@ func TestBidderParamValidatorValidate(t *testing.T) {
 		{
 			description:   "Invalid - Empty Object",
 			ext:           json.RawMessage(`{}`),
-			expectedError: "placementId: placementId is required",
+			expectedError: "(root): placementId is required",
 		},
 		{
 			description:   "Malformed",