Repository: prebid/prebid-server
Pull request #2165 (2022-02-22 18:31:38)

New Adapter: add JANet alias 
Docs: https://github.com/prebid/prebid.github.io/pull/3591


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/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..f70352ec 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}}")
@@ -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/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/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"]
+}


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/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",