Repository: prebid/prebid-server
Pull request #3096 (2023-09-12 03:47:00)

Modularity: Make request wrapper accessible in bidder request hook
Replaced BidRequest to RequestWrapper in BidderRequestPayload

SyntaxNode: Prebid Server 2.0 has been released and Go Module name has changed from `github.com/prebid/prebid-server` to `github.com/prebid/prebid-server/v2`, per Go versioning conventions.

Please merge the `master` branch and update all `prebid-server` import statements to reference the `v2` name change. Thank you.

Patch:
diff --git a/Dockerfile b/Dockerfile
index 090d9a79..bff68b61 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -20,7 +20,7 @@ RUN go mod tidy
 RUN go mod vendor
 ARG TEST="true"
 RUN if [ "$TEST" != "false" ]; then ./validate.sh ; fi
-RUN go build -mod=vendor -ldflags "-X github.com/prebid/prebid-server/version.Ver=`git describe --tags | sed 's/^v//'` -X github.com/prebid/prebid-server/version.Rev=`git rev-parse HEAD`" .
+RUN go build -mod=vendor -ldflags "-X github.com/prebid/prebid-server/v2/version.Ver=`git describe --tags | sed 's/^v//'` -X github.com/prebid/prebid-server/v2/version.Rev=`git rev-parse HEAD`" .
 
 FROM ubuntu:20.04 AS release
 LABEL maintainer="hans.hjort@xandr.com" 
diff --git a/Makefile b/Makefile
index b5b7281e..2d8aae6c 100644
--- a/Makefile
+++ b/Makefile
@@ -15,7 +15,7 @@ test: deps
 ifeq "$(adapter)" ""
 	./validate.sh
 else
-	go test github.com/prebid/prebid-server/adapters/$(adapter) -bench=.
+	go test github.com/prebid/prebid-server/v2/adapters/$(adapter) -bench=.
 endif
 
 # build-modules generates modules/builder.go file which provides a list of all available modules
@@ -32,4 +32,4 @@ image:
 
 # format runs format
 format:
-	./scripts/format.sh -f true
\ No newline at end of file
+	./scripts/format.sh -f true
diff --git a/config/account.go b/config/account.go
index f1de54f7..ee131873 100644
--- a/config/account.go
+++ b/config/account.go
@@ -58,17 +58,30 @@ type AccountCCPA struct {
 }
 
 type AccountPriceFloors struct {
-	Enabled                bool `mapstructure:"enabled" json:"enabled"`
-	EnforceFloorsRate      int  `mapstructure:"enforce_floors_rate" json:"enforce_floors_rate"`
-	AdjustForBidAdjustment bool `mapstructure:"adjust_for_bid_adjustment" json:"adjust_for_bid_adjustment"`
-	EnforceDealFloors      bool `mapstructure:"enforce_deal_floors" json:"enforce_deal_floors"`
-	UseDynamicData         bool `mapstructure:"use_dynamic_data" json:"use_dynamic_data"`
-	MaxRule                int  `mapstructure:"max_rules" json:"max_rules"`
-	MaxSchemaDims          int  `mapstructure:"max_schema_dims" json:"max_schema_dims"`
+	Enabled                bool              `mapstructure:"enabled" json:"enabled"`
+	EnforceFloorsRate      int               `mapstructure:"enforce_floors_rate" json:"enforce_floors_rate"`
+	AdjustForBidAdjustment bool              `mapstructure:"adjust_for_bid_adjustment" json:"adjust_for_bid_adjustment"`
+	EnforceDealFloors      bool              `mapstructure:"enforce_deal_floors" json:"enforce_deal_floors"`
+	UseDynamicData         bool              `mapstructure:"use_dynamic_data" json:"use_dynamic_data"`
+	MaxRule                int               `mapstructure:"max_rules" json:"max_rules"`
+	MaxSchemaDims          int               `mapstructure:"max_schema_dims" json:"max_schema_dims"`
+	Fetcher                AccountFloorFetch `mapstructure:"fetch" json:"fetch"`
 }
 
-func (pf *AccountPriceFloors) validate(errs []error) []error {
+// AccountFloorFetch defines the configuration for dynamic floors fetching.
+type AccountFloorFetch struct {
+	Enabled       bool   `mapstructure:"enabled" json:"enabled"`
+	URL           string `mapstructure:"url" json:"url"`
+	Timeout       int    `mapstructure:"timeout_ms" json:"timeout_ms"`
+	MaxFileSizeKB int    `mapstructure:"max_file_size_kb" json:"max_file_size_kb"`
+	MaxRules      int    `mapstructure:"max_rules" json:"max_rules"`
+	MaxAge        int    `mapstructure:"max_age_sec" json:"max_age_sec"`
+	Period        int    `mapstructure:"period_sec" json:"period_sec"`
+	MaxSchemaDims int    `mapstructure:"max_schema_dims" json:"max_schema_dims"`
+	AccountID     string `mapstructure:"accountID" json:"accountID"`
+}
 
+func (pf *AccountPriceFloors) validate(errs []error) []error {
 	if pf.EnforceFloorsRate < 0 || pf.EnforceFloorsRate > 100 {
 		errs = append(errs, fmt.Errorf(`account_defaults.price_floors.enforce_floors_rate should be between 0 and 100`))
 	}
@@ -81,6 +94,34 @@ func (pf *AccountPriceFloors) validate(errs []error) []error {
 		errs = append(errs, fmt.Errorf(`account_defaults.price_floors.max_schema_dims should be between 0 and 20`))
 	}
 
+	if pf.Fetcher.Period > pf.Fetcher.MaxAge {
+		errs = append(errs, fmt.Errorf(`account_defaults.price_floors.fetch.period_sec should be less than account_defaults.price_floors.fetch.max_age_sec`))
+	}
+
+	if pf.Fetcher.Period < 300 {
+		errs = append(errs, fmt.Errorf(`account_defaults.price_floors.fetch.period_sec should not be less than 300 seconds`))
+	}
+
+	if pf.Fetcher.MaxAge < 600 {
+		errs = append(errs, fmt.Errorf(`account_defaults.price_floors.fetch.max_age_sec should not be less than 600 seconds and greater than maximum integer value`))
+	}
+
+	if !(pf.Fetcher.Timeout > 10 && pf.Fetcher.Timeout < 10000) {
+		errs = append(errs, fmt.Errorf(`account_defaults.price_floors.fetch.timeout_ms should be between 10 to 10,000 miliseconds`))
+	}
+
+	if pf.Fetcher.MaxRules < 0 {
+		errs = append(errs, fmt.Errorf(`account_defaults.price_floors.fetch.max_rules should be greater than or equal to 0`))
+	}
+
+	if pf.Fetcher.MaxFileSizeKB < 0 {
+		errs = append(errs, fmt.Errorf(`account_defaults.price_floors.fetch.max_file_size_kb should be greater than or equal to 0`))
+	}
+
+	if !(pf.Fetcher.MaxSchemaDims >= 0 && pf.Fetcher.MaxSchemaDims < 20) {
+		errs = append(errs, fmt.Errorf(`account_defaults.price_floors.fetch.max_schema_dims should not be less than 0 and greater than 20`))
+	}
+
 	return errs
 }
 
diff --git a/config/bidderinfo.go b/config/bidderinfo.go
index 45abc852..d6d0ba79 100644
--- a/config/bidderinfo.go
+++ b/config/bidderinfo.go
@@ -560,58 +560,79 @@ func validateSyncer(bidderInfo BidderInfo) error {
 	return nil
 }
 
-func applyBidderInfoConfigOverrides(configBidderInfos BidderInfos, fsBidderInfos BidderInfos, normalizeBidderName func(string) (openrtb_ext.BidderName, bool)) (BidderInfos, error) {
-	for bidderName, bidderInfo := range configBidderInfos {
-		normalizedBidderName, bidderNameExists := normalizeBidderName(bidderName)
-		if !bidderNameExists {
+func applyBidderInfoConfigOverrides(configBidderInfos nillableFieldBidderInfos, fsBidderInfos BidderInfos, normalizeBidderName func(string) (openrtb_ext.BidderName, bool)) (BidderInfos, error) {
+	mergedBidderInfos := make(map[string]BidderInfo, len(fsBidderInfos))
+
+	for bidderName, configBidderInfo := range configBidderInfos {
+		normalizedBidderName, exists := normalizeBidderName(bidderName)
+		if !exists {
 			return nil, fmt.Errorf("error setting configuration for bidder %s: unknown bidder", bidderName)
 		}
-		if fsBidderCfg, exists := fsBidderInfos[string(normalizedBidderName)]; exists {
-			bidderInfo.Syncer = bidderInfo.Syncer.Override(fsBidderCfg.Syncer)
+		fsBidderInfo, exists := fsBidderInfos[string(normalizedBidderName)]
+		if !exists {
+			return nil, fmt.Errorf("error finding configuration for bidder %s: unknown bidder", bidderName)
+		}
 
-			if bidderInfo.Endpoint == "" && len(fsBidderCfg.Endpoint) > 0 {
-				bidderInfo.Endpoint = fsBidderCfg.Endpoint
-			}
-			if bidderInfo.ExtraAdapterInfo == "" && len(fsBidderCfg.ExtraAdapterInfo) > 0 {
-				bidderInfo.ExtraAdapterInfo = fsBidderCfg.ExtraAdapterInfo
-			}
-			if bidderInfo.Maintainer == nil && fsBidderCfg.Maintainer != nil {
-				bidderInfo.Maintainer = fsBidderCfg.Maintainer
-			}
-			if bidderInfo.Capabilities == nil && fsBidderCfg.Capabilities != nil {
-				bidderInfo.Capabilities = fsBidderCfg.Capabilities
-			}
-			if bidderInfo.Debug == nil && fsBidderCfg.Debug != nil {
-				bidderInfo.Debug = fsBidderCfg.Debug
-			}
-			if bidderInfo.GVLVendorID == 0 && fsBidderCfg.GVLVendorID > 0 {
-				bidderInfo.GVLVendorID = fsBidderCfg.GVLVendorID
-			}
-			if bidderInfo.XAPI.Username == "" && fsBidderCfg.XAPI.Username != "" {
-				bidderInfo.XAPI.Username = fsBidderCfg.XAPI.Username
-			}
-			if bidderInfo.XAPI.Password == "" && fsBidderCfg.XAPI.Password != "" {
-				bidderInfo.XAPI.Password = fsBidderCfg.XAPI.Password
-			}
-			if bidderInfo.XAPI.Tracker == "" && fsBidderCfg.XAPI.Tracker != "" {
-				bidderInfo.XAPI.Tracker = fsBidderCfg.XAPI.Tracker
-			}
-			if bidderInfo.PlatformID == "" && fsBidderCfg.PlatformID != "" {
-				bidderInfo.PlatformID = fsBidderCfg.PlatformID
-			}
-			if bidderInfo.AppSecret == "" && fsBidderCfg.AppSecret != "" {
-				bidderInfo.AppSecret = fsBidderCfg.AppSecret
-			}
-			if bidderInfo.EndpointCompression == "" && fsBidderCfg.EndpointCompression != "" {
-				bidderInfo.EndpointCompression = fsBidderCfg.EndpointCompression
-			}
+		mergedBidderInfo := fsBidderInfo
+		mergedBidderInfo.Syncer = configBidderInfo.bidderInfo.Syncer.Override(fsBidderInfo.Syncer)
+		if len(configBidderInfo.bidderInfo.Endpoint) > 0 {
+			mergedBidderInfo.Endpoint = configBidderInfo.bidderInfo.Endpoint
+		}
+		if len(configBidderInfo.bidderInfo.ExtraAdapterInfo) > 0 {
+			mergedBidderInfo.ExtraAdapterInfo = configBidderInfo.bidderInfo.ExtraAdapterInfo
+		}
+		if configBidderInfo.bidderInfo.Maintainer != nil {
+			mergedBidderInfo.Maintainer = configBidderInfo.bidderInfo.Maintainer
+		}
+		if configBidderInfo.bidderInfo.Capabilities != nil {
+			mergedBidderInfo.Capabilities = configBidderInfo.bidderInfo.Capabilities
+		}
+		if configBidderInfo.bidderInfo.Debug != nil {
+			mergedBidderInfo.Debug = configBidderInfo.bidderInfo.Debug
+		}
+		if configBidderInfo.bidderInfo.GVLVendorID > 0 {
+			mergedBidderInfo.GVLVendorID = configBidderInfo.bidderInfo.GVLVendorID
+		}
+		if configBidderInfo.bidderInfo.XAPI.Username != "" {
+			mergedBidderInfo.XAPI.Username = configBidderInfo.bidderInfo.XAPI.Username
+		}
+		if configBidderInfo.bidderInfo.XAPI.Password != "" {
+			mergedBidderInfo.XAPI.Password = configBidderInfo.bidderInfo.XAPI.Password
+		}
+		if configBidderInfo.bidderInfo.XAPI.Tracker != "" {
+			mergedBidderInfo.XAPI.Tracker = configBidderInfo.bidderInfo.XAPI.Tracker
+		}
+		if configBidderInfo.bidderInfo.PlatformID != "" {
+			mergedBidderInfo.PlatformID = configBidderInfo.bidderInfo.PlatformID
+		}
+		if configBidderInfo.bidderInfo.AppSecret != "" {
+			mergedBidderInfo.AppSecret = configBidderInfo.bidderInfo.AppSecret
+		}
+		if configBidderInfo.nillableFields.Disabled != nil {
+			mergedBidderInfo.Disabled = configBidderInfo.bidderInfo.Disabled
+		}
+		if configBidderInfo.nillableFields.ModifyingVastXmlAllowed != nil {
+			mergedBidderInfo.ModifyingVastXmlAllowed = configBidderInfo.bidderInfo.ModifyingVastXmlAllowed
+		}
+		if configBidderInfo.bidderInfo.Experiment.AdsCert.Enabled == true {
+			mergedBidderInfo.Experiment.AdsCert.Enabled = true
+		}
+		if configBidderInfo.bidderInfo.EndpointCompression != "" {
+			mergedBidderInfo.EndpointCompression = configBidderInfo.bidderInfo.EndpointCompression
+		}
+		if configBidderInfo.bidderInfo.OpenRTB != nil {
+			mergedBidderInfo.OpenRTB = configBidderInfo.bidderInfo.OpenRTB
+		}
 
-			fsBidderInfos[string(normalizedBidderName)] = bidderInfo
-		} else {
-			return nil, fmt.Errorf("error finding configuration for bidder %s: unknown bidder", bidderName)
+		mergedBidderInfos[string(normalizedBidderName)] = mergedBidderInfo
+	}
+	for bidderName, fsBidderInfo := range fsBidderInfos {
+		if _, exists := mergedBidderInfos[bidderName]; !exists {
+			mergedBidderInfos[bidderName] = fsBidderInfo
 		}
 	}
-	return fsBidderInfos, nil
+
+	return mergedBidderInfos, nil
 }
 
 // Override returns a new Syncer object where values in the original are replaced by non-empty/non-default
diff --git a/config/config.go b/config/config.go
index 11b7eb58..60a541e2 100644
--- a/config/config.go
+++ b/config/config.go
@@ -103,7 +103,16 @@ type Configuration struct {
 }
 
 type PriceFloors struct {
-	Enabled bool `mapstructure:"enabled"`
+	Enabled bool              `mapstructure:"enabled"`
+	Fetcher PriceFloorFetcher `mapstructure:"fetcher"`
+}
+
+type PriceFloorFetcher struct {
+	HttpClient HTTPClient `mapstructure:"http_client"`
+	CacheSize  int        `mapstructure:"cache_size_mb"`
+	Worker     int        `mapstructure:"worker"`
+	Capacity   int        `mapstructure:"capacity"`
+	MaxRetries int        `mapstructure:"max_retries"`
 }
 
 const MIN_COOKIE_SIZE_BYTES = 500
@@ -140,10 +149,6 @@ func (cfg *Configuration) validate(v *viper.Viper) []error {
 		glog.Warning(`account_defaults.events has no effect as the feature is under development.`)
 	}
 
-	if cfg.PriceFloors.Enabled {
-		glog.Warning(`cfg.PriceFloors.Enabled will currently not do anything as price floors feature is still under development.`)
-	}
-
 	errs = cfg.Experiment.validate(errs)
 	errs = cfg.BidderInfos.validate(errs)
 	errs = cfg.AccountDefaults.Privacy.IPv6Config.Validate(errs)
@@ -758,7 +763,11 @@ func New(v *viper.Viper, bidderInfos BidderInfos, normalizeBidderName func(strin
 	// Migrate combo stored request config to separate stored_reqs and amp stored_reqs configs.
 	resolvedStoredRequestsConfig(&c)
 
-	mergedBidderInfos, err := applyBidderInfoConfigOverrides(c.BidderInfos, bidderInfos, normalizeBidderName)
+	configBidderInfosWithNillableFields, err := setConfigBidderInfoNillableFields(v, c.BidderInfos)
+	if err != nil {
+		return nil, err
+	}
+	mergedBidderInfos, err := applyBidderInfoConfigOverrides(configBidderInfosWithNillableFields, bidderInfos, normalizeBidderName)
 	if err != nil {
 		return nil, err
 	}
@@ -773,6 +782,36 @@ func New(v *viper.Viper, bidderInfos BidderInfos, normalizeBidderName func(strin
 	return &c, nil
 }
 
+type bidderInfoNillableFields struct {
+	Disabled                *bool `yaml:"disabled" mapstructure:"disabled"`
+	ModifyingVastXmlAllowed *bool `yaml:"modifyingVastXmlAllowed" mapstructure:"modifyingVastXmlAllowed"`
+}
+type nillableFieldBidderInfos map[string]nillableFieldBidderInfo
+type nillableFieldBidderInfo struct {
+	nillableFields bidderInfoNillableFields
+	bidderInfo     BidderInfo
+}
+
+func setConfigBidderInfoNillableFields(v *viper.Viper, bidderInfos BidderInfos) (nillableFieldBidderInfos, error) {
+	if len(bidderInfos) == 0 || v == nil {
+		return nil, nil
+	}
+	infos := make(nillableFieldBidderInfos, len(bidderInfos))
+
+	for bidderName, bidderInfo := range bidderInfos {
+		info := nillableFieldBidderInfo{bidderInfo: bidderInfo}
+
+		if err := v.UnmarshalKey("adapters."+bidderName+".disabled", &info.nillableFields.Disabled); err != nil {
+			return nil, fmt.Errorf("viper failed to unmarshal bidder config disabled: %v", err)
+		}
+		if err := v.UnmarshalKey("adapters."+bidderName+".modifyingvastxmlallowed", &info.nillableFields.ModifyingVastXmlAllowed); err != nil {
+			return nil, fmt.Errorf("viper failed to unmarshal bidder config modifyingvastxmlallowed: %v", err)
+		}
+		infos[bidderName] = info
+	}
+	return infos, nil
+}
+
 // MarshalAccountDefaults compiles AccountDefaults into the JSON format used for merge patch
 func (cfg *Configuration) MarshalAccountDefaults() error {
 	var err error
@@ -1060,9 +1099,30 @@ func SetupViper(v *viper.Viper, filename string, bidderInfos BidderInfos) {
 	v.SetDefault("account_defaults.price_floors.use_dynamic_data", false)
 	v.SetDefault("account_defaults.price_floors.max_rules", 100)
 	v.SetDefault("account_defaults.price_floors.max_schema_dims", 3)
+	v.SetDefault("account_defaults.price_floors.fetch.enabled", false)
+	v.SetDefault("account_defaults.price_floors.fetch.url", "")
+	v.SetDefault("account_defaults.price_floors.fetch.timeout_ms", 3000)
+	v.SetDefault("account_defaults.price_floors.fetch.max_file_size_kb", 100)
+	v.SetDefault("account_defaults.price_floors.fetch.max_rules", 1000)
+	v.SetDefault("account_defaults.price_floors.fetch.max_age_sec", 86400)
+	v.SetDefault("account_defaults.price_floors.fetch.period_sec", 3600)
+	v.SetDefault("account_defaults.price_floors.fetch.max_schema_dims", 0)
+
+	v.SetDefault("account_defaults.events_enabled", false)
 	v.SetDefault("account_defaults.privacy.ipv6.anon_keep_bits", 56)
 	v.SetDefault("account_defaults.privacy.ipv4.anon_keep_bits", 24)
 
+	//Defaults for Price floor fetcher
+	v.SetDefault("price_floors.fetcher.worker", 20)
+	v.SetDefault("price_floors.fetcher.capacity", 20000)
+	v.SetDefault("price_floors.fetcher.cache_size_mb", 64)
+	v.SetDefault("price_floors.fetcher.http_client.max_connections_per_host", 0) // unlimited
+	v.SetDefault("price_floors.fetcher.http_client.max_idle_connections", 40)
+	v.SetDefault("price_floors.fetcher.http_client.max_idle_connections_per_host", 2)
+	v.SetDefault("price_floors.fetcher.http_client.idle_connection_timeout_seconds", 60)
+	v.SetDefault("price_floors.fetcher.max_retries", 10)
+
+	v.SetDefault("account_defaults.events_enabled", false)
 	v.SetDefault("compression.response.enable_gzip", false)
 	v.SetDefault("compression.request.enable_gzip", false)
 
diff --git a/exchange/bidder.go b/exchange/bidder.go
index db654bf4..3cc55acb 100644
--- a/exchange/bidder.go
+++ b/exchange/bidder.go
@@ -131,7 +131,8 @@ type bidderAdapterConfig struct {
 }
 
 func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, hookExecutor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) ([]*entities.PbsOrtbSeatBid, extraBidderRespInfo, []error) {
-	reject := hookExecutor.ExecuteBidderRequestStage(bidderRequest.BidRequest, string(bidderRequest.BidderName))
+	request := openrtb_ext.RequestWrapper{BidRequest: bidderRequest.BidRequest}
+	reject := hookExecutor.ExecuteBidderRequestStage(&request, string(bidderRequest.BidderName))
 	if reject != nil {
 		return nil, extraBidderRespInfo{}, []error{reject}
 	}
@@ -143,6 +144,10 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest Bidde
 		extraRespInfo   extraBidderRespInfo
 	)
 
+	// rebuild request after modules execution
+	request.RebuildRequest()
+	bidderRequest.BidRequest = request.BidRequest
+
 	//check if real request exists for this bidder or it only has stored responses
 	dataLen := 0
 	if len(bidderRequest.BidRequest.Imp) > 0 {
@@ -406,7 +411,7 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest Bidde
 
 func addNativeTypes(bid *openrtb2.Bid, request *openrtb2.BidRequest) (*nativeResponse.Response, []error) {
 	var errs []error
-	var nativeMarkup *nativeResponse.Response
+	var nativeMarkup nativeResponse.Response
 	if err := jsonutil.UnmarshalValid(json.RawMessage(bid.AdM), &nativeMarkup); err != nil || len(nativeMarkup.Assets) == 0 {
 		// Some bidders are returning non-IAB compliant native markup. In this case Prebid server will not be able to add types. E.g Facebook
 		return nil, errs
@@ -429,7 +434,7 @@ func addNativeTypes(bid *openrtb2.Bid, request *openrtb2.BidRequest) (*nativeRes
 		}
 	}
 
-	return nativeMarkup, errs
+	return &nativeMarkup, errs
 }
 
 func setAssetTypes(asset nativeResponse.Asset, nativePayload nativeRequests.Request) error {
diff --git a/exchange/exchange.go b/exchange/exchange.go
index b07c4bdf..bb59fe90 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -81,7 +81,8 @@ type exchange struct {
 	bidValidationEnforcement config.Validations
 	requestSplitter          requestSplitter
 	macroReplacer            macros.Replacer
-	floor                    config.PriceFloors
+	priceFloorEnabled        bool
+	priceFloorFetcher        floors.FloorFetcher
 }
 
 // Container to pass out response ext data from the GetAllBids goroutines back into the main thread
@@ -130,7 +131,7 @@ func (randomDeduplicateBidBooleanGenerator) Generate() bool {
 	return rand.Intn(100) < 50
 }
 
-func NewExchange(adapters map[openrtb_ext.BidderName]AdaptedBidder, cache prebid_cache_client.Client, cfg *config.Configuration, syncersByBidder map[string]usersync.Syncer, metricsEngine metrics.MetricsEngine, infos config.BidderInfos, gdprPermsBuilder gdpr.PermissionsBuilder, currencyConverter *currency.RateConverter, categoriesFetcher stored_requests.CategoryFetcher, adsCertSigner adscert.Signer, macroReplacer macros.Replacer) Exchange {
+func NewExchange(adapters map[openrtb_ext.BidderName]AdaptedBidder, cache prebid_cache_client.Client, cfg *config.Configuration, syncersByBidder map[string]usersync.Syncer, metricsEngine metrics.MetricsEngine, infos config.BidderInfos, gdprPermsBuilder gdpr.PermissionsBuilder, currencyConverter *currency.RateConverter, categoriesFetcher stored_requests.CategoryFetcher, adsCertSigner adscert.Signer, macroReplacer macros.Replacer, priceFloorFetcher floors.FloorFetcher) Exchange {
 	bidderToSyncerKey := map[string]string{}
 	for bidder, syncer := range syncersByBidder {
 		bidderToSyncerKey[bidder] = syncer.Key()
@@ -175,7 +176,8 @@ func NewExchange(adapters map[openrtb_ext.BidderName]AdaptedBidder, cache prebid
 		bidValidationEnforcement: cfg.Validations,
 		requestSplitter:          requestSplitter,
 		macroReplacer:            macroReplacer,
-		floor:                    cfg.PriceFloors,
+		priceFloorEnabled:        cfg.PriceFloors.Enabled,
+		priceFloorFetcher:        priceFloorFetcher,
 	}
 }
 
@@ -233,7 +235,6 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog
 		return nil, nil
 	}
 
-	var floorErrs []error
 	err := r.HookExecutor.ExecuteProcessedAuctionStage(r.BidRequestWrapper)
 	if err != nil {
 		return nil, err
@@ -268,8 +269,9 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog
 	// Get currency rates conversions for the auction
 	conversions := e.getAuctionCurrencyRates(requestExtPrebid.CurrencyConversions)
 
-	if e.floor.Enabled {
-		floorErrs = floors.EnrichWithPriceFloors(r.BidRequestWrapper, r.Account, conversions)
+	var floorErrs []error
+	if e.priceFloorEnabled {
+		floorErrs = floors.EnrichWithPriceFloors(r.BidRequestWrapper, r.Account, conversions, e.priceFloorFetcher)
 	}
 
 	responseDebugAllow, accountDebugAllow, debugLog := getDebugInfo(r.BidRequestWrapper.Test, requestExtPrebid, r.Account.DebugAllow, debugLog)
@@ -386,7 +388,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog
 	)
 
 	if anyBidsReturned {
-		if e.floor.Enabled {
+		if e.priceFloorEnabled {
 			var rejectedBids []*entities.PbsOrtbSeatBid
 			var enforceErrs []error
 
diff --git a/floors/fetcher.go b/floors/fetcher.go
new file mode 100644
index 00000000..6df43445
--- /dev/null
+++ b/floors/fetcher.go
@@ -0,0 +1,331 @@
+package floors
+
+import (
+	"container/heap"
+	"context"
+	"encoding/json"
+	"errors"
+	"io"
+	"math"
+	"net/http"
+	"strconv"
+	"time"
+
+	"github.com/alitto/pond"
+	validator "github.com/asaskevich/govalidator"
+	"github.com/coocood/freecache"
+	"github.com/golang/glog"
+	"github.com/prebid/prebid-server/v2/config"
+	"github.com/prebid/prebid-server/v2/metrics"
+	"github.com/prebid/prebid-server/v2/openrtb_ext"
+	"github.com/prebid/prebid-server/v2/util/timeutil"
+)
+
+var refetchCheckInterval = 300
+
+type fetchInfo struct {
+	config.AccountFloorFetch
+	fetchTime      int64
+	refetchRequest bool
+	retryCount     int
+}
+
+type WorkerPool interface {
+	TrySubmit(task func()) bool
+	Stop()
+}
+
+type FloorFetcher interface {
+	Fetch(configs config.AccountPriceFloors) (*openrtb_ext.PriceFloorRules, string)
+	Stop()
+}
+
+type PriceFloorFetcher struct {
+	pool            WorkerPool            // Goroutines worker pool
+	fetchQueue      FetchQueue            // Priority Queue to fetch floor data
+	fetchInProgress map[string]bool       // Map of URL with fetch status
+	configReceiver  chan fetchInfo        // Channel which recieves URLs to be fetched
+	done            chan struct{}         // Channel to close fetcher
+	cache           *freecache.Cache      // cache
+	httpClient      *http.Client          // http client to fetch data from url
+	time            timeutil.Time         // time interface to record request timings
+	metricEngine    metrics.MetricsEngine // Records malfunctions in dynamic fetch
+	maxRetries      int                   // Max number of retries for failing URLs
+}
+
+type FetchQueue []*fetchInfo
+
+func (fq FetchQueue) Len() int {
+	return len(fq)
+}
+
+func (fq FetchQueue) Less(i, j int) bool {
+	return fq[i].fetchTime < fq[j].fetchTime
+}
+
+func (fq FetchQueue) Swap(i, j int) {
+	fq[i], fq[j] = fq[j], fq[i]
+}
+
+func (fq *FetchQueue) Push(element interface{}) {
+	fetchInfo := element.(*fetchInfo)
+	*fq = append(*fq, fetchInfo)
+}
+
+func (fq *FetchQueue) Pop() interface{} {
+	old := *fq
+	n := len(old)
+	fetchInfo := old[n-1]
+	old[n-1] = nil
+	*fq = old[0 : n-1]
+	return fetchInfo
+}
+
+func (fq *FetchQueue) Top() *fetchInfo {
+	old := *fq
+	if len(old) == 0 {
+		return nil
+	}
+	return old[0]
+}
+
+func workerPanicHandler(p interface{}) {
+	glog.Errorf("floor fetcher worker panicked: %v", p)
+}
+
+func NewPriceFloorFetcher(config config.PriceFloors, httpClient *http.Client, metricEngine metrics.MetricsEngine) *PriceFloorFetcher {
+	if !config.Enabled {
+		return nil
+	}
+
+	floorFetcher := PriceFloorFetcher{
+		pool:            pond.New(config.Fetcher.Worker, config.Fetcher.Capacity, pond.PanicHandler(workerPanicHandler)),
+		fetchQueue:      make(FetchQueue, 0, 100),
+		fetchInProgress: make(map[string]bool),
+		configReceiver:  make(chan fetchInfo, config.Fetcher.Capacity),
+		done:            make(chan struct{}),
+		cache:           freecache.NewCache(config.Fetcher.CacheSize * 1024 * 1024),
+		httpClient:      httpClient,
+		time:            &timeutil.RealTime{},
+		metricEngine:    metricEngine,
+		maxRetries:      config.Fetcher.MaxRetries,
+	}
+
+	go floorFetcher.Fetcher()
+
+	return &floorFetcher
+}
+
+func (f *PriceFloorFetcher) SetWithExpiry(key string, value json.RawMessage, cacheExpiry int) {
+	f.cache.Set([]byte(key), value, cacheExpiry)
+}
+
+func (f *PriceFloorFetcher) Get(key string) (json.RawMessage, bool) {
+	data, err := f.cache.Get([]byte(key))
+	if err != nil {
+		return nil, false
+	}
+
+	return data, true
+}
+
+func (f *PriceFloorFetcher) Fetch(config config.AccountPriceFloors) (*openrtb_ext.PriceFloorRules, string) {
+	if f == nil || !config.UseDynamicData || len(config.Fetcher.URL) == 0 || !validator.IsURL(config.Fetcher.URL) {
+		return nil, openrtb_ext.FetchNone
+	}
+
+	// Check for floors JSON in cache
+	if result, found := f.Get(config.Fetcher.URL); found {
+		var fetchedFloorData openrtb_ext.PriceFloorRules
+		if err := json.Unmarshal(result, &fetchedFloorData); err != nil || fetchedFloorData.Data == nil {
+			return nil, openrtb_ext.FetchError
+		}
+		return &fetchedFloorData, openrtb_ext.FetchSuccess
+	}
+
+	//miss: push to channel to fetch and return empty response
+	if config.Enabled && config.Fetcher.Enabled && config.Fetcher.Timeout > 0 {
+		fetchConfig := fetchInfo{AccountFloorFetch: config.Fetcher, fetchTime: f.time.Now().Unix(), refetchRequest: false, retryCount: 0}
+		f.configReceiver <- fetchConfig
+	}
+
+	return nil, openrtb_ext.FetchInprogress
+}
+
+func (f *PriceFloorFetcher) worker(fetchConfig fetchInfo) {
+	floorData, fetchedMaxAge := f.fetchAndValidate(fetchConfig.AccountFloorFetch)
+	if floorData != nil {
+		// Reset retry count when data is successfully fetched
+		fetchConfig.retryCount = 0
+
+		// Update cache with new floor rules
+		cacheExpiry := fetchConfig.AccountFloorFetch.MaxAge
+		if fetchedMaxAge != 0 {
+			cacheExpiry = fetchedMaxAge
+		}
+		floorData, err := json.Marshal(floorData)
+		if err != nil {
+			glog.Errorf("Error while marshaling fetched floor data for url %s", fetchConfig.AccountFloorFetch.URL)
+		} else {
+			f.SetWithExpiry(fetchConfig.AccountFloorFetch.URL, floorData, cacheExpiry)
+		}
+	} else {
+		fetchConfig.retryCount++
+	}
+
+	// Send to refetch channel
+	if fetchConfig.retryCount < f.maxRetries {
+		fetchConfig.fetchTime = f.time.Now().Add(time.Duration(fetchConfig.AccountFloorFetch.Period) * time.Second).Unix()
+		fetchConfig.refetchRequest = true
+		f.configReceiver <- fetchConfig
+	}
+}
+
+// Stop terminates price floor fetcher
+func (f *PriceFloorFetcher) Stop() {
+	if f == nil {
+		return
+	}
+
+	close(f.done)
+	f.pool.Stop()
+	close(f.configReceiver)
+}
+
+func (f *PriceFloorFetcher) submit(fetchConfig *fetchInfo) {
+	status := f.pool.TrySubmit(func() {
+		f.worker(*fetchConfig)
+	})
+	if !status {
+		heap.Push(&f.fetchQueue, fetchConfig)
+	}
+}
+
+func (f *PriceFloorFetcher) Fetcher() {
+	//Create Ticker of 5 minutes
+	ticker := time.NewTicker(time.Duration(refetchCheckInterval) * time.Second)
+
+	for {
+		select {
+		case fetchConfig := <-f.configReceiver:
+			if fetchConfig.refetchRequest {
+				heap.Push(&f.fetchQueue, &fetchConfig)
+			} else {
+				if _, ok := f.fetchInProgress[fetchConfig.URL]; !ok {
+					f.fetchInProgress[fetchConfig.URL] = true
+					f.submit(&fetchConfig)
+				}
+			}
+		case <-ticker.C:
+			currentTime := f.time.Now().Unix()
+			for top := f.fetchQueue.Top(); top != nil && top.fetchTime <= currentTime; top = f.fetchQueue.Top() {
+				nextFetch := heap.Pop(&f.fetchQueue)
+				f.submit(nextFetch.(*fetchInfo))
+			}
+		case <-f.done:
+			ticker.Stop()
+			glog.Info("Price Floor fetcher terminated")
+			return
+		}
+	}
+}
+
+func (f *PriceFloorFetcher) fetchAndValidate(config config.AccountFloorFetch) (*openrtb_ext.PriceFloorRules, int) {
+	floorResp, maxAge, err := f.fetchFloorRulesFromURL(config)
+	if floorResp == nil || err != nil {
+		glog.Errorf("Error while fetching floor data from URL: %s, reason : %s", config.URL, err.Error())
+		return nil, 0
+	}
+
+	if len(floorResp) > (config.MaxFileSizeKB * 1024) {
+		glog.Errorf("Recieved invalid floor data from URL: %s, reason : floor file size is greater than MaxFileSize", config.URL)
+		return nil, 0
+	}
+
+	var priceFloors openrtb_ext.PriceFloorRules
+	if err = json.Unmarshal(floorResp, &priceFloors.Data); err != nil {
+		glog.Errorf("Recieved invalid price floor json from URL: %s", config.URL)
+		return nil, 0
+	}
+
+	if err := validateRules(config, &priceFloors); err != nil {
+		glog.Errorf("Validation failed for floor JSON from URL: %s, reason: %s", config.URL, err.Error())
+		return nil, 0
+	}
+
+	return &priceFloors, maxAge
+}
+
+// fetchFloorRulesFromURL returns a price floor JSON and time for which this JSON is valid
+// from provided URL with timeout constraints
+func (f *PriceFloorFetcher) fetchFloorRulesFromURL(config config.AccountFloorFetch) ([]byte, int, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(config.Timeout)*time.Millisecond)
+	defer cancel()
+
+	httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, config.URL, nil)
+	if err != nil {
+		return nil, 0, errors.New("error while forming http fetch request : " + err.Error())
+	}
+
+	httpResp, err := f.httpClient.Do(httpReq)
+	if err != nil {
+		return nil, 0, errors.New("error while getting response from url : " + err.Error())
+	}
+
+	if httpResp.StatusCode != http.StatusOK {
+		return nil, 0, errors.New("no response from server")
+	}
+
+	var maxAge int
+	if maxAgeStr := httpResp.Header.Get("max-age"); maxAgeStr != "" {
+		maxAge, err = strconv.Atoi(maxAgeStr)
+		if err != nil {
+			glog.Errorf("max-age in header is malformed for url %s", config.URL)
+		}
+		if maxAge <= config.Period || maxAge > math.MaxInt32 {
+			glog.Errorf("Invalid max-age = %s provided, value should be valid integer and should be within (%v, %v)", maxAgeStr, config.Period, math.MaxInt32)
+		}
+	}
+
+	respBody, err := io.ReadAll(httpResp.Body)
+	if err != nil {
+		return nil, 0, errors.New("unable to read response")
+	}
+	defer httpResp.Body.Close()
+
+	return respBody, maxAge, nil
+}
+
+func validateRules(config config.AccountFloorFetch, priceFloors *openrtb_ext.PriceFloorRules) error {
+	if priceFloors.Data == nil {
+		return errors.New("empty data in floor JSON")
+	}
+
+	if len(priceFloors.Data.ModelGroups) == 0 {
+		return errors.New("no model groups found in price floor data")
+	}
+
+	if priceFloors.Data.SkipRate < 0 || priceFloors.Data.SkipRate > 100 {
+		return errors.New("skip rate should be greater than or equal to 0 and less than 100")
+	}
+
+	for _, modelGroup := range priceFloors.Data.ModelGroups {
+		if len(modelGroup.Values) == 0 || len(modelGroup.Values) > config.MaxRules {
+			return errors.New("invalid number of floor rules, floor rules should be greater than zero and less than MaxRules specified in account config")
+		}
+
+		if modelGroup.ModelWeight != nil && (*modelGroup.ModelWeight < 1 || *modelGroup.ModelWeight > 100) {
+			return errors.New("modelGroup[].modelWeight should be greater than or equal to 1 and less than 100")
+		}
+
+		if modelGroup.SkipRate < 0 || modelGroup.SkipRate > 100 {
+			return errors.New("model group skip rate should be greater than or equal to 0 and less than 100")
+		}
+
+		if modelGroup.Default < 0 {
+			return errors.New("modelGroup.Default should be greater than 0")
+		}
+	}
+
+	return nil
+}
diff --git a/floors/floors.go b/floors/floors.go
index c1e93029..3fdeb4c5 100644
--- a/floors/floors.go
+++ b/floors/floors.go
@@ -4,10 +4,12 @@ import (
 	"errors"
 	"math"
 	"math/rand"
+	"strings"
 
 	"github.com/prebid/prebid-server/v2/config"
 	"github.com/prebid/prebid-server/v2/currency"
 	"github.com/prebid/prebid-server/v2/openrtb_ext"
+	"github.com/prebid/prebid-server/v2/util/ptrutil"
 )
 
 type Price struct {
@@ -30,8 +32,7 @@ const (
 
 // EnrichWithPriceFloors checks for floors enabled in account and request and selects floors data from dynamic fetched if present
 // else selects floors data from req.ext.prebid.floors and update request with selected floors details
-func EnrichWithPriceFloors(bidRequestWrapper *openrtb_ext.RequestWrapper, account config.Account, conversions currency.Conversions) []error {
-
+func EnrichWithPriceFloors(bidRequestWrapper *openrtb_ext.RequestWrapper, account config.Account, conversions currency.Conversions, priceFloorFetcher FloorFetcher) []error {
 	if bidRequestWrapper == nil || bidRequestWrapper.BidRequest == nil {
 		return []error{errors.New("Empty bidrequest")}
 	}
@@ -40,7 +41,7 @@ func EnrichWithPriceFloors(bidRequestWrapper *openrtb_ext.RequestWrapper, accoun
 		return []error{errors.New("Floors feature is disabled at account or in the request")}
 	}
 
-	floors, err := resolveFloors(account, bidRequestWrapper, conversions)
+	floors, err := resolveFloors(account, bidRequestWrapper, conversions, priceFloorFetcher)
 
 	updateReqErrs := updateBidRequestWithFloors(floors, bidRequestWrapper, conversions)
 	updateFloorsInRequest(bidRequestWrapper, floors)
@@ -136,12 +137,26 @@ func isPriceFloorsEnabledForRequest(bidRequestWrapper *openrtb_ext.RequestWrappe
 }
 
 // resolveFloors does selection of floors fields from request data and dynamic fetched data if dynamic fetch is enabled
-func resolveFloors(account config.Account, bidRequestWrapper *openrtb_ext.RequestWrapper, conversions currency.Conversions) (*openrtb_ext.PriceFloorRules, []error) {
+func resolveFloors(account config.Account, bidRequestWrapper *openrtb_ext.RequestWrapper, conversions currency.Conversions, priceFloorFetcher FloorFetcher) (*openrtb_ext.PriceFloorRules, []error) {
 	var errList []error
 	var floorRules *openrtb_ext.PriceFloorRules
 
 	reqFloor := extractFloorsFromRequest(bidRequestWrapper)
-	if reqFloor != nil {
+	if reqFloor != nil && reqFloor.Location != nil && len(reqFloor.Location.URL) > 0 {
+		account.PriceFloors.Fetcher.URL = reqFloor.Location.URL
+	}
+	account.PriceFloors.Fetcher.AccountID = account.ID
+
+	var fetchResult *openrtb_ext.PriceFloorRules
+	var fetchStatus string
+	if priceFloorFetcher != nil && account.PriceFloors.UseDynamicData {
+		fetchResult, fetchStatus = priceFloorFetcher.Fetch(account.PriceFloors)
+	}
+
+	if fetchResult != nil && fetchStatus == openrtb_ext.FetchSuccess {
+		mergedFloor := mergeFloors(reqFloor, fetchResult, conversions)
+		floorRules, errList = createFloorsFrom(mergedFloor, account, fetchStatus, openrtb_ext.FetchLocation)
+	} else if reqFloor != nil {
 		floorRules, errList = createFloorsFrom(reqFloor, account, openrtb_ext.FetchNone, openrtb_ext.RequestLocation)
 	} else {
 		floorRules, errList = createFloorsFrom(nil, account, openrtb_ext.FetchNone, openrtb_ext.NoDataLocation)
@@ -210,3 +225,83 @@ func updateFloorsInRequest(bidRequestWrapper *openrtb_ext.RequestWrapper, priceF
 		bidRequestWrapper.RebuildRequest()
 	}
 }
+
+// resolveFloorMin gets floorMin value from request and dynamic fetched data
+func resolveFloorMin(reqFloors *openrtb_ext.PriceFloorRules, fetchFloors *openrtb_ext.PriceFloorRules, conversions currency.Conversions) Price {
+	var requestFloorMinCur, providerFloorMinCur string
+	var requestFloorMin, providerFloorMin float64
+
+	if reqFloors != nil {
+		requestFloorMin = reqFloors.FloorMin
+		requestFloorMinCur = reqFloors.FloorMinCur
+		if len(requestFloorMinCur) == 0 && reqFloors.Data != nil {
+			requestFloorMinCur = reqFloors.Data.Currency
+		}
+	}
+
+	if fetchFloors != nil {
+		providerFloorMin = fetchFloors.FloorMin
+		providerFloorMinCur = fetchFloors.FloorMinCur
+		if len(providerFloorMinCur) == 0 && fetchFloors.Data != nil {
+			providerFloorMinCur = fetchFloors.Data.Currency
+		}
+	}
+
+	if len(requestFloorMinCur) > 0 {
+		if requestFloorMin > 0 {
+			return Price{FloorMin: requestFloorMin, FloorMinCur: requestFloorMinCur}
+		}
+
+		if providerFloorMin > 0 {
+			if strings.Compare(providerFloorMinCur, requestFloorMinCur) == 0 || len(providerFloorMinCur) == 0 {
+				return Price{FloorMin: providerFloorMin, FloorMinCur: requestFloorMinCur}
+			}
+			rate, err := conversions.GetRate(providerFloorMinCur, requestFloorMinCur)
+			if err != nil {
+				return Price{FloorMin: 0, FloorMinCur: requestFloorMinCur}
+			}
+			return Price{FloorMin: roundToFourDecimals(rate * providerFloorMin), FloorMinCur: requestFloorMinCur}
+		}
+	}
+
+	if len(providerFloorMinCur) > 0 {
+		if providerFloorMin > 0 {
+			return Price{FloorMin: providerFloorMin, FloorMinCur: providerFloorMinCur}
+		}
+		if requestFloorMin > 0 {
+			return Price{FloorMin: requestFloorMin, FloorMinCur: providerFloorMinCur}
+		}
+	}
+
+	return Price{FloorMin: requestFloorMin, FloorMinCur: requestFloorMinCur}
+
+}
+
+// mergeFloors does merging for floors data from request and dynamic fetch
+func mergeFloors(reqFloors *openrtb_ext.PriceFloorRules, fetchFloors *openrtb_ext.PriceFloorRules, conversions currency.Conversions) *openrtb_ext.PriceFloorRules {
+	mergedFloors := fetchFloors.DeepCopy()
+	if mergedFloors.Enabled == nil {
+		mergedFloors.Enabled = new(bool)
+	}
+	*mergedFloors.Enabled = fetchFloors.GetEnabled() && reqFloors.GetEnabled()
+
+	if reqFloors == nil {
+		return mergedFloors
+	}
+
+	if reqFloors.Enforcement != nil {
+		mergedFloors.Enforcement = reqFloors.Enforcement.DeepCopy()
+	}
+
+	floorMinPrice := resolveFloorMin(reqFloors, fetchFloors, conversions)
+	if floorMinPrice.FloorMin > 0 {
+		mergedFloors.FloorMin = floorMinPrice.FloorMin
+		mergedFloors.FloorMinCur = floorMinPrice.FloorMinCur
+	}
+
+	if reqFloors != nil && reqFloors.Location != nil && reqFloors.Location.URL != "" {
+		mergedFloors.Location = ptrutil.Clone(reqFloors.Location)
+	}
+
+	return mergedFloors
+}
diff --git a/floors/rule.go b/floors/rule.go
index db12719c..50c12462 100644
--- a/floors/rule.go
+++ b/floors/rule.go
@@ -38,8 +38,7 @@ const (
 // getFloorCurrency returns floors currency provided in floors JSON,
 // if currency is not provided then defaults to USD
 func getFloorCurrency(floorExt *openrtb_ext.PriceFloorRules) string {
-	var floorCur string
-
+	floorCur := defaultCurrency
 	if floorExt != nil && floorExt.Data != nil {
 		if floorExt.Data.Currency != "" {
 			floorCur = floorExt.Data.Currency
@@ -50,10 +49,6 @@ func getFloorCurrency(floorExt *openrtb_ext.PriceFloorRules) string {
 		}
 	}
 
-	if len(floorCur) == 0 {
-		floorCur = defaultCurrency
-	}
-
 	return floorCur
 }
 
diff --git a/go.mod b/go.mod
index 912cdaa2..ac394a67 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,7 @@ require (
 	github.com/DATA-DOG/go-sqlmock v1.5.0
 	github.com/IABTechLab/adscert v0.34.0
 	github.com/NYTimes/gziphandler v1.1.1
+	github.com/alitto/pond v1.8.3
 	github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
 	github.com/benbjohnson/clock v1.3.0
 	github.com/buger/jsonparser v1.1.1
@@ -14,7 +15,8 @@ require (
 	github.com/docker/go-units v0.4.0
 	github.com/go-sql-driver/mysql v1.6.0
 	github.com/gofrs/uuid v4.2.0+incompatible
-	github.com/golang/glog v1.0.0
+	github.com/golang/glog v1.1.0
+	github.com/json-iterator/go v1.1.12
 	github.com/julienschmidt/httprouter v1.3.0
 	github.com/lib/pq v1.10.4
 	github.com/mitchellh/copystructure v1.2.0
@@ -31,9 +33,9 @@ require (
 	github.com/vrischmann/go-metrics-influxdb v0.1.1
 	github.com/xeipuuv/gojsonschema v1.2.0
 	github.com/yudai/gojsondiff v1.0.0
-	golang.org/x/net v0.7.0
-	golang.org/x/text v0.7.0
-	google.golang.org/grpc v1.53.0
+	golang.org/x/net v0.17.0
+	golang.org/x/text v0.13.0
+	google.golang.org/grpc v1.56.3
 	gopkg.in/evanphx/json-patch.v4 v4.12.0
 	gopkg.in/yaml.v3 v3.0.1
 )
@@ -43,10 +45,9 @@ require (
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/fsnotify/fsnotify v1.5.4 // indirect
-	github.com/golang/protobuf v1.5.2 // indirect
+	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d // indirect
-	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/magiconair/properties v1.8.6 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -69,10 +70,10 @@ require (
 	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
 	github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
 	github.com/yudai/pp v2.0.1+incompatible // indirect
-	golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect
-	golang.org/x/sys v0.5.0 // indirect
-	google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
-	google.golang.org/protobuf v1.28.1 // indirect
+	golang.org/x/crypto v0.14.0 // indirect
+	golang.org/x/sys v0.13.0 // indirect
+	google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
+	google.golang.org/protobuf v1.30.0 // indirect
 	gopkg.in/ini.v1 v1.66.4 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 )
diff --git a/go.sum b/go.sum
index eefe0228..6331beb3 100644
--- a/go.sum
+++ b/go.sum
@@ -64,6 +64,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
+github.com/alitto/pond v1.8.3 h1:ydIqygCLVPqIX/USe5EaV/aSRXTRXDEI9JwuDdu+/xs=
+github.com/alitto/pond v1.8.3/go.mod h1:CmvIIGd5jKLasGI3D87qDkQxjzChdKMmnXMg3fG6M6Q=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
@@ -160,8 +162,8 @@ github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
-github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
+github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE=
+github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -191,8 +193,9 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
 github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
-github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -527,8 +530,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm
 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA=
-golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
+golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -613,8 +616,8 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx
 golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
-golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -725,8 +728,8 @@ golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/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=
@@ -739,8 +742,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -911,8 +914,8 @@ google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ6
 google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
 google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
 google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w=
-google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
+google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
+google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
 google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
@@ -943,8 +946,8 @@ google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K
 google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
 google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
 google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
-google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
-google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
+google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
+google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -959,8 +962,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
-google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
+google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/hooks/hookexecution/executor.go b/hooks/hookexecution/executor.go
index dd6953fb..f820b79f 100644
--- a/hooks/hookexecution/executor.go
+++ b/hooks/hookexecution/executor.go
@@ -34,7 +34,7 @@ type StageExecutor interface {
 	ExecuteEntrypointStage(req *http.Request, body []byte) ([]byte, *RejectError)
 	ExecuteRawAuctionStage(body []byte) ([]byte, *RejectError)
 	ExecuteProcessedAuctionStage(req *openrtb_ext.RequestWrapper) error
-	ExecuteBidderRequestStage(req *openrtb2.BidRequest, bidder string) *RejectError
+	ExecuteBidderRequestStage(req *openrtb_ext.RequestWrapper, bidder string) *RejectError
 	ExecuteRawBidderResponseStage(response *adapters.BidderResponse, bidder string) *RejectError
 	ExecuteAllProcessedBidResponsesStage(adapterBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid)
 	ExecuteAuctionResponseStage(response *openrtb2.BidResponse)
@@ -160,7 +160,7 @@ func (e *hookExecutor) ExecuteProcessedAuctionStage(request *openrtb_ext.Request
 
 	stageName := hooks.StageProcessedAuctionRequest.String()
 	executionCtx := e.newContext(stageName)
-	payload := hookstage.ProcessedAuctionRequestPayload{RequestWrapper: request}
+	payload := hookstage.ProcessedAuctionRequestPayload{Request: request}
 
 	outcome, _, contexts, reject := executeStage(executionCtx, plan, payload, handler, e.metricEngine)
 	outcome.Entity = entityAuctionRequest
@@ -177,7 +177,7 @@ func (e *hookExecutor) ExecuteProcessedAuctionStage(request *openrtb_ext.Request
 	return reject
 }
 
-func (e *hookExecutor) ExecuteBidderRequestStage(req *openrtb2.BidRequest, bidder string) *RejectError {
+func (e *hookExecutor) ExecuteBidderRequestStage(req *openrtb_ext.RequestWrapper, bidder string) *RejectError {
 	plan := e.planBuilder.PlanForBidderRequestStage(e.endpoint, e.account)
 	if len(plan) == 0 {
 		return nil
@@ -194,7 +194,7 @@ func (e *hookExecutor) ExecuteBidderRequestStage(req *openrtb2.BidRequest, bidde
 
 	stageName := hooks.StageBidderRequest.String()
 	executionCtx := e.newContext(stageName)
-	payload := hookstage.BidderRequestPayload{BidRequest: req, Bidder: bidder}
+	payload := hookstage.BidderRequestPayload{Request: req, Bidder: bidder}
 	outcome, payload, contexts, reject := executeStage(executionCtx, plan, payload, handler, e.metricEngine)
 	outcome.Entity = entity(bidder)
 	outcome.Stage = stageName
@@ -332,7 +332,7 @@ func (executor EmptyHookExecutor) ExecuteProcessedAuctionStage(_ *openrtb_ext.Re
 	return nil
 }
 
-func (executor EmptyHookExecutor) ExecuteBidderRequestStage(_ *openrtb2.BidRequest, bidder string) *RejectError {
+func (executor EmptyHookExecutor) ExecuteBidderRequestStage(_ *openrtb_ext.RequestWrapper, bidder string) *RejectError {
 	return nil
 }
 
diff --git a/hooks/hookstage/bidderrequest.go b/hooks/hookstage/bidderrequest.go
index 6609a103..af480c54 100644
--- a/hooks/hookstage/bidderrequest.go
+++ b/hooks/hookstage/bidderrequest.go
@@ -2,8 +2,7 @@ package hookstage
 
 import (
 	"context"
-
-	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/v2/openrtb_ext"
 )
 
 // BidderRequest hooks are invoked for each bidder participating in auction.
@@ -25,6 +24,6 @@ type BidderRequest interface {
 // distilled for the particular bidder.
 // Hooks are allowed to modify openrtb2.BidRequest using mutations.
 type BidderRequestPayload struct {
-	BidRequest *openrtb2.BidRequest
-	Bidder     string
+	Request *openrtb_ext.RequestWrapper
+	Bidder  string
 }
diff --git a/hooks/hookstage/bidderrequest_mutations.go b/hooks/hookstage/bidderrequest_mutations.go
index 6dfd1c64..746878cb 100644
--- a/hooks/hookstage/bidderrequest_mutations.go
+++ b/hooks/hookstage/bidderrequest_mutations.go
@@ -2,9 +2,9 @@ package hookstage
 
 import (
 	"errors"
+	"github.com/prebid/prebid-server/v2/openrtb_ext"
 
 	"github.com/prebid/openrtb/v19/adcom1"
-	"github.com/prebid/openrtb/v19/openrtb2"
 )
 
 func (c *ChangeSet[T]) BidderRequest() ChangeSetBidderRequest[T] {
@@ -31,12 +31,12 @@ func (c ChangeSetBidderRequest[T]) BApp() ChangeSetBApp[T] {
 	return ChangeSetBApp[T]{changeSetBidderRequest: c}
 }
 
-func (c ChangeSetBidderRequest[T]) castPayload(p T) (*openrtb2.BidRequest, error) {
+func (c ChangeSetBidderRequest[T]) castPayload(p T) (*openrtb_ext.RequestWrapper, error) {
 	if payload, ok := any(p).(BidderRequestPayload); ok {
-		if payload.BidRequest == nil {
-			return nil, errors.New("empty BidRequest provided")
+		if payload.Request == nil || payload.Request.BidRequest == nil {
+			return nil, errors.New("payload contains a nil bid request")
 		}
-		return payload.BidRequest, nil
+		return payload.Request, nil
 	}
 	return nil, errors.New("failed to cast BidderRequestPayload")
 }
diff --git a/hooks/hookstage/processedauctionrequest.go b/hooks/hookstage/processedauctionrequest.go
index 9ee06fab..f4205613 100644
--- a/hooks/hookstage/processedauctionrequest.go
+++ b/hooks/hookstage/processedauctionrequest.go
@@ -26,5 +26,5 @@ type ProcessedAuctionRequest interface {
 // ProcessedAuctionRequestPayload consists of the openrtb_ext.RequestWrapper object.
 // Hooks are allowed to modify openrtb_ext.RequestWrapper using mutations.
 type ProcessedAuctionRequestPayload struct {
-	RequestWrapper *openrtb_ext.RequestWrapper
+	Request *openrtb_ext.RequestWrapper
 }
diff --git a/modules/prebid/ortb2blocking/hook_bidderrequest.go b/modules/prebid/ortb2blocking/hook_bidderrequest.go
index 0bd990eb..602e75ae 100644
--- a/modules/prebid/ortb2blocking/hook_bidderrequest.go
+++ b/modules/prebid/ortb2blocking/hook_bidderrequest.go
@@ -16,11 +16,11 @@ func handleBidderRequestHook(
 	cfg config,
 	payload hookstage.BidderRequestPayload,
 ) (result hookstage.HookResult[hookstage.BidderRequestPayload], err error) {
-	if payload.BidRequest == nil {
-		return result, hookexecution.NewFailure("empty BidRequest provided")
+	if payload.Request == nil || payload.Request.BidRequest == nil {
+		return result, hookexecution.NewFailure("payload contains a nil bid request")
 	}
 
-	mediaTypes := mediaTypesFrom(payload.BidRequest)
+	mediaTypes := mediaTypesFrom(payload.Request.BidRequest)
 	changeSet := hookstage.ChangeSet[hookstage.BidderRequestPayload]{}
 	blockingAttributes := blockingAttributes{}
 
@@ -60,7 +60,7 @@ func updateBAdv(
 	result *hookstage.HookResult[hookstage.BidderRequestPayload],
 	changeSet *hookstage.ChangeSet[hookstage.BidderRequestPayload],
 ) (err error) {
-	if len(payload.BidRequest.BAdv) > 0 {
+	if len(payload.Request.BAdv) > 0 {
 		return nil
 	}
 
@@ -87,7 +87,7 @@ func updateBApp(
 	result *hookstage.HookResult[hookstage.BidderRequestPayload],
 	changeSet *hookstage.ChangeSet[hookstage.BidderRequestPayload],
 ) (err error) {
-	if len(payload.BidRequest.BApp) > 0 {
+	if len(payload.Request.BApp) > 0 {
 		return nil
 	}
 
@@ -114,7 +114,7 @@ func updateBCat(
 	result *hookstage.HookResult[hookstage.BidderRequestPayload],
 	changeSet *hookstage.ChangeSet[hookstage.BidderRequestPayload],
 ) (err error) {
-	if len(payload.BidRequest.BCat) > 0 {
+	if len(payload.Request.BCat) > 0 {
 		return nil
 	}
 
@@ -191,7 +191,7 @@ func updateCatTax(
 	attributes *blockingAttributes,
 	changeSet *hookstage.ChangeSet[hookstage.BidderRequestPayload],
 ) {
-	if payload.BidRequest.CatTax > 0 {
+	if payload.Request.CatTax > 0 {
 		return
 	}
 
@@ -226,7 +226,7 @@ func mutationForImp(
 	impUpdater impUpdateFunc,
 ) hookstage.MutationFunc[hookstage.BidderRequestPayload] {
 	return func(payload hookstage.BidderRequestPayload) (hookstage.BidderRequestPayload, error) {
-		for i, imp := range payload.BidRequest.Imp {
+		for i, imp := range payload.Request.Imp {
 			if values, ok := valuesByImp[imp.ID]; ok {
 				if len(values) == 0 {
 					continue
@@ -236,7 +236,7 @@ func mutationForImp(
 					imp.Banner = &openrtb2.Banner{}
 				}
 
-				payload.BidRequest.Imp[i] = impUpdater(imp, values)
+				payload.Request.Imp[i] = impUpdater(imp, values)
 			}
 		}
 		return payload, nil
@@ -310,7 +310,7 @@ func findImpressionOverrides(
 	overrides := map[string][]int{}
 	messages := []string{}
 
-	for _, imp := range payload.BidRequest.Imp {
+	for _, imp := range payload.Request.Imp {
 		// do not add override for attribute if it already exists in request
 		if isAttrPresent(imp) {
 			continue
diff --git a/openrtb_ext/floors.go b/openrtb_ext/floors.go
index 3553946c..0e773c65 100644
--- a/openrtb_ext/floors.go
+++ b/openrtb_ext/floors.go
@@ -1,5 +1,11 @@
 package openrtb_ext
 
+import (
+	"github.com/prebid/prebid-server/v2/util/maputil"
+	"github.com/prebid/prebid-server/v2/util/ptrutil"
+	"github.com/prebid/prebid-server/v2/util/sliceutil"
+)
+
 // Defines strings for FetchStatus
 const (
 	FetchSuccess    = "success"
@@ -145,3 +151,59 @@ type ExtImp struct {
 type ImpExtPrebid struct {
 	Floors Price `json:"floors,omitempty"`
 }
+
+func (pf *PriceFloorRules) DeepCopy() *PriceFloorRules {
+	if pf == nil {
+		return nil
+	}
+
+	newRules := *pf
+	newRules.Enabled = ptrutil.Clone(pf.Enabled)
+	newRules.Skipped = ptrutil.Clone(pf.Skipped)
+	newRules.Location = ptrutil.Clone(pf.Location)
+	newRules.Data = pf.Data.DeepCopy()
+	newRules.Enforcement = pf.Enforcement.DeepCopy()
+
+	return &newRules
+}
+
+func (data *PriceFloorData) DeepCopy() *PriceFloorData {
+	if data == nil {
+		return nil
+	}
+
+	newData := *data
+	newModelGroups := make([]PriceFloorModelGroup, len(data.ModelGroups))
+
+	for i := range data.ModelGroups {
+		var eachGroup PriceFloorModelGroup
+		eachGroup.Currency = data.ModelGroups[i].Currency
+		eachGroup.ModelWeight = ptrutil.Clone(data.ModelGroups[i].ModelWeight)
+		eachGroup.ModelVersion = data.ModelGroups[i].ModelVersion
+		eachGroup.SkipRate = data.ModelGroups[i].SkipRate
+		eachGroup.Values = maputil.Clone(data.ModelGroups[i].Values)
+		eachGroup.Default = data.ModelGroups[i].Default
+		eachGroup.Schema = PriceFloorSchema{
+			Fields:    sliceutil.Clone(data.ModelGroups[i].Schema.Fields),
+			Delimiter: data.ModelGroups[i].Schema.Delimiter,
+		}
+		newModelGroups[i] = eachGroup
+	}
+	newData.ModelGroups = newModelGroups
+
+	return &newData
+}
+
+func (enforcement *PriceFloorEnforcement) DeepCopy() *PriceFloorEnforcement {
+	if enforcement == nil {
+		return nil
+	}
+
+	newEnforcement := *enforcement
+	newEnforcement.EnforceJS = ptrutil.Clone(enforcement.EnforceJS)
+	newEnforcement.EnforcePBS = ptrutil.Clone(enforcement.EnforcePBS)
+	newEnforcement.FloorDeals = ptrutil.Clone(enforcement.FloorDeals)
+	newEnforcement.BidAdjustment = ptrutil.Clone(enforcement.BidAdjustment)
+
+	return &newEnforcement
+}
diff --git a/router/router.go b/router/router.go
index 0ee9fc7c..99fe2e5d 100644
--- a/router/router.go
+++ b/router/router.go
@@ -20,6 +20,7 @@ import (
 	"github.com/prebid/prebid-server/v2/errortypes"
 	"github.com/prebid/prebid-server/v2/exchange"
 	"github.com/prebid/prebid-server/v2/experiment/adscert"
+	"github.com/prebid/prebid-server/v2/floors"
 	"github.com/prebid/prebid-server/v2/gdpr"
 	"github.com/prebid/prebid-server/v2/hooks"
 	"github.com/prebid/prebid-server/v2/macros"
@@ -92,7 +93,7 @@ func newJsonDirectoryServer(schemaDirectory string, validator openrtb_ext.Bidder
 		data[aliasName] = bidderData
 	}
 
-	response, err := jsonutil.Marshal(data)
+	response, err := json.Marshal(data)
 	if err != nil {
 		glog.Fatalf("Failed to marshal bidder param JSON-schema: %v", err)
 	}
@@ -162,6 +163,16 @@ func New(cfg *config.Configuration, rateConvertor *currency.RateConverter) (r *R
 		},
 	}
 
+	floorFechterHttpClient := &http.Client{
+		Transport: &http.Transport{
+			Proxy:               http.ProxyFromEnvironment,
+			MaxConnsPerHost:     cfg.PriceFloors.Fetcher.HttpClient.MaxConnsPerHost,
+			MaxIdleConns:        cfg.PriceFloors.Fetcher.HttpClient.MaxIdleConns,
+			MaxIdleConnsPerHost: cfg.PriceFloors.Fetcher.HttpClient.MaxIdleConnsPerHost,
+			IdleConnTimeout:     time.Duration(cfg.PriceFloors.Fetcher.HttpClient.IdleConnTimeout) * time.Second,
+		},
+	}
+
 	if err := checkSupportedUserSyncEndpoints(cfg.BidderInfos); err != nil {
 		return nil, err
 	}
@@ -224,10 +235,12 @@ func New(cfg *config.Configuration, rateConvertor *currency.RateConverter) (r *R
 		glog.Fatalf("Failed to create ads cert signer: %v", err)
 	}
 
+	priceFloorFetcher := floors.NewPriceFloorFetcher(cfg.PriceFloors, floorFechterHttpClient, r.MetricsEngine)
+
 	tmaxAdjustments := exchange.ProcessTMaxAdjustments(cfg.TmaxAdjustments)
 	planBuilder := hooks.NewExecutionPlanBuilder(cfg.Hooks, repo)
 	macroReplacer := macros.NewStringIndexBasedReplacer()
-	theExchange := exchange.NewExchange(adapters, cacheClient, cfg, syncersByBidder, r.MetricsEngine, cfg.BidderInfos, gdprPermsBuilder, rateConvertor, categoriesFetcher, adsCertSigner, macroReplacer)
+	theExchange := exchange.NewExchange(adapters, cacheClient, cfg, syncersByBidder, r.MetricsEngine, cfg.BidderInfos, gdprPermsBuilder, rateConvertor, categoriesFetcher, adsCertSigner, macroReplacer, priceFloorFetcher)
 	var uuidGenerator uuidutil.UUIDRandomGenerator
 	openrtbEndpoint, err := openrtb2.NewEndpoint(uuidGenerator, theExchange, paramsValidator, fetcher, accounts, cfg, r.MetricsEngine, analyticsRunner, disabledBidders, defReqJSON, activeBidders, storedRespFetcher, planBuilder, tmaxAdjustments)
 	if err != nil {


Test patch:
diff --git a/config/account_test.go b/config/account_test.go
index 648618f2..65e7f3e8 100644
--- a/config/account_test.go
+++ b/config/account_test.go
@@ -795,7 +795,6 @@ func TestModulesGetConfig(t *testing.T) {
 }
 
 func TestAccountPriceFloorsValidate(t *testing.T) {
-
 	tests := []struct {
 		description string
 		pf          *AccountPriceFloors
@@ -807,12 +806,22 @@ func TestAccountPriceFloorsValidate(t *testing.T) {
 				EnforceFloorsRate: 100,
 				MaxRule:           200,
 				MaxSchemaDims:     10,
+				Fetcher: AccountFloorFetch{
+					Period:  300,
+					MaxAge:  600,
+					Timeout: 12,
+				},
 			},
 		},
 		{
 			description: "Invalid configuration: EnforceFloorRate:110",
 			pf: &AccountPriceFloors{
 				EnforceFloorsRate: 110,
+				Fetcher: AccountFloorFetch{
+					Period:  300,
+					MaxAge:  600,
+					Timeout: 12,
+				},
 			},
 			want: []error{errors.New("account_defaults.price_floors.enforce_floors_rate should be between 0 and 100")},
 		},
@@ -820,6 +829,11 @@ func TestAccountPriceFloorsValidate(t *testing.T) {
 			description: "Invalid configuration: EnforceFloorRate:-10",
 			pf: &AccountPriceFloors{
 				EnforceFloorsRate: -10,
+				Fetcher: AccountFloorFetch{
+					Period:  300,
+					MaxAge:  600,
+					Timeout: 12,
+				},
 			},
 			want: []error{errors.New("account_defaults.price_floors.enforce_floors_rate should be between 0 and 100")},
 		},
@@ -827,6 +841,11 @@ func TestAccountPriceFloorsValidate(t *testing.T) {
 			description: "Invalid configuration: MaxRule:-20",
 			pf: &AccountPriceFloors{
 				MaxRule: -20,
+				Fetcher: AccountFloorFetch{
+					Period:  300,
+					MaxAge:  600,
+					Timeout: 12,
+				},
 			},
 			want: []error{errors.New("account_defaults.price_floors.max_rules should be between 0 and 2147483647")},
 		},
@@ -834,9 +853,116 @@ func TestAccountPriceFloorsValidate(t *testing.T) {
 			description: "Invalid configuration: MaxSchemaDims:100",
 			pf: &AccountPriceFloors{
 				MaxSchemaDims: 100,
+				Fetcher: AccountFloorFetch{
+					Period:  300,
+					MaxAge:  600,
+					Timeout: 12,
+				},
 			},
 			want: []error{errors.New("account_defaults.price_floors.max_schema_dims should be between 0 and 20")},
 		},
+		{
+			description: "Invalid period for fetch",
+			pf: &AccountPriceFloors{
+				EnforceFloorsRate: 100,
+				MaxRule:           200,
+				MaxSchemaDims:     10,
+				Fetcher: AccountFloorFetch{
+					Period:  100,
+					MaxAge:  600,
+					Timeout: 12,
+				},
+			},
+			want: []error{errors.New("account_defaults.price_floors.fetch.period_sec should not be less than 300 seconds")},
+		},
+		{
+			description: "Invalid max age for fetch",
+			pf: &AccountPriceFloors{
+				EnforceFloorsRate: 100,
+				MaxRule:           200,
+				MaxSchemaDims:     10,
+				Fetcher: AccountFloorFetch{
+					Period:  300,
+					MaxAge:  500,
+					Timeout: 12,
+				},
+			},
+			want: []error{errors.New("account_defaults.price_floors.fetch.max_age_sec should not be less than 600 seconds and greater than maximum integer value")},
+		},
+		{
+			description: "Period is greater than max age",
+			pf: &AccountPriceFloors{
+				EnforceFloorsRate: 100,
+				MaxRule:           200,
+				MaxSchemaDims:     10,
+				Fetcher: AccountFloorFetch{
+					Period:  700,
+					MaxAge:  600,
+					Timeout: 12,
+				},
+			},
+			want: []error{errors.New("account_defaults.price_floors.fetch.period_sec should be less than account_defaults.price_floors.fetch.max_age_sec")},
+		},
+		{
+			description: "Invalid timeout",
+			pf: &AccountPriceFloors{
+				EnforceFloorsRate: 100,
+				MaxRule:           200,
+				MaxSchemaDims:     10,
+				Fetcher: AccountFloorFetch{
+					Period:  300,
+					MaxAge:  600,
+					Timeout: 4,
+				},
+			},
+			want: []error{errors.New("account_defaults.price_floors.fetch.timeout_ms should be between 10 to 10,000 miliseconds")},
+		},
+		{
+			description: "Invalid Max Rules",
+			pf: &AccountPriceFloors{
+				EnforceFloorsRate: 100,
+				MaxRule:           200,
+				MaxSchemaDims:     10,
+				Fetcher: AccountFloorFetch{
+					Period:   300,
+					MaxAge:   600,
+					Timeout:  12,
+					MaxRules: -2,
+				},
+			},
+			want: []error{errors.New("account_defaults.price_floors.fetch.max_rules should be greater than or equal to 0")},
+		},
+		{
+			description: "Invalid Max File size",
+			pf: &AccountPriceFloors{
+				EnforceFloorsRate: 100,
+				MaxRule:           200,
+				MaxSchemaDims:     10,
+				Fetcher: AccountFloorFetch{
+					Period:        300,
+					MaxAge:        600,
+					Timeout:       12,
+					MaxFileSizeKB: -1,
+				},
+			},
+			want: []error{errors.New("account_defaults.price_floors.fetch.max_file_size_kb should be greater than or equal to 0")},
+		},
+		{
+			description: "Invalid max_schema_dims",
+			pf: &AccountPriceFloors{
+				EnforceFloorsRate: 100,
+				MaxRule:           200,
+				MaxSchemaDims:     10,
+				Fetcher: AccountFloorFetch{
+					Period:        300,
+					MaxAge:        600,
+					Timeout:       12,
+					MaxFileSizeKB: 10,
+					MaxSchemaDims: 40,
+				},
+			},
+			want: []error{errors.New("account_defaults.price_floors.fetch.max_schema_dims should not be less than 0 and greater than 20")},
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.description, func(t *testing.T) {
diff --git a/config/bidderinfo_test.go b/config/bidderinfo_test.go
index ad242bd0..ee1b35d4 100644
--- a/config/bidderinfo_test.go
+++ b/config/bidderinfo_test.go
@@ -1490,8 +1490,12 @@ func TestSyncerEndpointOverride(t *testing.T) {
 func TestApplyBidderInfoConfigSyncerOverrides(t *testing.T) {
 	var (
 		givenFileSystem = BidderInfos{"a": {Syncer: &Syncer{Key: "original"}}}
-		givenConfig     = BidderInfos{"a": {Syncer: &Syncer{Key: "override"}}}
-		expected        = BidderInfos{"a": {Syncer: &Syncer{Key: "override"}}}
+		givenConfig     = nillableFieldBidderInfos{
+			"a": {
+				bidderInfo: BidderInfo{Syncer: &Syncer{Key: "override"}},
+			},
+		}
+		expected = BidderInfos{"a": {Syncer: &Syncer{Key: "override"}}}
 	)
 
 	result, resultErr := applyBidderInfoConfigOverrides(givenConfig, givenFileSystem, mockNormalizeBidderName)
@@ -1500,47 +1504,49 @@ func TestApplyBidderInfoConfigSyncerOverrides(t *testing.T) {
 }
 
 func TestApplyBidderInfoConfigOverrides(t *testing.T) {
+	falseValue := false
+
 	var testCases = []struct {
 		description            string
 		givenFsBidderInfos     BidderInfos
-		givenConfigBidderInfos BidderInfos
+		givenConfigBidderInfos nillableFieldBidderInfos
 		expectedError          string
 		expectedBidderInfos    BidderInfos
 	}{
 		{
 			description:            "Don't override endpoint",
 			givenFsBidderInfos:     BidderInfos{"a": {Endpoint: "original"}},
-			givenConfigBidderInfos: BidderInfos{"a": {Syncer: &Syncer{Key: "override"}}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{Syncer: &Syncer{Key: "override"}}}},
 			expectedBidderInfos:    BidderInfos{"a": {Endpoint: "original", Syncer: &Syncer{Key: "override"}}},
 		},
 		{
 			description:            "Override endpoint",
 			givenFsBidderInfos:     BidderInfos{"a": {Endpoint: "original"}},
-			givenConfigBidderInfos: BidderInfos{"a": {Endpoint: "override", Syncer: &Syncer{Key: "override"}}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{Endpoint: "override", Syncer: &Syncer{Key: "override"}}}},
 			expectedBidderInfos:    BidderInfos{"a": {Endpoint: "override", Syncer: &Syncer{Key: "override"}}},
 		},
 		{
 			description:            "Don't override ExtraAdapterInfo",
 			givenFsBidderInfos:     BidderInfos{"a": {ExtraAdapterInfo: "original"}},
-			givenConfigBidderInfos: BidderInfos{"a": {Syncer: &Syncer{Key: "override"}}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{Syncer: &Syncer{Key: "override"}}}},
 			expectedBidderInfos:    BidderInfos{"a": {ExtraAdapterInfo: "original", Syncer: &Syncer{Key: "override"}}},
 		},
 		{
 			description:            "Override ExtraAdapterInfo",
 			givenFsBidderInfos:     BidderInfos{"a": {ExtraAdapterInfo: "original"}},
-			givenConfigBidderInfos: BidderInfos{"a": {ExtraAdapterInfo: "override", Syncer: &Syncer{Key: "override"}}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{ExtraAdapterInfo: "override", Syncer: &Syncer{Key: "override"}}}},
 			expectedBidderInfos:    BidderInfos{"a": {ExtraAdapterInfo: "override", Syncer: &Syncer{Key: "override"}}},
 		},
 		{
 			description:            "Don't override Maintainer",
 			givenFsBidderInfos:     BidderInfos{"a": {Maintainer: &MaintainerInfo{Email: "original"}}},
-			givenConfigBidderInfos: BidderInfos{"a": {Syncer: &Syncer{Key: "override"}}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{Syncer: &Syncer{Key: "override"}}}},
 			expectedBidderInfos:    BidderInfos{"a": {Maintainer: &MaintainerInfo{Email: "original"}, Syncer: &Syncer{Key: "override"}}},
 		},
 		{
 			description:            "Override maintainer",
 			givenFsBidderInfos:     BidderInfos{"a": {Maintainer: &MaintainerInfo{Email: "original"}}},
-			givenConfigBidderInfos: BidderInfos{"a": {Maintainer: &MaintainerInfo{Email: "override"}, Syncer: &Syncer{Key: "override"}}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{Maintainer: &MaintainerInfo{Email: "override"}, Syncer: &Syncer{Key: "override"}}}},
 			expectedBidderInfos:    BidderInfos{"a": {Maintainer: &MaintainerInfo{Email: "override"}, Syncer: &Syncer{Key: "override"}}},
 		},
 		{
@@ -1548,7 +1554,7 @@ func TestApplyBidderInfoConfigOverrides(t *testing.T) {
 			givenFsBidderInfos: BidderInfos{"a": {
 				Capabilities: &CapabilitiesInfo{App: &PlatformInfo{MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeVideo}}},
 			}},
-			givenConfigBidderInfos: BidderInfos{"a": {Syncer: &Syncer{Key: "override"}}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{Syncer: &Syncer{Key: "override"}}}},
 			expectedBidderInfos: BidderInfos{"a": {
 				Syncer:       &Syncer{Key: "override"},
 				Capabilities: &CapabilitiesInfo{App: &PlatformInfo{MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeVideo}}},
@@ -1559,10 +1565,10 @@ func TestApplyBidderInfoConfigOverrides(t *testing.T) {
 			givenFsBidderInfos: BidderInfos{"a": {
 				Capabilities: &CapabilitiesInfo{App: &PlatformInfo{MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeVideo}}},
 			}},
-			givenConfigBidderInfos: BidderInfos{"a": {
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{
 				Syncer:       &Syncer{Key: "override"},
 				Capabilities: &CapabilitiesInfo{App: &PlatformInfo{MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner}}},
-			}},
+			}}},
 			expectedBidderInfos: BidderInfos{"a": {
 				Syncer:       &Syncer{Key: "override"},
 				Capabilities: &CapabilitiesInfo{App: &PlatformInfo{MediaTypes: []openrtb_ext.BidType{openrtb_ext.BidTypeBanner}}},
@@ -1571,25 +1577,25 @@ func TestApplyBidderInfoConfigOverrides(t *testing.T) {
 		{
 			description:            "Don't override Debug",
 			givenFsBidderInfos:     BidderInfos{"a": {Debug: &DebugInfo{Allow: true}}},
-			givenConfigBidderInfos: BidderInfos{"a": {Syncer: &Syncer{Key: "override"}}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{Syncer: &Syncer{Key: "override"}}}},
 			expectedBidderInfos:    BidderInfos{"a": {Debug: &DebugInfo{Allow: true}, Syncer: &Syncer{Key: "override"}}},
 		},
 		{
 			description:            "Override Debug",
 			givenFsBidderInfos:     BidderInfos{"a": {Debug: &DebugInfo{Allow: true}}},
-			givenConfigBidderInfos: BidderInfos{"a": {Debug: &DebugInfo{Allow: false}, Syncer: &Syncer{Key: "override"}}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{Debug: &DebugInfo{Allow: false}, Syncer: &Syncer{Key: "override"}}}},
 			expectedBidderInfos:    BidderInfos{"a": {Debug: &DebugInfo{Allow: false}, Syncer: &Syncer{Key: "override"}}},
 		},
 		{
 			description:            "Don't override GVLVendorID",
 			givenFsBidderInfos:     BidderInfos{"a": {GVLVendorID: 5}},
-			givenConfigBidderInfos: BidderInfos{"a": {Syncer: &Syncer{Key: "override"}}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{Syncer: &Syncer{Key: "override"}}}},
 			expectedBidderInfos:    BidderInfos{"a": {GVLVendorID: 5, Syncer: &Syncer{Key: "override"}}},
 		},
 		{
 			description:            "Override GVLVendorID",
 			givenFsBidderInfos:     BidderInfos{"a": {}},
-			givenConfigBidderInfos: BidderInfos{"a": {GVLVendorID: 5, Syncer: &Syncer{Key: "override"}}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{GVLVendorID: 5, Syncer: &Syncer{Key: "override"}}}},
 			expectedBidderInfos:    BidderInfos{"a": {GVLVendorID: 5, Syncer: &Syncer{Key: "override"}}},
 		},
 		{
@@ -1597,7 +1603,7 @@ func TestApplyBidderInfoConfigOverrides(t *testing.T) {
 			givenFsBidderInfos: BidderInfos{"a": {
 				XAPI: AdapterXAPI{Username: "username1", Password: "password2", Tracker: "tracker3"},
 			}},
-			givenConfigBidderInfos: BidderInfos{"a": {Syncer: &Syncer{Key: "override"}}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{Syncer: &Syncer{Key: "override"}}}},
 			expectedBidderInfos: BidderInfos{"a": {
 				XAPI:   AdapterXAPI{Username: "username1", Password: "password2", Tracker: "tracker3"},
 				Syncer: &Syncer{Key: "override"}}},
@@ -1606,9 +1612,9 @@ func TestApplyBidderInfoConfigOverrides(t *testing.T) {
 			description: "Override XAPI",
 			givenFsBidderInfos: BidderInfos{"a": {
 				XAPI: AdapterXAPI{Username: "username", Password: "password", Tracker: "tracker"}}},
-			givenConfigBidderInfos: BidderInfos{"a": {
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{
 				XAPI:   AdapterXAPI{Username: "username1", Password: "password2", Tracker: "tracker3"},
-				Syncer: &Syncer{Key: "override"}}},
+				Syncer: &Syncer{Key: "override"}}}},
 			expectedBidderInfos: BidderInfos{"a": {
 				XAPI:   AdapterXAPI{Username: "username1", Password: "password2", Tracker: "tracker3"},
 				Syncer: &Syncer{Key: "override"}}},
@@ -1616,39 +1622,93 @@ func TestApplyBidderInfoConfigOverrides(t *testing.T) {
 		{
 			description:            "Don't override PlatformID",
 			givenFsBidderInfos:     BidderInfos{"a": {PlatformID: "PlatformID"}},
-			givenConfigBidderInfos: BidderInfos{"a": {Syncer: &Syncer{Key: "override"}}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{Syncer: &Syncer{Key: "override"}}}},
 			expectedBidderInfos:    BidderInfos{"a": {PlatformID: "PlatformID", Syncer: &Syncer{Key: "override"}}},
 		},
 		{
 			description:            "Override PlatformID",
 			givenFsBidderInfos:     BidderInfos{"a": {PlatformID: "PlatformID1"}},
-			givenConfigBidderInfos: BidderInfos{"a": {PlatformID: "PlatformID2", Syncer: &Syncer{Key: "override"}}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{PlatformID: "PlatformID2", Syncer: &Syncer{Key: "override"}}}},
 			expectedBidderInfos:    BidderInfos{"a": {PlatformID: "PlatformID2", Syncer: &Syncer{Key: "override"}}},
 		},
 		{
 			description:            "Don't override AppSecret",
 			givenFsBidderInfos:     BidderInfos{"a": {AppSecret: "AppSecret"}},
-			givenConfigBidderInfos: BidderInfos{"a": {Syncer: &Syncer{Key: "override"}}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{Syncer: &Syncer{Key: "override"}}}},
 			expectedBidderInfos:    BidderInfos{"a": {AppSecret: "AppSecret", Syncer: &Syncer{Key: "override"}}},
 		},
 		{
 			description:            "Override AppSecret",
 			givenFsBidderInfos:     BidderInfos{"a": {AppSecret: "AppSecret1"}},
-			givenConfigBidderInfos: BidderInfos{"a": {AppSecret: "AppSecret2", Syncer: &Syncer{Key: "override"}}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{AppSecret: "AppSecret2", Syncer: &Syncer{Key: "override"}}}},
 			expectedBidderInfos:    BidderInfos{"a": {AppSecret: "AppSecret2", Syncer: &Syncer{Key: "override"}}},
 		},
 		{
 			description:            "Don't override EndpointCompression",
 			givenFsBidderInfos:     BidderInfos{"a": {EndpointCompression: "GZIP"}},
-			givenConfigBidderInfos: BidderInfos{"a": {Syncer: &Syncer{Key: "override"}}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{Syncer: &Syncer{Key: "override"}}}},
 			expectedBidderInfos:    BidderInfos{"a": {EndpointCompression: "GZIP", Syncer: &Syncer{Key: "override"}}},
 		},
 		{
 			description:            "Override EndpointCompression",
 			givenFsBidderInfos:     BidderInfos{"a": {EndpointCompression: "GZIP"}},
-			givenConfigBidderInfos: BidderInfos{"a": {EndpointCompression: "LZ77", Syncer: &Syncer{Key: "override"}}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{EndpointCompression: "LZ77", Syncer: &Syncer{Key: "override"}}}},
 			expectedBidderInfos:    BidderInfos{"a": {EndpointCompression: "LZ77", Syncer: &Syncer{Key: "override"}}},
 		},
+		{
+			description:            "Don't override Disabled",
+			givenFsBidderInfos:     BidderInfos{"a": {Disabled: true}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{Disabled: false, Syncer: &Syncer{Key: "override"}}, nillableFields: bidderInfoNillableFields{Disabled: nil}}},
+			expectedBidderInfos:    BidderInfos{"a": {Disabled: true, Syncer: &Syncer{Key: "override"}}},
+		},
+		{
+			description:            "Override Disabled",
+			givenFsBidderInfos:     BidderInfos{"a": {Disabled: true}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{Disabled: false, Syncer: &Syncer{Key: "override"}}, nillableFields: bidderInfoNillableFields{Disabled: &falseValue}}},
+			expectedBidderInfos:    BidderInfos{"a": {Disabled: false, Syncer: &Syncer{Key: "override"}}},
+		},
+		{
+			description:            "Don't override ModifyingVastXmlAllowed",
+			givenFsBidderInfos:     BidderInfos{"a": {ModifyingVastXmlAllowed: true}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{ModifyingVastXmlAllowed: false, Syncer: &Syncer{Key: "override"}}, nillableFields: bidderInfoNillableFields{ModifyingVastXmlAllowed: nil}}},
+			expectedBidderInfos:    BidderInfos{"a": {ModifyingVastXmlAllowed: true, Syncer: &Syncer{Key: "override"}}},
+		},
+		{
+			description:            "Override ModifyingVastXmlAllowed",
+			givenFsBidderInfos:     BidderInfos{"a": {ModifyingVastXmlAllowed: true}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{ModifyingVastXmlAllowed: false, Syncer: &Syncer{Key: "override"}}, nillableFields: bidderInfoNillableFields{ModifyingVastXmlAllowed: &falseValue}}},
+			expectedBidderInfos:    BidderInfos{"a": {ModifyingVastXmlAllowed: false, Syncer: &Syncer{Key: "override"}}},
+		},
+		{
+			description:            "Don't override OpenRTB",
+			givenFsBidderInfos:     BidderInfos{"a": {OpenRTB: &OpenRTBInfo{Version: "1"}}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{Syncer: &Syncer{Key: "override"}}}},
+			expectedBidderInfos:    BidderInfos{"a": {OpenRTB: &OpenRTBInfo{Version: "1"}, Syncer: &Syncer{Key: "override"}}},
+		},
+		{
+			description:            "Override OpenRTB",
+			givenFsBidderInfos:     BidderInfos{"a": {OpenRTB: &OpenRTBInfo{Version: "1"}}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{OpenRTB: &OpenRTBInfo{Version: "2"}, Syncer: &Syncer{Key: "override"}}}},
+			expectedBidderInfos:    BidderInfos{"a": {OpenRTB: &OpenRTBInfo{Version: "2"}, Syncer: &Syncer{Key: "override"}}},
+		},
+		{
+			description:            "Don't override AliasOf",
+			givenFsBidderInfos:     BidderInfos{"a": {AliasOf: "Alias1"}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{}}},
+			expectedBidderInfos:    BidderInfos{"a": {AliasOf: "Alias1"}},
+		},
+		{
+			description:            "Attempt override AliasOf but ignored",
+			givenFsBidderInfos:     BidderInfos{"a": {AliasOf: "Alias1"}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{AliasOf: "Alias2"}}},
+			expectedBidderInfos:    BidderInfos{"a": {AliasOf: "Alias1"}},
+		},
+		{
+			description:            "Two bidder infos: One with overrides and one without",
+			givenFsBidderInfos:     BidderInfos{"a": {Endpoint: "original"}, "b": {Endpoint: "b endpoint"}},
+			givenConfigBidderInfos: nillableFieldBidderInfos{"a": {bidderInfo: BidderInfo{Endpoint: "override", Syncer: &Syncer{Key: "override"}}}},
+			expectedBidderInfos:    BidderInfos{"a": {Endpoint: "override", Syncer: &Syncer{Key: "override"}}, "b": {Endpoint: "b endpoint"}},
+		},
 	}
 	for _, test := range testCases {
 		bidderInfos, resultErr := applyBidderInfoConfigOverrides(test.givenConfigBidderInfos, test.givenFsBidderInfos, mockNormalizeBidderName)
@@ -1659,26 +1719,31 @@ func TestApplyBidderInfoConfigOverrides(t *testing.T) {
 
 func TestApplyBidderInfoConfigOverridesInvalid(t *testing.T) {
 	var testCases = []struct {
-		description            string
-		givenFsBidderInfos     BidderInfos
-		givenConfigBidderInfos BidderInfos
-		expectedError          string
-		expectedBidderInfos    BidderInfos
+		description                   string
+		givenFsBidderInfos            BidderInfos
+		givenNillableFieldBidderInfos nillableFieldBidderInfos
+		expectedError                 string
+		expectedBidderInfos           BidderInfos
 	}{
 		{
-			description:            "Bidder doesn't exists in bidder list",
-			givenConfigBidderInfos: BidderInfos{"unknown": {Syncer: &Syncer{Key: "override"}}},
-			expectedError:          "error setting configuration for bidder unknown: unknown bidder",
+			description: "Bidder doesn't exists in bidder list",
+			givenNillableFieldBidderInfos: nillableFieldBidderInfos{"unknown": nillableFieldBidderInfo{
+				bidderInfo: BidderInfo{Syncer: &Syncer{Key: "override"}},
+			}},
+			expectedError: "error setting configuration for bidder unknown: unknown bidder",
 		},
 		{
-			description:            "Bidder doesn't exists in file system",
-			givenFsBidderInfos:     BidderInfos{"unknown": {Endpoint: "original"}},
-			givenConfigBidderInfos: BidderInfos{"bidderA": {Syncer: &Syncer{Key: "override"}}},
-			expectedError:          "error finding configuration for bidder bidderA: unknown bidder",
+			description:        "Bidder doesn't exists in file system",
+			givenFsBidderInfos: BidderInfos{"unknown": {Endpoint: "original"}},
+			givenNillableFieldBidderInfos: nillableFieldBidderInfos{"bidderA": nillableFieldBidderInfo{
+				bidderInfo: BidderInfo{Syncer: &Syncer{Key: "override"}},
+			}},
+			expectedError: "error finding configuration for bidder bidderA: unknown bidder",
 		},
 	}
 	for _, test := range testCases {
-		_, err := applyBidderInfoConfigOverrides(test.givenConfigBidderInfos, test.givenFsBidderInfos, mockNormalizeBidderName)
+
+		_, err := applyBidderInfoConfigOverrides(test.givenNillableFieldBidderInfos, test.givenFsBidderInfos, mockNormalizeBidderName)
 		assert.ErrorContains(t, err, test.expectedError, test.description+":err")
 	}
 }
@@ -1690,7 +1755,19 @@ func TestReadFullYamlBidderConfig(t *testing.T) {
 	err := yaml.Unmarshal([]byte(fullBidderYAMLConfig), &bidderInf)
 	require.NoError(t, err)
 
-	actualBidderInfo, err := applyBidderInfoConfigOverrides(BidderInfos{bidder: bidderInf}, BidderInfos{bidder: {Syncer: &Syncer{Supports: []string{"iframe"}}}}, mockNormalizeBidderName)
+	bidderInfoOverrides := nillableFieldBidderInfos{
+		bidder: nillableFieldBidderInfo{
+			bidderInfo: bidderInf,
+			nillableFields: bidderInfoNillableFields{
+				Disabled:                &bidderInf.Disabled,
+				ModifyingVastXmlAllowed: &bidderInf.ModifyingVastXmlAllowed,
+			},
+		},
+	}
+	bidderInfoBase := BidderInfos{
+		bidder: {Syncer: &Syncer{Supports: []string{"iframe"}}},
+	}
+	actualBidderInfo, err := applyBidderInfoConfigOverrides(bidderInfoOverrides, bidderInfoBase, mockNormalizeBidderName)
 	require.NoError(t, err)
 
 	expectedBidderInfo := BidderInfos{
diff --git a/config/config_test.go b/config/config_test.go
index 535b4e7b..a551c1be 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -5,6 +5,7 @@ import (
 	"errors"
 	"net"
 	"os"
+	"strings"
 	"testing"
 	"time"
 
@@ -174,6 +175,14 @@ func TestDefaults(t *testing.T) {
 
 	//Assert the price floor default values
 	cmpBools(t, "price_floors.enabled", false, cfg.PriceFloors.Enabled)
+	cmpInts(t, "price_floors.fetcher.worker", 20, cfg.PriceFloors.Fetcher.Worker)
+	cmpInts(t, "price_floors.fetcher.capacity", 20000, cfg.PriceFloors.Fetcher.Capacity)
+	cmpInts(t, "price_floors.fetcher.cache_size_mb", 64, cfg.PriceFloors.Fetcher.CacheSize)
+	cmpInts(t, "price_floors.fetcher.http_client.max_connections_per_host", 0, cfg.PriceFloors.Fetcher.HttpClient.MaxConnsPerHost)
+	cmpInts(t, "price_floors.fetcher.http_client.max_idle_connections", 40, cfg.PriceFloors.Fetcher.HttpClient.MaxIdleConns)
+	cmpInts(t, "price_floors.fetcher.http_client.max_idle_connections_per_host", 2, cfg.PriceFloors.Fetcher.HttpClient.MaxIdleConnsPerHost)
+	cmpInts(t, "price_floors.fetcher.http_client.idle_connection_timeout_seconds", 60, cfg.PriceFloors.Fetcher.HttpClient.IdleConnTimeout)
+	cmpInts(t, "price_floors.fetcher.max_retries", 10, cfg.PriceFloors.Fetcher.MaxRetries)
 
 	// Assert compression related defaults
 	cmpBools(t, "compression.request.enable_gzip", false, cfg.Compression.Request.GZIP)
@@ -186,6 +195,15 @@ func TestDefaults(t *testing.T) {
 	cmpBools(t, "account_defaults.price_floors.use_dynamic_data", false, cfg.AccountDefaults.PriceFloors.UseDynamicData)
 	cmpInts(t, "account_defaults.price_floors.max_rules", 100, cfg.AccountDefaults.PriceFloors.MaxRule)
 	cmpInts(t, "account_defaults.price_floors.max_schema_dims", 3, cfg.AccountDefaults.PriceFloors.MaxSchemaDims)
+	cmpBools(t, "account_defaults.price_floors.fetch.enabled", false, cfg.AccountDefaults.PriceFloors.Fetcher.Enabled)
+	cmpStrings(t, "account_defaults.price_floors.fetch.url", "", cfg.AccountDefaults.PriceFloors.Fetcher.URL)
+	cmpInts(t, "account_defaults.price_floors.fetch.timeout_ms", 3000, cfg.AccountDefaults.PriceFloors.Fetcher.Timeout)
+	cmpInts(t, "account_defaults.price_floors.fetch.max_file_size_kb", 100, cfg.AccountDefaults.PriceFloors.Fetcher.MaxFileSizeKB)
+	cmpInts(t, "account_defaults.price_floors.fetch.max_rules", 1000, cfg.AccountDefaults.PriceFloors.Fetcher.MaxRules)
+	cmpInts(t, "account_defaults.price_floors.fetch.period_sec", 3600, cfg.AccountDefaults.PriceFloors.Fetcher.Period)
+	cmpInts(t, "account_defaults.price_floors.fetch.max_age_sec", 86400, cfg.AccountDefaults.PriceFloors.Fetcher.MaxAge)
+	cmpInts(t, "account_defaults.price_floors.fetch.max_schema_dims", 0, cfg.AccountDefaults.PriceFloors.Fetcher.MaxSchemaDims)
+
 	cmpBools(t, "account_defaults.events.enabled", false, cfg.AccountDefaults.Events.Enabled)
 
 	cmpBools(t, "hooks.enabled", false, cfg.Hooks.Enabled)
@@ -450,6 +468,16 @@ hooks:
     enabled: true
 price_floors:
     enabled: true
+    fetcher:
+      worker: 20
+      capacity: 20000
+      cache_size_mb: 8
+      http_client:
+        max_connections_per_host: 5
+        max_idle_connections: 1
+        max_idle_connections_per_host: 2
+        idle_connection_timeout_seconds: 10
+      max_retries: 5
 account_defaults:
     events:
         enabled: true
@@ -461,6 +489,15 @@ account_defaults:
         use_dynamic_data: true
         max_rules: 120
         max_schema_dims: 5
+        fetch:
+          enabled: true
+          url: http://test.com/floors
+          timeout_ms: 500
+          max_file_size_kb: 200
+          max_rules: 500
+          period_sec: 2000
+          max_age_sec: 6000
+          max_schema_dims: 10
     privacy:
         ipv6:
             anon_keep_bits: 50
@@ -556,6 +593,14 @@ func TestFullConfig(t *testing.T) {
 
 	//Assert the price floor values
 	cmpBools(t, "price_floors.enabled", true, cfg.PriceFloors.Enabled)
+	cmpInts(t, "price_floors.fetcher.worker", 20, cfg.PriceFloors.Fetcher.Worker)
+	cmpInts(t, "price_floors.fetcher.capacity", 20000, cfg.PriceFloors.Fetcher.Capacity)
+	cmpInts(t, "price_floors.fetcher.cache_size_mb", 8, cfg.PriceFloors.Fetcher.CacheSize)
+	cmpInts(t, "price_floors.fetcher.http_client.max_connections_per_host", 5, cfg.PriceFloors.Fetcher.HttpClient.MaxConnsPerHost)
+	cmpInts(t, "price_floors.fetcher.http_client.max_idle_connections", 1, cfg.PriceFloors.Fetcher.HttpClient.MaxIdleConns)
+	cmpInts(t, "price_floors.fetcher.http_client.max_idle_connections_per_host", 2, cfg.PriceFloors.Fetcher.HttpClient.MaxIdleConnsPerHost)
+	cmpInts(t, "price_floors.fetcher.http_client.idle_connection_timeout_seconds", 10, cfg.PriceFloors.Fetcher.HttpClient.IdleConnTimeout)
+	cmpInts(t, "price_floors.fetcher.max_retries", 5, cfg.PriceFloors.Fetcher.MaxRetries)
 	cmpBools(t, "account_defaults.price_floors.enabled", true, cfg.AccountDefaults.PriceFloors.Enabled)
 	cmpInts(t, "account_defaults.price_floors.enforce_floors_rate", 50, cfg.AccountDefaults.PriceFloors.EnforceFloorsRate)
 	cmpBools(t, "account_defaults.price_floors.adjust_for_bid_adjustment", false, cfg.AccountDefaults.PriceFloors.AdjustForBidAdjustment)
@@ -563,6 +608,15 @@ func TestFullConfig(t *testing.T) {
 	cmpBools(t, "account_defaults.price_floors.use_dynamic_data", true, cfg.AccountDefaults.PriceFloors.UseDynamicData)
 	cmpInts(t, "account_defaults.price_floors.max_rules", 120, cfg.AccountDefaults.PriceFloors.MaxRule)
 	cmpInts(t, "account_defaults.price_floors.max_schema_dims", 5, cfg.AccountDefaults.PriceFloors.MaxSchemaDims)
+	cmpBools(t, "account_defaults.price_floors.fetch.enabled", true, cfg.AccountDefaults.PriceFloors.Fetcher.Enabled)
+	cmpStrings(t, "account_defaults.price_floors.fetch.url", "http://test.com/floors", cfg.AccountDefaults.PriceFloors.Fetcher.URL)
+	cmpInts(t, "account_defaults.price_floors.fetch.timeout_ms", 500, cfg.AccountDefaults.PriceFloors.Fetcher.Timeout)
+	cmpInts(t, "account_defaults.price_floors.fetch.max_file_size_kb", 200, cfg.AccountDefaults.PriceFloors.Fetcher.MaxFileSizeKB)
+	cmpInts(t, "account_defaults.price_floors.fetch.max_rules", 500, cfg.AccountDefaults.PriceFloors.Fetcher.MaxRules)
+	cmpInts(t, "account_defaults.price_floors.fetch.period_sec", 2000, cfg.AccountDefaults.PriceFloors.Fetcher.Period)
+	cmpInts(t, "account_defaults.price_floors.fetch.max_age_sec", 6000, cfg.AccountDefaults.PriceFloors.Fetcher.MaxAge)
+	cmpInts(t, "account_defaults.price_floors.fetch.max_schema_dims", 10, cfg.AccountDefaults.PriceFloors.Fetcher.MaxSchemaDims)
+
 	cmpBools(t, "account_defaults.events.enabled", true, cfg.AccountDefaults.Events.Enabled)
 
 	cmpInts(t, "account_defaults.privacy.ipv6.anon_keep_bits", 50, cfg.AccountDefaults.Privacy.IPv6Config.AnonKeepBits)
@@ -771,6 +825,15 @@ func TestValidateConfig(t *testing.T) {
 			Files:         FileFetcherConfig{Enabled: true},
 			InMemoryCache: InMemoryCache{Type: "none"},
 		},
+		AccountDefaults: Account{
+			PriceFloors: AccountPriceFloors{
+				Fetcher: AccountFloorFetch{
+					Timeout: 100,
+					Period:  300,
+					MaxAge:  600,
+				},
+			},
+		},
 	}
 
 	v := viper.New()
@@ -1294,6 +1357,155 @@ func TestSpecialFeature1VendorExceptionMap(t *testing.T) {
 	}
 }
 
+func TestSetConfigBidderInfoNillableFields(t *testing.T) {
+	falseValue := false
+	trueValue := true
+
+	bidder1ConfigFalses := []byte(`
+    adapters:
+      bidder1:
+        disabled: false
+        modifyingVastXmlAllowed: false`)
+	bidder1ConfigTrues := []byte(`
+    adapters:
+      bidder1:
+        disabled: true
+        modifyingVastXmlAllowed: true`)
+	bidder1ConfigNils := []byte(`
+    adapters:
+      bidder1:
+        disabled: null
+        modifyingVastXmlAllowed: null`)
+	bidder1Bidder2ConfigMixed := []byte(`
+    adapters:
+      bidder1:
+        disabled: true
+        modifyingVastXmlAllowed: false
+      bidder2:
+        disabled: false
+        modifyingVastXmlAllowed: true`)
+
+	tests := []struct {
+		name        string
+		rawConfig   []byte
+		bidderInfos BidderInfos
+		expected    nillableFieldBidderInfos
+		expectError bool
+	}{
+		{
+			name:     "viper and bidder infos are nil",
+			expected: nil,
+		},
+		{
+			name:        "viper is nil",
+			bidderInfos: map[string]BidderInfo{},
+			expected:    nil,
+		},
+		{
+			name:      "bidder infos is nil",
+			rawConfig: []byte{},
+			expected:  nil,
+		},
+		{
+			name:        "bidder infos is empty",
+			bidderInfos: map[string]BidderInfo{},
+			expected:    nil,
+		},
+		{
+			name: "one: bidder info has nillable fields as false, viper has as nil",
+			bidderInfos: map[string]BidderInfo{
+				"bidder1": {Disabled: false, ModifyingVastXmlAllowed: false},
+			},
+			rawConfig: bidder1ConfigNils,
+			expected: nillableFieldBidderInfos{
+				"bidder1": nillableFieldBidderInfo{
+					nillableFields: bidderInfoNillableFields{
+						Disabled:                nil,
+						ModifyingVastXmlAllowed: nil,
+					},
+					bidderInfo: BidderInfo{Disabled: false, ModifyingVastXmlAllowed: false},
+				},
+			},
+		},
+		{
+			name: "one: bidder info has nillable fields as false, viper has as false",
+			bidderInfos: map[string]BidderInfo{
+				"bidder1": {Disabled: false, ModifyingVastXmlAllowed: false},
+			},
+			rawConfig: bidder1ConfigFalses,
+			expected: nillableFieldBidderInfos{
+				"bidder1": nillableFieldBidderInfo{
+					nillableFields: bidderInfoNillableFields{
+						Disabled:                &falseValue,
+						ModifyingVastXmlAllowed: &falseValue,
+					},
+					bidderInfo: BidderInfo{Disabled: false, ModifyingVastXmlAllowed: false},
+				},
+			},
+		},
+		{
+			name: "one: bidder info has nillable fields as false, viper has as true",
+			bidderInfos: map[string]BidderInfo{
+				"bidder1": {Disabled: false, ModifyingVastXmlAllowed: false},
+			},
+			rawConfig: bidder1ConfigTrues,
+			expected: nillableFieldBidderInfos{
+				"bidder1": nillableFieldBidderInfo{
+					nillableFields: bidderInfoNillableFields{
+						Disabled:                &trueValue,
+						ModifyingVastXmlAllowed: &trueValue,
+					},
+					bidderInfo: BidderInfo{Disabled: false, ModifyingVastXmlAllowed: false},
+				},
+			},
+		},
+		{
+			name: "many with extra info: bidder infos have nillable fields as false and true, viper has as true and false",
+			bidderInfos: map[string]BidderInfo{
+				"bidder1": {Disabled: false, ModifyingVastXmlAllowed: true, Endpoint: "endpoint a"},
+				"bidder2": {Disabled: true, ModifyingVastXmlAllowed: false, Endpoint: "endpoint b"},
+			},
+			rawConfig: bidder1Bidder2ConfigMixed,
+			expected: nillableFieldBidderInfos{
+				"bidder1": nillableFieldBidderInfo{
+					nillableFields: bidderInfoNillableFields{
+						Disabled:                &trueValue,
+						ModifyingVastXmlAllowed: &falseValue,
+					},
+					bidderInfo: BidderInfo{Disabled: false, ModifyingVastXmlAllowed: true, Endpoint: "endpoint a"},
+				},
+				"bidder2": nillableFieldBidderInfo{
+					nillableFields: bidderInfoNillableFields{
+						Disabled:                &falseValue,
+						ModifyingVastXmlAllowed: &trueValue,
+					},
+					bidderInfo: BidderInfo{Disabled: true, ModifyingVastXmlAllowed: false, Endpoint: "endpoint b"},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			v := viper.New()
+			v.SetConfigType("yaml")
+			for bidderName := range tt.bidderInfos {
+				setBidderDefaults(v, strings.ToLower(bidderName))
+			}
+			v.ReadConfig(bytes.NewBuffer(tt.rawConfig))
+
+			result, err := setConfigBidderInfoNillableFields(v, tt.bidderInfos)
+
+			assert.Equal(t, tt.expected, result)
+			if tt.expectError {
+				assert.NotNil(t, err)
+			} else {
+				assert.Nil(t, err)
+			}
+		})
+	}
+}
+
 func TestTCF2PurposeEnforced(t *testing.T) {
 	tests := []struct {
 		description          string
diff --git a/endpoints/openrtb2/auction_benchmark_test.go b/endpoints/openrtb2/auction_benchmark_test.go
index 88a55e62..835d2920 100644
--- a/endpoints/openrtb2/auction_benchmark_test.go
+++ b/endpoints/openrtb2/auction_benchmark_test.go
@@ -95,6 +95,7 @@ func BenchmarkOpenrtbEndpoint(b *testing.B) {
 		empty_fetcher.EmptyFetcher{},
 		&adscert.NilSigner{},
 		macros.NewStringIndexBasedReplacer(),
+		nil,
 	)
 
 	endpoint, _ := NewEndpoint(
diff --git a/endpoints/openrtb2/test_utils.go b/endpoints/openrtb2/test_utils.go
index cfc8f5d8..e3e7803e 100644
--- a/endpoints/openrtb2/test_utils.go
+++ b/endpoints/openrtb2/test_utils.go
@@ -1204,6 +1204,7 @@ func buildTestExchange(testCfg *testConfigValues, adapterMap map[openrtb_ext.Bid
 		mockFetcher,
 		&adscert.NilSigner{},
 		macros.NewStringIndexBasedReplacer(),
+		nil,
 	)
 
 	testExchange = &exchangeTestWrapper{
diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go
index 824aecc8..6db61d0d 100644
--- a/exchange/bidder_test.go
+++ b/exchange/bidder_test.go
@@ -1568,6 +1568,46 @@ func TestMobileNativeTypes(t *testing.T) {
 	}
 }
 
+func TestAddNativeTypes(t *testing.T) {
+	testCases := []struct {
+		description      string
+		bidderRequest    *openrtb2.BidRequest
+		bid              *openrtb2.Bid
+		expectedResponse *nativeResponse.Response
+		expectedErrors   []error
+	}{
+		{
+			description: "Null in bid.Adm in response",
+			bidderRequest: &openrtb2.BidRequest{
+				Imp: []openrtb2.Imp{
+					{
+						ID: "some-imp-id",
+						Native: &openrtb2.Native{
+							Request: "{\"ver\":\"1.1\",\"context\":1,\"contextsubtype\":11,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":500}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":1,\"hmin\":1}},{\"id\":3,\"required\":0,\"data\":{\"type\":1,\"len\":200}},{\"id\":4,\"required\":0,\"data\":{\"type\":2,\"len\":15000}},{\"id\":5,\"required\":0,\"data\":{\"type\":6,\"len\":40}}]}",
+						},
+					},
+				},
+				App: &openrtb2.App{},
+			},
+			bid: &openrtb2.Bid{
+				ImpID: "some-imp-id",
+				AdM:   "null",
+				Price: 10,
+			},
+			expectedResponse: nil,
+			expectedErrors:   nil,
+		},
+	}
+
+	for _, tt := range testCases {
+		t.Run(tt.description, func(t *testing.T) {
+			resp, errs := addNativeTypes(tt.bid, tt.bidderRequest)
+			assert.Equal(t, tt.expectedResponse, resp, "response")
+			assert.Equal(t, tt.expectedErrors, errs, "errors")
+		})
+	}
+}
+
 func TestRequestBidsStoredBidResponses(t *testing.T) {
 	respBody := "{\"bid\":false}"
 	respStatus := 200
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index bfeb9374..25925e83 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -83,7 +83,7 @@ func TestNewExchange(t *testing.T) {
 		},
 	}.Builder
 
-	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer()).(*exchange)
+	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
 	for _, bidderName := range knownAdapters {
 		if _, ok := e.adapterMap[bidderName]; !ok {
 			if biddersInfo[string(bidderName)].IsEnabled() {
@@ -133,7 +133,7 @@ func TestCharacterEscape(t *testing.T) {
 		},
 	}.Builder
 
-	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer()).(*exchange)
+	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
 
 	// 	3) Build all the parameters e.buildBidResponse(ctx.Background(), liveA... ) needs
 	//liveAdapters []openrtb_ext.BidderName,
@@ -816,6 +816,14 @@ func TestAdapterCurrency(t *testing.T) {
 	}
 }
 
+type mockPriceFloorFetcher struct{}
+
+func (mpf *mockPriceFloorFetcher) Fetch(configs config.AccountPriceFloors) (*openrtb_ext.PriceFloorRules, string) {
+	return nil, openrtb_ext.FetchNone
+}
+
+func (mpf *mockPriceFloorFetcher) Stop() {}
+
 func TestFloorsSignalling(t *testing.T) {
 
 	fakeCurrencyClient := &fakeCurrencyRatesHttpClient{
@@ -840,7 +848,8 @@ func TestFloorsSignalling(t *testing.T) {
 		currencyConverter: currencyConverter,
 		categoriesFetcher: nilCategoryFetcher{},
 		bidIDGenerator:    &mockBidIDGenerator{false, false},
-		floor:             config.PriceFloors{Enabled: true},
+		priceFloorEnabled: true,
+		priceFloorFetcher: &mockPriceFloorFetcher{},
 	}
 	e.requestSplitter = requestSplitter{
 		me:               e.me,
@@ -1428,7 +1437,7 @@ func TestGetBidCacheInfoEndToEnd(t *testing.T) {
 		},
 	}.Builder
 
-	e := NewExchange(adapters, pbc, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer()).(*exchange)
+	e := NewExchange(adapters, pbc, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
 	// 	3) Build all the parameters e.buildBidResponse(ctx.Background(), liveA... ) needs
 	liveAdapters := []openrtb_ext.BidderName{bidderName}
 
@@ -1785,7 +1794,7 @@ func TestBidResponseCurrency(t *testing.T) {
 		},
 	}.Builder
 
-	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer()).(*exchange)
+	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
 
 	liveAdapters := make([]openrtb_ext.BidderName, 1)
 	liveAdapters[0] = "appnexus"
@@ -1931,7 +1940,7 @@ func TestBidResponseImpExtInfo(t *testing.T) {
 		t.Fatalf("Error intializing adapters: %v", adaptersErr)
 	}
 
-	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, nil, gdprPermsBuilder, nil, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer()).(*exchange)
+	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, nil, gdprPermsBuilder, nil, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
 
 	liveAdapters := make([]openrtb_ext.BidderName, 1)
 	liveAdapters[0] = "appnexus"
@@ -2023,7 +2032,7 @@ func TestRaceIntegration(t *testing.T) {
 		},
 	}.Builder
 
-	ex := NewExchange(adapters, &wellBehavedCache{}, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, &nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer()).(*exchange)
+	ex := NewExchange(adapters, &wellBehavedCache{}, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, &nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
 	_, err = ex.HoldAuction(context.Background(), auctionRequest, &debugLog)
 	if err != nil {
 		t.Errorf("HoldAuction returned unexpected error: %v", err)
@@ -2121,7 +2130,7 @@ func TestPanicRecovery(t *testing.T) {
 		},
 	}.Builder
 
-	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer()).(*exchange)
+	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
 
 	chBids := make(chan *bidResponseWrapper, 1)
 	panicker := func(bidderRequest BidderRequest, conversions currency.Conversions) {
@@ -2191,7 +2200,7 @@ func TestPanicRecoveryHighLevel(t *testing.T) {
 			allowAllBidders: true,
 		},
 	}.Builder
-	e := NewExchange(adapters, &mockCache{}, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, categoriesFetcher, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer()).(*exchange)
+	e := NewExchange(adapters, &mockCache{}, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, categoriesFetcher, &adscert.NilSigner{}, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
 
 	e.adapterMap[openrtb_ext.BidderBeachfront] = panicingAdapter{}
 	e.adapterMap[openrtb_ext.BidderAppnexus] = panicingAdapter{}
@@ -2634,7 +2643,8 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string]
 		server:                   config.Server{ExternalUrl: server.ExternalUrl, GvlID: server.GvlID, DataCenter: server.DataCenter},
 		bidValidationEnforcement: hostBidValidation,
 		requestSplitter:          requestSplitter,
-		floor:                    config.PriceFloors{Enabled: floorsFlag},
+		priceFloorEnabled:        floorsFlag,
+		priceFloorFetcher:        &mockPriceFloorFetcher{},
 	}
 }
 
@@ -4660,7 +4670,7 @@ func TestPassExperimentConfigsToHoldAuction(t *testing.T) {
 		},
 	}.Builder
 
-	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &signer, macros.NewStringIndexBasedReplacer()).(*exchange)
+	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &signer, macros.NewStringIndexBasedReplacer(), nil).(*exchange)
 
 	// Define mock incoming bid requeset
 	mockBidRequest := &openrtb2.BidRequest{
@@ -5620,12 +5630,12 @@ func (e mockUpdateBidRequestHook) HandleBidderRequestHook(_ context.Context, mct
 	c := hookstage.ChangeSet[hookstage.BidderRequestPayload]{}
 	c.AddMutation(
 		func(payload hookstage.BidderRequestPayload) (hookstage.BidderRequestPayload, error) {
-			payload.BidRequest.Site.Name = "test"
+			payload.Request.Site.Name = "test"
 			return payload, nil
 		}, hookstage.MutationUpdate, "bidRequest", "site.name",
 	).AddMutation(
 		func(payload hookstage.BidderRequestPayload) (hookstage.BidderRequestPayload, error) {
-			payload.BidRequest.Site.Domain = "test.com"
+			payload.Request.Site.Domain = "test.com"
 			return payload, nil
 		}, hookstage.MutationUpdate, "bidRequest", "site.domain",
 	)
diff --git a/floors/fetcher_test.go b/floors/fetcher_test.go
new file mode 100644
index 00000000..085fd3ed
--- /dev/null
+++ b/floors/fetcher_test.go
@@ -0,0 +1,1279 @@
+package floors
+
+import (
+	"encoding/json"
+	"fmt"
+	"math/rand"
+	"net/http"
+	"net/http/httptest"
+	"reflect"
+	"testing"
+	"time"
+
+	"github.com/alitto/pond"
+	"github.com/coocood/freecache"
+	"github.com/prebid/prebid-server/v2/config"
+	"github.com/prebid/prebid-server/v2/metrics"
+	metricsConf "github.com/prebid/prebid-server/v2/metrics/config"
+	"github.com/prebid/prebid-server/v2/openrtb_ext"
+	"github.com/prebid/prebid-server/v2/util/timeutil"
+	"github.com/stretchr/testify/assert"
+)
+
+const MaxAge = "max-age"
+
+func TestFetchQueueLen(t *testing.T) {
+	tests := []struct {
+		name string
+		fq   FetchQueue
+		want int
+	}{
+		{
+			name: "Queue is empty",
+			fq:   make(FetchQueue, 0),
+			want: 0,
+		},
+		{
+			name: "Queue is of lenght 1",
+			fq:   make(FetchQueue, 1),
+			want: 1,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := tt.fq.Len(); got != tt.want {
+				t.Errorf("FetchQueue.Len() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestFetchQueueLess(t *testing.T) {
+	type args struct {
+		i int
+		j int
+	}
+	tests := []struct {
+		name string
+		fq   FetchQueue
+		args args
+		want bool
+	}{
+		{
+			name: "first fetchperiod is less than second",
+			fq:   FetchQueue{&fetchInfo{fetchTime: 10}, &fetchInfo{fetchTime: 20}},
+			args: args{i: 0, j: 1},
+			want: true,
+		},
+		{
+			name: "first fetchperiod is greater than second",
+			fq:   FetchQueue{&fetchInfo{fetchTime: 30}, &fetchInfo{fetchTime: 10}},
+			args: args{i: 0, j: 1},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := tt.fq.Less(tt.args.i, tt.args.j); got != tt.want {
+				t.Errorf("FetchQueue.Less() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestFetchQueueSwap(t *testing.T) {
+	type args struct {
+		i int
+		j int
+	}
+	tests := []struct {
+		name string
+		fq   FetchQueue
+		args args
+	}{
+		{
+			name: "Swap two elements at index i and j",
+			fq:   FetchQueue{&fetchInfo{fetchTime: 30}, &fetchInfo{fetchTime: 10}},
+			args: args{i: 0, j: 1},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			fInfo1, fInfo2 := tt.fq[0], tt.fq[1]
+			tt.fq.Swap(tt.args.i, tt.args.j)
+			assert.Equal(t, fInfo1, tt.fq[1], "elements are not swapped")
+			assert.Equal(t, fInfo2, tt.fq[0], "elements are not swapped")
+		})
+	}
+}
+
+func TestFetchQueuePush(t *testing.T) {
+	type args struct {
+		element interface{}
+	}
+	tests := []struct {
+		name string
+		fq   *FetchQueue
+		args args
+	}{
+		{
+			name: "Push element to queue",
+			fq:   &FetchQueue{},
+			args: args{element: &fetchInfo{fetchTime: 10}},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			tt.fq.Push(tt.args.element)
+			q := *tt.fq
+			assert.Equal(t, q[0], &fetchInfo{fetchTime: 10})
+		})
+	}
+}
+
+func TestFetchQueuePop(t *testing.T) {
+	tests := []struct {
+		name string
+		fq   *FetchQueue
+		want interface{}
+	}{
+		{
+			name: "Pop element from queue",
+			fq:   &FetchQueue{&fetchInfo{fetchTime: 10}},
+			want: &fetchInfo{fetchTime: 10},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := tt.fq.Pop(); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("FetchQueue.Pop() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestFetchQueueTop(t *testing.T) {
+	tests := []struct {
+		name string
+		fq   *FetchQueue
+		want *fetchInfo
+	}{
+		{
+			name: "Get top element from queue",
+			fq:   &FetchQueue{&fetchInfo{fetchTime: 20}},
+			want: &fetchInfo{fetchTime: 20},
+		},
+		{
+			name: "Queue is empty",
+			fq:   &FetchQueue{},
+			want: nil,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := tt.fq.Top(); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("FetchQueue.Top() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestValidatePriceFloorRules(t *testing.T) {
+	var zero = 0
+	var one_o_one = 101
+	var testURL = "abc.com"
+	type args struct {
+		configs     config.AccountFloorFetch
+		priceFloors *openrtb_ext.PriceFloorRules
+	}
+	tests := []struct {
+		name    string
+		args    args
+		wantErr bool
+	}{
+		{
+			name: "Price floor data is empty",
+			args: args{
+				configs: config.AccountFloorFetch{
+					Enabled:       true,
+					URL:           testURL,
+					Timeout:       5,
+					MaxFileSizeKB: 20,
+					MaxRules:      5,
+					MaxAge:        20,
+					Period:        10,
+				},
+				priceFloors: &openrtb_ext.PriceFloorRules{},
+			},
+			wantErr: true,
+		},
+		{
+			name: "Model group array is empty",
+			args: args{
+				configs: config.AccountFloorFetch{
+					Enabled:       true,
+					URL:           testURL,
+					Timeout:       5,
+					MaxFileSizeKB: 20,
+					MaxRules:      5,
+					MaxAge:        20,
+					Period:        10,
+				},
+				priceFloors: &openrtb_ext.PriceFloorRules{
+					Data: &openrtb_ext.PriceFloorData{},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "floor rules is empty",
+			args: args{
+				configs: config.AccountFloorFetch{
+					Enabled:       true,
+					URL:           testURL,
+					Timeout:       5,
+					MaxFileSizeKB: 20,
+					MaxRules:      5,
+					MaxAge:        20,
+					Period:        10,
+				},
+				priceFloors: &openrtb_ext.PriceFloorRules{
+					Data: &openrtb_ext.PriceFloorData{
+						ModelGroups: []openrtb_ext.PriceFloorModelGroup{{
+							Values: map[string]float64{},
+						}},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "floor rules is grater than max floor rules",
+			args: args{
+				configs: config.AccountFloorFetch{
+					Enabled:       true,
+					URL:           testURL,
+					Timeout:       5,
+					MaxFileSizeKB: 20,
+					MaxRules:      0,
+					MaxAge:        20,
+					Period:        10,
+				},
+				priceFloors: &openrtb_ext.PriceFloorRules{
+					Data: &openrtb_ext.PriceFloorData{
+						ModelGroups: []openrtb_ext.PriceFloorModelGroup{{
+							Values: map[string]float64{
+								"*|*|www.website.com": 15.01,
+							},
+						}},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "Modelweight is zero",
+			args: args{
+				configs: config.AccountFloorFetch{
+					Enabled:       true,
+					URL:           testURL,
+					Timeout:       5,
+					MaxFileSizeKB: 20,
+					MaxRules:      1,
+					MaxAge:        20,
+					Period:        10,
+				},
+				priceFloors: &openrtb_ext.PriceFloorRules{
+					Data: &openrtb_ext.PriceFloorData{
+						ModelGroups: []openrtb_ext.PriceFloorModelGroup{{
+							Values: map[string]float64{
+								"*|*|www.website.com": 15.01,
+							},
+							ModelWeight: &zero,
+						}},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "Modelweight is 101",
+			args: args{
+				configs: config.AccountFloorFetch{
+					Enabled:       true,
+					URL:           testURL,
+					Timeout:       5,
+					MaxFileSizeKB: 20,
+					MaxRules:      1,
+					MaxAge:        20,
+					Period:        10,
+				},
+				priceFloors: &openrtb_ext.PriceFloorRules{
+					Data: &openrtb_ext.PriceFloorData{
+						ModelGroups: []openrtb_ext.PriceFloorModelGroup{{
+							Values: map[string]float64{
+								"*|*|www.website.com": 15.01,
+							},
+							ModelWeight: &one_o_one,
+						}},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "skiprate is 101",
+			args: args{
+				configs: config.AccountFloorFetch{
+					Enabled:       true,
+					URL:           testURL,
+					Timeout:       5,
+					MaxFileSizeKB: 20,
+					MaxRules:      1,
+					MaxAge:        20,
+					Period:        10,
+				},
+				priceFloors: &openrtb_ext.PriceFloorRules{
+					Data: &openrtb_ext.PriceFloorData{
+						ModelGroups: []openrtb_ext.PriceFloorModelGroup{{
+							Values: map[string]float64{
+								"*|*|www.website.com": 15.01,
+							},
+							SkipRate: 101,
+						}},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "Default is -1",
+			args: args{
+				configs: config.AccountFloorFetch{
+					Enabled:       true,
+					URL:           testURL,
+					Timeout:       5,
+					MaxFileSizeKB: 20,
+					MaxRules:      1,
+					MaxAge:        20,
+					Period:        10,
+				},
+				priceFloors: &openrtb_ext.PriceFloorRules{
+					Data: &openrtb_ext.PriceFloorData{
+						ModelGroups: []openrtb_ext.PriceFloorModelGroup{{
+							Values: map[string]float64{
+								"*|*|www.website.com": 15.01,
+							},
+							Default: -1,
+						}},
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "Invalid skip rate in data",
+			args: args{
+				configs: config.AccountFloorFetch{
+					Enabled:       true,
+					URL:           testURL,
+					Timeout:       5,
+					MaxFileSizeKB: 20,
+					MaxRules:      1,
+					MaxAge:        20,
+					Period:        10,
+				},
+				priceFloors: &openrtb_ext.PriceFloorRules{
+					Data: &openrtb_ext.PriceFloorData{
+						SkipRate: -44,
+						ModelGroups: []openrtb_ext.PriceFloorModelGroup{{
+							Values: map[string]float64{
+								"*|*|www.website.com": 15.01,
+							},
+						}},
+					},
+				},
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if err := validateRules(tt.args.configs, tt.args.priceFloors); (err != nil) != tt.wantErr {
+				t.Errorf("validatePriceFloorRules() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}
+
+func TestFetchFloorRulesFromURL(t *testing.T) {
+	mockHandler := func(mockResponse []byte, mockStatus int) http.Handler {
+		return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+			w.Header().Add("Content-Length", "645")
+			w.Header().Add(MaxAge, "20")
+			w.WriteHeader(mockStatus)
+			w.Write(mockResponse)
+		})
+	}
+
+	type args struct {
+		configs config.AccountFloorFetch
+	}
+	tests := []struct {
+		name           string
+		args           args
+		response       []byte
+		responseStatus int
+		want           []byte
+		want1          int
+		wantErr        bool
+	}{
+		{
+			name: "Floor data is successfully returned",
+			args: args{
+				configs: config.AccountFloorFetch{
+					URL:     "",
+					Timeout: 60,
+					Period:  300,
+				},
+			},
+			response: func() []byte {
+				data := `{"data":{"currency":"USD","modelgroups":[{"modelweight":40,"modelversion":"version1","default":5,"values":{"banner|300x600|www.website.com":3,"banner|728x90|www.website.com":5,"banner|300x600|*":4,"banner|300x250|*":2,"*|*|*":16,"*|300x250|*":10,"*|300x600|*":12,"*|300x600|www.website.com":11,"banner|*|*":8,"banner|300x250|www.website.com":1,"*|728x90|www.website.com":13,"*|300x250|www.website.com":9,"*|728x90|*":14,"banner|728x90|*":6,"banner|*|www.website.com":7,"*|*|www.website.com":15},"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"}}]},"enabled":true,"floormin":1,"enforcement":{"enforcepbs":false,"floordeals":true}}`
+				return []byte(data)
+			}(),
+			responseStatus: 200,
+			want: func() []byte {
+				data := `{"data":{"currency":"USD","modelgroups":[{"modelweight":40,"modelversion":"version1","default":5,"values":{"banner|300x600|www.website.com":3,"banner|728x90|www.website.com":5,"banner|300x600|*":4,"banner|300x250|*":2,"*|*|*":16,"*|300x250|*":10,"*|300x600|*":12,"*|300x600|www.website.com":11,"banner|*|*":8,"banner|300x250|www.website.com":1,"*|728x90|www.website.com":13,"*|300x250|www.website.com":9,"*|728x90|*":14,"banner|728x90|*":6,"banner|*|www.website.com":7,"*|*|www.website.com":15},"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"}}]},"enabled":true,"floormin":1,"enforcement":{"enforcepbs":false,"floordeals":true}}`
+				return []byte(data)
+			}(),
+			want1:   20,
+			wantErr: false,
+		},
+		{
+			name: "Time out occured",
+			args: args{
+				configs: config.AccountFloorFetch{
+					URL:     "",
+					Timeout: 0,
+					Period:  300,
+				},
+			},
+			want1:          0,
+			responseStatus: 200,
+			wantErr:        true,
+		},
+		{
+			name: "Invalid URL",
+			args: args{
+				configs: config.AccountFloorFetch{
+					URL:     "%%",
+					Timeout: 10,
+					Period:  300,
+				},
+			},
+			want1:          0,
+			responseStatus: 200,
+			wantErr:        true,
+		},
+		{
+			name: "No response from server",
+			args: args{
+				configs: config.AccountFloorFetch{
+					URL:     "",
+					Timeout: 10,
+					Period:  300,
+				},
+			},
+			want1:          0,
+			responseStatus: 500,
+			wantErr:        true,
+		},
+		{
+			name: "Invalid response",
+			args: args{
+				configs: config.AccountFloorFetch{
+					URL:     "",
+					Timeout: 10,
+					Period:  300,
+				},
+			},
+			want1:          0,
+			response:       []byte("1"),
+			responseStatus: 200,
+			wantErr:        true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mockHttpServer := httptest.NewServer(mockHandler(tt.response, tt.responseStatus))
+			defer mockHttpServer.Close()
+
+			if tt.args.configs.URL == "" {
+				tt.args.configs.URL = mockHttpServer.URL
+			}
+			pff := PriceFloorFetcher{
+				httpClient: mockHttpServer.Client(),
+			}
+			got, got1, err := pff.fetchFloorRulesFromURL(tt.args.configs)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("fetchFloorRulesFromURL() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("fetchFloorRulesFromURL() got = %v, want %v", got, tt.want)
+			}
+			if got1 != tt.want1 {
+				t.Errorf("fetchFloorRulesFromURL() got1 = %v, want %v", got1, tt.want1)
+			}
+		})
+	}
+}
+
+func TestFetchFloorRulesFromURLInvalidMaxAge(t *testing.T) {
+	mockHandler := func(mockResponse []byte, mockStatus int) http.Handler {
+		return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+			w.Header().Add("Content-Length", "645")
+			w.Header().Add(MaxAge, "abc")
+			w.WriteHeader(mockStatus)
+			w.Write(mockResponse)
+		})
+	}
+
+	type args struct {
+		configs config.AccountFloorFetch
+	}
+	tests := []struct {
+		name           string
+		args           args
+		response       []byte
+		responseStatus int
+		want           []byte
+		want1          int
+		wantErr        bool
+	}{
+		{
+			name: "Floor data is successfully returned",
+			args: args{
+				configs: config.AccountFloorFetch{
+					URL:     "",
+					Timeout: 60,
+					Period:  300,
+				},
+			},
+			response: func() []byte {
+				data := `{"data":{"currency":"USD","modelgroups":[{"modelweight":40,"modelversion":"version1","default":5,"values":{"banner|300x600|www.website.com":3,"banner|728x90|www.website.com":5,"banner|300x600|*":4,"banner|300x250|*":2,"*|*|*":16,"*|300x250|*":10,"*|300x600|*":12,"*|300x600|www.website.com":11,"banner|*|*":8,"banner|300x250|www.website.com":1,"*|728x90|www.website.com":13,"*|300x250|www.website.com":9,"*|728x90|*":14,"banner|728x90|*":6,"banner|*|www.website.com":7,"*|*|www.website.com":15},"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"}}]},"enabled":true,"floormin":1,"enforcement":{"enforcepbs":false,"floordeals":true}}`
+				return []byte(data)
+			}(),
+			responseStatus: 200,
+			want: func() []byte {
+				data := `{"data":{"currency":"USD","modelgroups":[{"modelweight":40,"modelversion":"version1","default":5,"values":{"banner|300x600|www.website.com":3,"banner|728x90|www.website.com":5,"banner|300x600|*":4,"banner|300x250|*":2,"*|*|*":16,"*|300x250|*":10,"*|300x600|*":12,"*|300x600|www.website.com":11,"banner|*|*":8,"banner|300x250|www.website.com":1,"*|728x90|www.website.com":13,"*|300x250|www.website.com":9,"*|728x90|*":14,"banner|728x90|*":6,"banner|*|www.website.com":7,"*|*|www.website.com":15},"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"}}]},"enabled":true,"floormin":1,"enforcement":{"enforcepbs":false,"floordeals":true}}`
+				return []byte(data)
+			}(),
+			want1:   0,
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mockHttpServer := httptest.NewServer(mockHandler(tt.response, tt.responseStatus))
+			defer mockHttpServer.Close()
+
+			if tt.args.configs.URL == "" {
+				tt.args.configs.URL = mockHttpServer.URL
+			}
+
+			ppf := PriceFloorFetcher{
+				httpClient: mockHttpServer.Client(),
+			}
+			got, got1, err := ppf.fetchFloorRulesFromURL(tt.args.configs)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("fetchFloorRulesFromURL() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("fetchFloorRulesFromURL() got = %v, want %v", got, tt.want)
+			}
+			if got1 != tt.want1 {
+				t.Errorf("fetchFloorRulesFromURL() got1 = %v, want %v", got1, tt.want1)
+			}
+		})
+	}
+}
+
+func TestFetchAndValidate(t *testing.T) {
+	mockHandler := func(mockResponse []byte, mockStatus int) http.Handler {
+		return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+			w.Header().Add(MaxAge, "30")
+			w.WriteHeader(mockStatus)
+			w.Write(mockResponse)
+		})
+	}
+
+	type args struct {
+		configs config.AccountFloorFetch
+	}
+	tests := []struct {
+		name           string
+		args           args
+		response       []byte
+		responseStatus int
+		want           *openrtb_ext.PriceFloorRules
+		want1          int
+	}{
+		{
+			name: "Recieved valid price floor rules response",
+			args: args{
+				configs: config.AccountFloorFetch{
+					Enabled:       true,
+					Timeout:       30,
+					MaxFileSizeKB: 700,
+					MaxRules:      30,
+					MaxAge:        60,
+					Period:        40,
+				},
+			},
+			response: func() []byte {
+				data := `{"currency":"USD","modelgroups":[{"modelweight":40,"modelversion":"version1","default":5,"values":{"banner|300x600|www.website.com":3,"banner|728x90|www.website.com":5,"banner|300x600|*":4,"banner|300x250|*":2,"*|*|*":16,"*|300x250|*":10,"*|300x600|*":12,"*|300x600|www.website.com":11,"banner|*|*":8,"banner|300x250|www.website.com":1,"*|728x90|www.website.com":13,"*|300x250|www.website.com":9,"*|728x90|*":14,"banner|728x90|*":6,"banner|*|www.website.com":7,"*|*|www.website.com":15},"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"}}]}`
+				return []byte(data)
+			}(),
+			responseStatus: 200,
+			want: func() *openrtb_ext.PriceFloorRules {
+				var res openrtb_ext.PriceFloorRules
+				data := `{"currency":"USD","modelgroups":[{"modelweight":40,"modelversion":"version1","default":5,"values":{"banner|300x600|www.website.com":3,"banner|728x90|www.website.com":5,"banner|300x600|*":4,"banner|300x250|*":2,"*|*|*":16,"*|300x250|*":10,"*|300x600|*":12,"*|300x600|www.website.com":11,"banner|*|*":8,"banner|300x250|www.website.com":1,"*|728x90|www.website.com":13,"*|300x250|www.website.com":9,"*|728x90|*":14,"banner|728x90|*":6,"banner|*|www.website.com":7,"*|*|www.website.com":15},"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"}}]}`
+				_ = json.Unmarshal([]byte(data), &res.Data)
+				return &res
+			}(),
+			want1: 30,
+		},
+		{
+			name: "No response from server",
+			args: args{
+				configs: config.AccountFloorFetch{
+					Enabled:       true,
+					Timeout:       30,
+					MaxFileSizeKB: 700,
+					MaxRules:      30,
+					MaxAge:        60,
+					Period:        40,
+				},
+			},
+			response:       []byte{},
+			responseStatus: 500,
+			want:           nil,
+			want1:          0,
+		},
+		{
+			name: "File is greater than MaxFileSize",
+			args: args{
+				configs: config.AccountFloorFetch{
+					Enabled:       true,
+					Timeout:       30,
+					MaxFileSizeKB: 1,
+					MaxRules:      30,
+					MaxAge:        60,
+					Period:        40,
+				},
+			},
+			response: func() []byte {
+				data := `{"currency":"USD","floorProvider":"PM","floorsSchemaVersion":2,"modelGroups":[{"modelVersion":"M_0","modelWeight":1,"schema":{"fields":["domain"]},"values":{"missyusa.com":0.85,"www.missyusa.com":0.7}},{"modelVersion":"M_1","modelWeight":1,"schema":{"fields":["domain"]},"values":{"missyusa.com":1,"www.missyusa.com":1.85}},{"modelVersion":"M_2","modelWeight":5,"schema":{"fields":["domain"]},"values":{"missyusa.com":1.6,"www.missyusa.com":0.7}},{"modelVersion":"M_3","modelWeight":2,"schema":{"fields":["domain"]},"values":{"missyusa.com":1.9,"www.missyusa.com":0.75}},{"modelVersion":"M_4","modelWeight":1,"schema":{"fields":["domain"]},"values":{"www.missyusa.com":1.35,"missyusa.com":1.75}},{"modelVersion":"M_5","modelWeight":2,"schema":{"fields":["domain"]},"values":{"missyusa.com":1.4,"www.missyusa.com":0.9}},{"modelVersion":"M_6","modelWeight":43,"schema":{"fields":["domain"]},"values":{"www.missyusa.com":2,"missyusa.com":2}},{"modelVersion":"M_7","modelWeight":1,"schema":{"fields":["domain"]},"values":{"missyusa.com":1.4,"www.missyusa.com":1.85}},{"modelVersion":"M_8","modelWeight":3,"schema":{"fields":["domain"]},"values":{"www.missyusa.com":1.7,"missyusa.com":0.1}},{"modelVersion":"M_9","modelWeight":7,"schema":{"fields":["domain"]},"values":{"missyusa.com":1.9,"www.missyusa.com":1.05}},{"modelVersion":"M_10","modelWeight":9,"schema":{"fields":["domain"]},"values":{"www.missyusa.com":2,"missyusa.com":0.1}},{"modelVersion":"M_11","modelWeight":1,"schema":{"fields":["domain"]},"values":{"missyusa.com":0.45,"www.missyusa.com":1.5}},{"modelVersion":"M_12","modelWeight":8,"schema":{"fields":["domain"]},"values":{"missyusa.com":1.2,"www.missyusa.com":1.7}},{"modelVersion":"M_13","modelWeight":8,"schema":{"fields":["domain"]},"values":{"missyusa.com":0.85,"www.missyusa.com":0.75}},{"modelVersion":"M_14","modelWeight":1,"schema":{"fields":["domain"]},"values":{"missyusa.com":1.8,"www.missyusa.com":1}},{"modelVersion":"M_15","modelWeight":1,"schema":{"fields":["domain"]},"values":{"www.missyusa.com":1.2,"missyusa.com":1.75}},{"modelVersion":"M_16","modelWeight":2,"schema":{"fields":["domain"]},"values":{"missyusa.com":1,"www.missyusa.com":0.7}},{"modelVersion":"M_17","modelWeight":1,"schema":{"fields":["domain"]},"values":{"missyusa.com":0.45,"www.missyusa.com":0.35}},{"modelVersion":"M_18","modelWeight":3,"schema":{"fields":["domain"]},"values":{"missyusa.com":1.2,"www.missyusa.com":1.05}}],"skipRate":10}`
+				return []byte(data)
+			}(),
+			responseStatus: 200,
+			want:           nil,
+			want1:          0,
+		},
+		{
+			name: "Malformed response : json unmarshalling failed",
+			args: args{
+				configs: config.AccountFloorFetch{
+					Enabled:       true,
+					Timeout:       30,
+					MaxFileSizeKB: 800,
+					MaxRules:      30,
+					MaxAge:        60,
+					Period:        40,
+				},
+			},
+			response: func() []byte {
+				data := `{"data":nil?}`
+				return []byte(data)
+			}(),
+			responseStatus: 200,
+			want:           nil,
+			want1:          0,
+		},
+		{
+			name: "Validations failed for price floor rules response",
+			args: args{
+				configs: config.AccountFloorFetch{
+					Enabled:       true,
+					Timeout:       30,
+					MaxFileSizeKB: 700,
+					MaxRules:      30,
+					MaxAge:        60,
+					Period:        40,
+				},
+			},
+			response: func() []byte {
+				data := `{"data":{"currency":"USD","modelgroups":[]},"enabled":true,"floormin":1,"enforcement":{"enforcepbs":false,"floordeals":true}}`
+				return []byte(data)
+			}(),
+			responseStatus: 200,
+			want:           nil,
+			want1:          0,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mockHttpServer := httptest.NewServer(mockHandler(tt.response, tt.responseStatus))
+			defer mockHttpServer.Close()
+			ppf := PriceFloorFetcher{
+				httpClient: mockHttpServer.Client(),
+			}
+			tt.args.configs.URL = mockHttpServer.URL
+			got, got1 := ppf.fetchAndValidate(tt.args.configs)
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("fetchAndValidate() got = %v, want %v", got, tt.want)
+			}
+			if got1 != tt.want1 {
+				t.Errorf("fetchAndValidate() got1 = %v, want %v", got1, tt.want1)
+			}
+		})
+	}
+}
+
+func mockFetcherInstance(config config.PriceFloors, httpClient *http.Client, metricEngine metrics.MetricsEngine) *PriceFloorFetcher {
+	if !config.Enabled {
+		return nil
+	}
+
+	floorFetcher := PriceFloorFetcher{
+		pool:            pond.New(config.Fetcher.Worker, config.Fetcher.Capacity, pond.PanicHandler(workerPanicHandler)),
+		fetchQueue:      make(FetchQueue, 0, 100),
+		fetchInProgress: make(map[string]bool),
+		configReceiver:  make(chan fetchInfo, config.Fetcher.Capacity),
+		done:            make(chan struct{}),
+		cache:           freecache.NewCache(config.Fetcher.CacheSize * 1024 * 1024),
+		httpClient:      httpClient,
+		time:            &timeutil.RealTime{},
+		metricEngine:    metricEngine,
+		maxRetries:      10,
+	}
+
+	go floorFetcher.Fetcher()
+
+	return &floorFetcher
+}
+
+func TestFetcherWhenRequestGetSameURLInrequest(t *testing.T) {
+	refetchCheckInterval = 1
+	response := []byte(`{"currency":"USD","modelgroups":[{"modelweight":40,"modelversion":"version1","default":5,"values":{"banner|300x600|www.website.com":3,"banner|728x90|www.website.com":5,"banner|300x600|*":4,"banner|300x250|*":2,"*|*|*":16,"*|300x250|*":10,"*|300x600|*":12,"*|300x600|www.website.com":11,"banner|*|*":8,"banner|300x250|www.website.com":1,"*|728x90|www.website.com":13,"*|300x250|www.website.com":9,"*|728x90|*":14,"banner|728x90|*":6,"banner|*|www.website.com":7,"*|*|www.website.com":15},"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"}}]}`)
+	mockHandler := func(mockResponse []byte, mockStatus int) http.Handler {
+		return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+			w.WriteHeader(mockStatus)
+			w.Write(mockResponse)
+		})
+	}
+
+	mockHttpServer := httptest.NewServer(mockHandler(response, 200))
+	defer mockHttpServer.Close()
+
+	floorConfig := config.PriceFloors{
+		Enabled: true,
+		Fetcher: config.PriceFloorFetcher{
+			CacheSize: 1,
+			Worker:    5,
+			Capacity:  10,
+		},
+	}
+	fetcherInstance := mockFetcherInstance(floorConfig, mockHttpServer.Client(), &metricsConf.NilMetricsEngine{})
+	defer fetcherInstance.Stop()
+
+	fetchConfig := config.AccountPriceFloors{
+		Enabled:        true,
+		UseDynamicData: true,
+		Fetcher: config.AccountFloorFetch{
+			Enabled:       true,
+			URL:           mockHttpServer.URL,
+			Timeout:       100,
+			MaxFileSizeKB: 1000,
+			MaxRules:      100,
+			MaxAge:        20,
+			Period:        1,
+		},
+	}
+
+	for i := 0; i < 50; i++ {
+		fetcherInstance.Fetch(fetchConfig)
+	}
+
+	assert.Never(t, func() bool { return len(fetcherInstance.fetchQueue) > 1 }, time.Duration(2*time.Second), 100*time.Millisecond, "Queue Got more than one entry")
+	assert.Never(t, func() bool { return len(fetcherInstance.fetchInProgress) > 1 }, time.Duration(2*time.Second), 100*time.Millisecond, "Map Got more than one entry")
+
+}
+
+func TestFetcherDataPresentInCache(t *testing.T) {
+	floorConfig := config.PriceFloors{
+		Enabled: true,
+		Fetcher: config.PriceFloorFetcher{
+			CacheSize: 1,
+			Worker:    2,
+			Capacity:  5,
+		},
+	}
+
+	fetcherInstance := mockFetcherInstance(floorConfig, http.DefaultClient, &metricsConf.NilMetricsEngine{})
+	defer fetcherInstance.Stop()
+
+	fetchConfig := config.AccountPriceFloors{
+		Enabled:        true,
+		UseDynamicData: true,
+		Fetcher: config.AccountFloorFetch{
+			Enabled:       true,
+			URL:           "http://test.com/floor",
+			Timeout:       100,
+			MaxFileSizeKB: 1000,
+			MaxRules:      100,
+			MaxAge:        20,
+			Period:        5,
+		},
+	}
+	var res *openrtb_ext.PriceFloorRules
+	data := `{"data":{"currency":"USD","modelgroups":[{"modelweight":40,"modelversion":"version1","default":5,"values":{"banner|300x600|www.website.com":3,"banner|728x90|www.website.com":5,"banner|300x600|*":4,"banner|300x250|*":2,"*|*|*":16,"*|300x250|*":10,"*|300x600|*":12,"*|300x600|www.website.com":11,"banner|*|*":8,"banner|300x250|www.website.com":1,"*|728x90|www.website.com":13,"*|300x250|www.website.com":9,"*|728x90|*":14,"banner|728x90|*":6,"banner|*|www.website.com":7,"*|*|www.website.com":15},"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"}}]},"enabled":true,"floormin":1,"enforcement":{"enforcepbs":false,"floordeals":true}}`
+	_ = json.Unmarshal([]byte(data), &res)
+	fetcherInstance.SetWithExpiry("http://test.com/floor", []byte(data), fetchConfig.Fetcher.MaxAge)
+
+	val, status := fetcherInstance.Fetch(fetchConfig)
+	assert.Equal(t, res, val, "Invalid value in cache or cache is empty")
+	assert.Equal(t, "success", status, "Floor fetch should be success")
+}
+
+func TestFetcherDataNotPresentInCache(t *testing.T) {
+	floorConfig := config.PriceFloors{
+		Enabled: true,
+		Fetcher: config.PriceFloorFetcher{
+			CacheSize: 1,
+			Worker:    2,
+			Capacity:  5,
+		},
+	}
+
+	fetcherInstance := mockFetcherInstance(floorConfig, http.DefaultClient, &metricsConf.NilMetricsEngine{})
+	defer fetcherInstance.Stop()
+
+	fetchConfig := config.AccountPriceFloors{
+		Enabled:        true,
+		UseDynamicData: true,
+		Fetcher: config.AccountFloorFetch{
+			Enabled:       true,
+			URL:           "http://test.com/floor",
+			Timeout:       100,
+			MaxFileSizeKB: 1000,
+			MaxRules:      100,
+			MaxAge:        20,
+			Period:        5,
+		},
+	}
+	fetcherInstance.SetWithExpiry("http://test.com/floor", nil, fetchConfig.Fetcher.MaxAge)
+
+	val, status := fetcherInstance.Fetch(fetchConfig)
+
+	assert.Equal(t, (*openrtb_ext.PriceFloorRules)(nil), val, "Floor data should be nil")
+	assert.Equal(t, "error", status, "Floor fetch should be error")
+}
+
+func TestFetcherEntryNotPresentInCache(t *testing.T) {
+	floorConfig := config.PriceFloors{
+		Enabled: true,
+		Fetcher: config.PriceFloorFetcher{
+			CacheSize:  1,
+			Worker:     2,
+			Capacity:   5,
+			MaxRetries: 10,
+		},
+	}
+
+	fetcherInstance := NewPriceFloorFetcher(floorConfig, http.DefaultClient, &metricsConf.NilMetricsEngine{})
+	defer fetcherInstance.Stop()
+
+	fetchConfig := config.AccountPriceFloors{
+		Enabled:        true,
+		UseDynamicData: true,
+		Fetcher: config.AccountFloorFetch{
+			Enabled:       true,
+			URL:           "http://test.com/floor",
+			Timeout:       100,
+			MaxFileSizeKB: 1000,
+			MaxRules:      100,
+			MaxAge:        20,
+			Period:        5,
+		},
+	}
+
+	val, status := fetcherInstance.Fetch(fetchConfig)
+
+	assert.Equal(t, (*openrtb_ext.PriceFloorRules)(nil), val, "Floor data should be nil")
+	assert.Equal(t, openrtb_ext.FetchInprogress, status, "Floor fetch should be error")
+}
+
+func TestFetcherDynamicFetchDisable(t *testing.T) {
+	floorConfig := config.PriceFloors{
+		Enabled: true,
+		Fetcher: config.PriceFloorFetcher{
+			CacheSize:  1,
+			Worker:     2,
+			Capacity:   5,
+			MaxRetries: 5,
+		},
+	}
+
+	fetcherInstance := NewPriceFloorFetcher(floorConfig, http.DefaultClient, &metricsConf.NilMetricsEngine{})
+	defer fetcherInstance.Stop()
+
+	fetchConfig := config.AccountPriceFloors{
+		Enabled:        true,
+		UseDynamicData: false,
+		Fetcher: config.AccountFloorFetch{
+			Enabled:       true,
+			URL:           "http://test.com/floor",
+			Timeout:       100,
+			MaxFileSizeKB: 1000,
+			MaxRules:      100,
+			MaxAge:        20,
+			Period:        5,
+		},
+	}
+
+	val, status := fetcherInstance.Fetch(fetchConfig)
+
+	assert.Equal(t, (*openrtb_ext.PriceFloorRules)(nil), val, "Floor data should be nil")
+	assert.Equal(t, openrtb_ext.FetchNone, status, "Floor fetch should be error")
+}
+
+func TestPriceFloorFetcherWorker(t *testing.T) {
+	var floorData openrtb_ext.PriceFloorData
+	response := []byte(`{"currency":"USD","modelgroups":[{"modelweight":40,"modelversion":"version1","default":5,"values":{"banner|300x600|www.website.com":3,"banner|728x90|www.website.com":5,"banner|300x600|*":4,"banner|300x250|*":2,"*|*|*":16,"*|300x250|*":10,"*|300x600|*":12,"*|300x600|www.website.com":11,"banner|*|*":8,"banner|300x250|www.website.com":1,"*|728x90|www.website.com":13,"*|300x250|www.website.com":9,"*|728x90|*":14,"banner|728x90|*":6,"banner|*|www.website.com":7,"*|*|www.website.com":15},"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"}}]}`)
+	_ = json.Unmarshal(response, &floorData)
+	floorResp := &openrtb_ext.PriceFloorRules{
+		Data: &floorData,
+	}
+
+	mockHandler := func(mockResponse []byte, mockStatus int) http.Handler {
+		return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+			w.Header().Add(MaxAge, "5")
+			w.WriteHeader(mockStatus)
+			w.Write(mockResponse)
+		})
+	}
+
+	mockHttpServer := httptest.NewServer(mockHandler(response, 200))
+	defer mockHttpServer.Close()
+
+	fetcherInstance := PriceFloorFetcher{
+		pool:            nil,
+		fetchQueue:      nil,
+		fetchInProgress: nil,
+		configReceiver:  make(chan fetchInfo, 1),
+		done:            nil,
+		cache:           freecache.NewCache(1 * 1024 * 1024),
+		httpClient:      mockHttpServer.Client(),
+		time:            &timeutil.RealTime{},
+		metricEngine:    &metricsConf.NilMetricsEngine{},
+		maxRetries:      10,
+	}
+	defer close(fetcherInstance.configReceiver)
+
+	fetchConfig := fetchInfo{
+		AccountFloorFetch: config.AccountFloorFetch{
+			Enabled:       true,
+			URL:           mockHttpServer.URL,
+			Timeout:       100,
+			MaxFileSizeKB: 1000,
+			MaxRules:      100,
+			MaxAge:        20,
+			Period:        1,
+		},
+	}
+
+	fetcherInstance.worker(fetchConfig)
+	dataInCache, _ := fetcherInstance.Get(mockHttpServer.URL)
+	var gotFloorData *openrtb_ext.PriceFloorRules
+	json.Unmarshal(dataInCache, &gotFloorData)
+	assert.Equal(t, floorResp, gotFloorData, "Data should be stored in cache")
+
+	info := <-fetcherInstance.configReceiver
+	assert.Equal(t, true, info.refetchRequest, "Recieved request is not refetch request")
+	assert.Equal(t, mockHttpServer.URL, info.AccountFloorFetch.URL, "Recieved request with different url")
+
+}
+
+func TestPriceFloorFetcherWorkerRetry(t *testing.T) {
+	mockHandler := func(mockResponse []byte, mockStatus int) http.Handler {
+		return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+			w.WriteHeader(mockStatus)
+			w.Write(mockResponse)
+		})
+	}
+
+	mockHttpServer := httptest.NewServer(mockHandler(nil, 500))
+	defer mockHttpServer.Close()
+
+	fetcherInstance := PriceFloorFetcher{
+		pool:            nil,
+		fetchQueue:      nil,
+		fetchInProgress: nil,
+		configReceiver:  make(chan fetchInfo, 1),
+		done:            nil,
+		cache:           nil,
+		httpClient:      mockHttpServer.Client(),
+		time:            &timeutil.RealTime{},
+		metricEngine:    &metricsConf.NilMetricsEngine{},
+		maxRetries:      5,
+	}
+	defer close(fetcherInstance.configReceiver)
+
+	fetchConfig := fetchInfo{
+		AccountFloorFetch: config.AccountFloorFetch{
+			Enabled:       true,
+			URL:           mockHttpServer.URL,
+			Timeout:       100,
+			MaxFileSizeKB: 1000,
+			MaxRules:      100,
+			MaxAge:        20,
+			Period:        1,
+		},
+	}
+
+	fetcherInstance.worker(fetchConfig)
+
+	info := <-fetcherInstance.configReceiver
+	assert.Equal(t, 1, info.retryCount, "Retry Count is not 1")
+}
+
+func TestPriceFloorFetcherWorkerDefaultCacheExpiry(t *testing.T) {
+	var floorData openrtb_ext.PriceFloorData
+	response := []byte(`{"currency":"USD","modelgroups":[{"modelweight":40,"modelversion":"version1","default":5,"values":{"banner|300x600|www.website.com":3,"banner|728x90|www.website.com":5,"banner|300x600|*":4,"banner|300x250|*":2,"*|*|*":16,"*|300x250|*":10,"*|300x600|*":12,"*|300x600|www.website.com":11,"banner|*|*":8,"banner|300x250|www.website.com":1,"*|728x90|www.website.com":13,"*|300x250|www.website.com":9,"*|728x90|*":14,"banner|728x90|*":6,"banner|*|www.website.com":7,"*|*|www.website.com":15},"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"}}]}`)
+	_ = json.Unmarshal(response, &floorData)
+	floorResp := &openrtb_ext.PriceFloorRules{
+		Data: &floorData,
+	}
+
+	mockHandler := func(mockResponse []byte, mockStatus int) http.Handler {
+		return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+			w.WriteHeader(mockStatus)
+			w.Write(mockResponse)
+		})
+	}
+
+	mockHttpServer := httptest.NewServer(mockHandler(response, 200))
+	defer mockHttpServer.Close()
+
+	fetcherInstance := &PriceFloorFetcher{
+		pool:            nil,
+		fetchQueue:      nil,
+		fetchInProgress: nil,
+		configReceiver:  make(chan fetchInfo, 1),
+		done:            nil,
+		cache:           freecache.NewCache(1 * 1024 * 1024),
+		httpClient:      mockHttpServer.Client(),
+		time:            &timeutil.RealTime{},
+		metricEngine:    &metricsConf.NilMetricsEngine{},
+		maxRetries:      5,
+	}
+
+	fetchConfig := fetchInfo{
+		AccountFloorFetch: config.AccountFloorFetch{
+			Enabled:       true,
+			URL:           mockHttpServer.URL,
+			Timeout:       100,
+			MaxFileSizeKB: 1000,
+			MaxRules:      100,
+			MaxAge:        20,
+			Period:        1,
+		},
+	}
+
+	fetcherInstance.worker(fetchConfig)
+	dataInCache, _ := fetcherInstance.Get(mockHttpServer.URL)
+	var gotFloorData *openrtb_ext.PriceFloorRules
+	json.Unmarshal(dataInCache, &gotFloorData)
+	assert.Equal(t, floorResp, gotFloorData, "Data should be stored in cache")
+
+	info := <-fetcherInstance.configReceiver
+	defer close(fetcherInstance.configReceiver)
+	assert.Equal(t, true, info.refetchRequest, "Recieved request is not refetch request")
+	assert.Equal(t, mockHttpServer.URL, info.AccountFloorFetch.URL, "Recieved request with different url")
+
+}
+
+func TestPriceFloorFetcherSubmit(t *testing.T) {
+	response := []byte(`{"currency":"USD","modelgroups":[{"modelweight":40,"modelversion":"version1","default":5,"values":{"banner|300x600|www.website.com":3,"banner|728x90|www.website.com":5,"banner|300x600|*":4,"banner|300x250|*":2,"*|*|*":16,"*|300x250|*":10,"*|300x600|*":12,"*|300x600|www.website.com":11,"banner|*|*":8,"banner|300x250|www.website.com":1,"*|728x90|www.website.com":13,"*|300x250|www.website.com":9,"*|728x90|*":14,"banner|728x90|*":6,"banner|*|www.website.com":7,"*|*|www.website.com":15},"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"}}]}`)
+	mockHandler := func(mockResponse []byte, mockStatus int) http.Handler {
+		return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+			w.WriteHeader(mockStatus)
+			w.Write(mockResponse)
+		})
+	}
+
+	mockHttpServer := httptest.NewServer(mockHandler(response, 200))
+	defer mockHttpServer.Close()
+
+	fetcherInstance := &PriceFloorFetcher{
+		pool:            pond.New(1, 1),
+		fetchQueue:      make(FetchQueue, 0),
+		fetchInProgress: nil,
+		configReceiver:  make(chan fetchInfo, 1),
+		done:            make(chan struct{}),
+		cache:           freecache.NewCache(1 * 1024 * 1024),
+		httpClient:      mockHttpServer.Client(),
+		time:            &timeutil.RealTime{},
+		metricEngine:    &metricsConf.NilMetricsEngine{},
+		maxRetries:      5,
+	}
+	defer fetcherInstance.Stop()
+
+	fetchInfo := fetchInfo{
+		refetchRequest: false,
+		fetchTime:      time.Now().Unix(),
+		AccountFloorFetch: config.AccountFloorFetch{
+			Enabled:       true,
+			URL:           mockHttpServer.URL,
+			Timeout:       100,
+			MaxFileSizeKB: 1000,
+			MaxRules:      100,
+			MaxAge:        2,
+			Period:        1,
+		},
+	}
+
+	fetcherInstance.submit(&fetchInfo)
+
+	info := <-fetcherInstance.configReceiver
+	assert.Equal(t, true, info.refetchRequest, "Recieved request is not refetch request")
+	assert.Equal(t, mockHttpServer.URL, info.AccountFloorFetch.URL, "Recieved request with different url")
+
+}
+
+type testPool struct{}
+
+func (t *testPool) TrySubmit(task func()) bool {
+	return false
+}
+
+func (t *testPool) Stop() {}
+
+func TestPriceFloorFetcherSubmitFailed(t *testing.T) {
+	fetcherInstance := &PriceFloorFetcher{
+		pool:            &testPool{},
+		fetchQueue:      make(FetchQueue, 0),
+		fetchInProgress: nil,
+		configReceiver:  nil,
+		done:            nil,
+		cache:           nil,
+	}
+	defer fetcherInstance.pool.Stop()
+
+	fetchInfo := fetchInfo{
+		refetchRequest: false,
+		fetchTime:      time.Now().Unix(),
+		AccountFloorFetch: config.AccountFloorFetch{
+			Enabled:       true,
+			URL:           "http://test.com",
+			Timeout:       100,
+			MaxFileSizeKB: 1000,
+			MaxRules:      100,
+			MaxAge:        2,
+			Period:        1,
+		},
+	}
+
+	fetcherInstance.submit(&fetchInfo)
+	assert.Equal(t, 1, len(fetcherInstance.fetchQueue), "Unable to submit the task")
+}
+
+func getRandomNumber() int {
+	rand.Seed(time.Now().UnixNano())
+	min := 1
+	max := 10
+	return rand.Intn(max-min+1) + min
+}
+
+func TestFetcherWhenRequestGetDifferentURLInrequest(t *testing.T) {
+	refetchCheckInterval = 1
+	response := []byte(`{"currency":"USD","modelgroups":[{"modelweight":40,"modelversion":"version1","default":5,"values":{"banner|300x600|www.website.com":3,"banner|728x90|www.website.com":5,"banner|300x600|*":4,"banner|300x250|*":2,"*|*|*":16,"*|300x250|*":10,"*|300x600|*":12,"*|300x600|www.website.com":11,"banner|*|*":8,"banner|300x250|www.website.com":1,"*|728x90|www.website.com":13,"*|300x250|www.website.com":9,"*|728x90|*":14,"banner|728x90|*":6,"banner|*|www.website.com":7,"*|*|www.website.com":15},"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"}}]}`)
+	mockHandler := func(mockResponse []byte, mockStatus int) http.Handler {
+		return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+			w.WriteHeader(mockStatus)
+			w.Write(mockResponse)
+		})
+	}
+
+	mockHttpServer := httptest.NewServer(mockHandler(response, 200))
+	defer mockHttpServer.Close()
+
+	floorConfig := config.PriceFloors{
+		Enabled: true,
+		Fetcher: config.PriceFloorFetcher{
+			CacheSize:  1,
+			Worker:     5,
+			Capacity:   10,
+			MaxRetries: 5,
+		},
+	}
+	fetcherInstance := mockFetcherInstance(floorConfig, mockHttpServer.Client(), &metricsConf.NilMetricsEngine{})
+	defer fetcherInstance.Stop()
+
+	fetchConfig := config.AccountPriceFloors{
+		Enabled:        true,
+		UseDynamicData: true,
+		Fetcher: config.AccountFloorFetch{
+			Enabled:       true,
+			URL:           mockHttpServer.URL,
+			Timeout:       100,
+			MaxFileSizeKB: 1000,
+			MaxRules:      100,
+			MaxAge:        5,
+			Period:        1,
+		},
+	}
+
+	for i := 0; i < 50; i++ {
+		fetchConfig.Fetcher.URL = fmt.Sprintf("%s?id=%d", mockHttpServer.URL, getRandomNumber())
+		fetcherInstance.Fetch(fetchConfig)
+	}
+
+	assert.Never(t, func() bool { return len(fetcherInstance.fetchQueue) > 10 }, time.Duration(2*time.Second), 100*time.Millisecond, "Queue Got more than one entry")
+	assert.Never(t, func() bool { return len(fetcherInstance.fetchInProgress) > 10 }, time.Duration(2*time.Second), 100*time.Millisecond, "Map Got more than one entry")
+}
+
+func TestFetchWhenPriceFloorsDisabled(t *testing.T) {
+	floorConfig := config.PriceFloors{
+		Enabled: false,
+		Fetcher: config.PriceFloorFetcher{
+			CacheSize: 1,
+			Worker:    5,
+			Capacity:  10,
+		},
+	}
+	fetcherInstance := NewPriceFloorFetcher(floorConfig, http.DefaultClient, &metricsConf.NilMetricsEngine{})
+	defer fetcherInstance.Stop()
+
+	fetchConfig := config.AccountPriceFloors{
+		Enabled:        true,
+		UseDynamicData: true,
+		Fetcher: config.AccountFloorFetch{
+			Enabled:       true,
+			URL:           "http://test.com/floors",
+			Timeout:       100,
+			MaxFileSizeKB: 1000,
+			MaxRules:      100,
+			MaxAge:        5,
+			Period:        1,
+		},
+	}
+
+	data, status := fetcherInstance.Fetch(fetchConfig)
+
+	assert.Equal(t, (*openrtb_ext.PriceFloorRules)(nil), data, "floor data should be nil as fetcher instance does not created")
+	assert.Equal(t, openrtb_ext.FetchNone, status, "floor status should be none as fetcher instance does not created")
+}
diff --git a/floors/floors_test.go b/floors/floors_test.go
index 56b21a1a..047b6738 100644
--- a/floors/floors_test.go
+++ b/floors/floors_test.go
@@ -3,6 +3,7 @@ package floors
 import (
 	"encoding/json"
 	"errors"
+	"reflect"
 	"testing"
 
 	"github.com/prebid/openrtb/v19/openrtb2"
@@ -50,6 +51,14 @@ func getCurrencyRates(rates map[string]map[string]float64) currency.Conversions
 	return currency.NewRates(rates)
 }
 
+type mockPriceFloorFetcher struct{}
+
+func (mpf *mockPriceFloorFetcher) Fetch(configs config.AccountPriceFloors) (*openrtb_ext.PriceFloorRules, string) {
+	return nil, openrtb_ext.FetchNone
+}
+
+func (mpf *mockPriceFloorFetcher) Stop() {}
+
 func TestEnrichWithPriceFloors(t *testing.T) {
 	rates := map[string]map[string]float64{
 		"USD": {
@@ -365,7 +374,7 @@ func TestEnrichWithPriceFloors(t *testing.T) {
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
 
-			ErrList := EnrichWithPriceFloors(tc.bidRequestWrapper, tc.account, getCurrencyRates(rates))
+			ErrList := EnrichWithPriceFloors(tc.bidRequestWrapper, tc.account, getCurrencyRates(rates), &mockPriceFloorFetcher{})
 			if tc.bidRequestWrapper != nil {
 				assert.Equal(t, tc.bidRequestWrapper.Imp[0].BidFloor, tc.expFloorVal, tc.name)
 				assert.Equal(t, tc.bidRequestWrapper.Imp[0].BidFloorCur, tc.expFloorCur, tc.name)
@@ -393,6 +402,51 @@ func getTrue() *bool {
 	return &trueFlag
 }
 
+func getFalse() *bool {
+	falseFlag := false
+	return &falseFlag
+}
+
+type MockFetch struct {
+	FakeFetch func(configs config.AccountPriceFloors) (*openrtb_ext.PriceFloorRules, string)
+}
+
+func (m *MockFetch) Stop() {}
+
+func (m *MockFetch) Fetch(configs config.AccountPriceFloors) (*openrtb_ext.PriceFloorRules, string) {
+
+	if !configs.UseDynamicData {
+		return nil, openrtb_ext.FetchNone
+	}
+	priceFloors := openrtb_ext.PriceFloorRules{
+		Enabled:            getTrue(),
+		PriceFloorLocation: openrtb_ext.RequestLocation,
+		Enforcement: &openrtb_ext.PriceFloorEnforcement{
+			EnforcePBS:  getTrue(),
+			EnforceRate: 100,
+			FloorDeals:  getTrue(),
+		},
+		Data: &openrtb_ext.PriceFloorData{
+			Currency: "USD",
+			ModelGroups: []openrtb_ext.PriceFloorModelGroup{
+				{
+					ModelVersion: "Version 101",
+					Currency:     "USD",
+					Values: map[string]float64{
+						"banner|300x600|www.website5.com": 15,
+						"*|*|*":                           25,
+					},
+					Schema: openrtb_ext.PriceFloorSchema{
+						Fields:    []string{"mediaType", "size", "domain"},
+						Delimiter: "|",
+					},
+				},
+			},
+		},
+	}
+	return &priceFloors, openrtb_ext.FetchSuccess
+}
+
 func TestResolveFloors(t *testing.T) {
 	rates := map[string]map[string]float64{
 		"USD": {
@@ -407,9 +461,149 @@ func TestResolveFloors(t *testing.T) {
 		bidRequestWrapper *openrtb_ext.RequestWrapper
 		account           config.Account
 		conversions       currency.Conversions
+		fetcher           FloorFetcher
 		expErr            []error
 		expFloors         *openrtb_ext.PriceFloorRules
 	}{
+		{
+			name: "Dynamic fetch disabled, floors from request selected",
+			bidRequestWrapper: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Site: &openrtb2.Site{
+						Publisher: &openrtb2.Publisher{Domain: "www.website.com"},
+					},
+					Imp: []openrtb2.Imp{{ID: "1234", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}}},
+					Ext: json.RawMessage(`{"prebid":{"floors":{"data":{"currency":"USD","modelgroups":[{"modelversion":"model 1 from req","currency":"USD","values":{"banner|300x600|www.website5.com":5,"*|*|*":7},"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"}}]},"enabled":true,"enforcement":{"enforcepbs":true,"floordeals":true,"enforcerate":100}}}}`),
+				},
+			},
+			account: config.Account{
+				PriceFloors: config.AccountPriceFloors{
+					Enabled:        true,
+					UseDynamicData: false,
+				},
+			},
+			fetcher: &MockFetch{},
+			expFloors: &openrtb_ext.PriceFloorRules{
+				Enabled:            getTrue(),
+				FetchStatus:        openrtb_ext.FetchNone,
+				PriceFloorLocation: openrtb_ext.RequestLocation,
+				Enforcement: &openrtb_ext.PriceFloorEnforcement{
+					EnforcePBS:  getTrue(),
+					EnforceRate: 100,
+					FloorDeals:  getTrue(),
+				},
+				Data: &openrtb_ext.PriceFloorData{
+					Currency: "USD",
+					ModelGroups: []openrtb_ext.PriceFloorModelGroup{
+						{
+							ModelVersion: "model 1 from req",
+							Currency:     "USD",
+							Values: map[string]float64{
+								"banner|300x600|www.website5.com": 5,
+								"*|*|*":                           7,
+							},
+							Schema: openrtb_ext.PriceFloorSchema{
+								Fields:    []string{"mediaType", "size", "domain"},
+								Delimiter: "|",
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "Dynamic fetch enabled, floors from fetched selected",
+			bidRequestWrapper: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Site: &openrtb2.Site{
+						Publisher: &openrtb2.Publisher{Domain: "www.website.com"},
+					},
+					Imp: []openrtb2.Imp{{ID: "1234", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}}},
+				},
+			},
+			account: config.Account{
+				PriceFloors: config.AccountPriceFloors{
+					Enabled:        true,
+					UseDynamicData: true,
+				},
+			},
+			fetcher: &MockFetch{},
+			expFloors: &openrtb_ext.PriceFloorRules{
+				Enabled:            getTrue(),
+				FetchStatus:        openrtb_ext.FetchSuccess,
+				PriceFloorLocation: openrtb_ext.FetchLocation,
+				Enforcement: &openrtb_ext.PriceFloorEnforcement{
+					EnforcePBS:  getTrue(),
+					FloorDeals:  getTrue(),
+					EnforceRate: 100,
+				},
+				Data: &openrtb_ext.PriceFloorData{
+					Currency: "USD",
+					ModelGroups: []openrtb_ext.PriceFloorModelGroup{
+						{
+							ModelVersion: "Version 101",
+							Currency:     "USD",
+							Values: map[string]float64{
+								"banner|300x600|www.website5.com": 15,
+								"*|*|*":                           25,
+							},
+							Schema: openrtb_ext.PriceFloorSchema{
+								Fields:    []string{"mediaType", "size", "domain"},
+								Delimiter: "|",
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "Dynamic fetch enabled, floors formed after merging",
+			bidRequestWrapper: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Site: &openrtb2.Site{
+						Publisher: &openrtb2.Publisher{Domain: "www.website.com"},
+					},
+					Imp: []openrtb2.Imp{{ID: "1234", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}}},
+					Ext: json.RawMessage(`{"prebid":{"floors":{"floormincur":"EUR","enabled":true,"data":{"currency":"USD","modelgroups":[{"modelversion":"model 1 from req","currency":"USD","values":{"banner|300x600|www.website5.com":5,"*|*|*":7},"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"}}]},"floormin":10.11,"enforcement":{"enforcepbs":true,"floordeals":true,"enforcerate":100}}}}`),
+				},
+			},
+			account: config.Account{
+				PriceFloors: config.AccountPriceFloors{
+					Enabled:        true,
+					UseDynamicData: true,
+				},
+			},
+			fetcher: &MockFetch{},
+			expFloors: &openrtb_ext.PriceFloorRules{
+				Enabled:            getTrue(),
+				FloorMin:           10.11,
+				FloorMinCur:        "EUR",
+				FetchStatus:        openrtb_ext.FetchSuccess,
+				PriceFloorLocation: openrtb_ext.FetchLocation,
+				Enforcement: &openrtb_ext.PriceFloorEnforcement{
+					EnforcePBS:  getTrue(),
+					EnforceRate: 100,
+					FloorDeals:  getTrue(),
+				},
+				Data: &openrtb_ext.PriceFloorData{
+					Currency: "USD",
+					ModelGroups: []openrtb_ext.PriceFloorModelGroup{
+						{
+							ModelVersion: "Version 101",
+							Currency:     "USD",
+							Values: map[string]float64{
+								"banner|300x600|www.website5.com": 15,
+								"*|*|*":                           25,
+							},
+							Schema: openrtb_ext.PriceFloorSchema{
+								Fields:    []string{"mediaType", "size", "domain"},
+								Delimiter: "|",
+							},
+						},
+					},
+				},
+			},
+		},
 		{
 			name: "Dynamic fetch disabled, only enforcement object present in req.ext",
 			bidRequestWrapper: &openrtb_ext.RequestWrapper{
@@ -427,6 +621,7 @@ func TestResolveFloors(t *testing.T) {
 					UseDynamicData: false,
 				},
 			},
+			fetcher: &MockFetch{},
 			expFloors: &openrtb_ext.PriceFloorRules{
 				Enforcement: &openrtb_ext.PriceFloorEnforcement{
 					EnforcePBS:  getTrue(),
@@ -437,11 +632,129 @@ func TestResolveFloors(t *testing.T) {
 				PriceFloorLocation: openrtb_ext.RequestLocation,
 			},
 		},
+		{
+			name: "Dynamic fetch enabled, floors from fetched selected and new URL is updated",
+			bidRequestWrapper: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Site: &openrtb2.Site{
+						Publisher: &openrtb2.Publisher{Domain: "www.website.com"},
+					},
+					Imp: []openrtb2.Imp{{ID: "1234", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}}},
+					Ext: json.RawMessage(`{"prebid":{"floors":{"floorendpoint":{"url":"http://test.com/floor"},"enabled":true}}}`),
+				},
+			},
+			account: config.Account{
+				PriceFloors: config.AccountPriceFloors{
+					Enabled:        true,
+					UseDynamicData: true,
+				},
+			},
+			fetcher: &MockFetch{},
+			expFloors: &openrtb_ext.PriceFloorRules{
+				Enabled:            getTrue(),
+				FetchStatus:        openrtb_ext.FetchSuccess,
+				PriceFloorLocation: openrtb_ext.FetchLocation,
+				Enforcement: &openrtb_ext.PriceFloorEnforcement{
+					EnforcePBS:  getTrue(),
+					FloorDeals:  getTrue(),
+					EnforceRate: 100,
+				},
+				Location: &openrtb_ext.PriceFloorEndpoint{
+					URL: "http://test.com/floor",
+				},
+				Data: &openrtb_ext.PriceFloorData{
+					Currency: "USD",
+					ModelGroups: []openrtb_ext.PriceFloorModelGroup{
+						{
+							ModelVersion: "Version 101",
+							Currency:     "USD",
+							Values: map[string]float64{
+								"banner|300x600|www.website5.com": 15,
+								"*|*|*":                           25,
+							},
+							Schema: openrtb_ext.PriceFloorSchema{
+								Fields:    []string{"mediaType", "size", "domain"},
+								Delimiter: "|",
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "Dynamic Fetch Enabled but price floor fetcher is nil, floors from request is selected",
+			bidRequestWrapper: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Site: &openrtb2.Site{
+						Publisher: &openrtb2.Publisher{Domain: "www.website.com"},
+					},
+					Imp: []openrtb2.Imp{{ID: "1234", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}}},
+					Ext: json.RawMessage(`{"prebid":{"floors":{"data":{"currency":"USD","modelgroups":[{"modelversion":"model 1 from req","currency":"USD","values":{"banner|300x600|www.website5.com":5,"*|*|*":7},"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"}}]},"enabled":true,"enforcement":{"enforcepbs":true,"floordeals":true,"enforcerate":100}}}}`),
+				},
+			},
+			account: config.Account{
+				PriceFloors: config.AccountPriceFloors{
+					Enabled:        true,
+					UseDynamicData: true,
+				},
+			},
+			fetcher: nil,
+			expFloors: &openrtb_ext.PriceFloorRules{
+				Enabled:            getTrue(),
+				FetchStatus:        openrtb_ext.FetchNone,
+				PriceFloorLocation: openrtb_ext.RequestLocation,
+				Enforcement: &openrtb_ext.PriceFloorEnforcement{
+					EnforcePBS:  getTrue(),
+					EnforceRate: 100,
+					FloorDeals:  getTrue(),
+				},
+				Data: &openrtb_ext.PriceFloorData{
+					Currency: "USD",
+					ModelGroups: []openrtb_ext.PriceFloorModelGroup{
+						{
+							ModelVersion: "model 1 from req",
+							Currency:     "USD",
+							Values: map[string]float64{
+								"banner|300x600|www.website5.com": 5,
+								"*|*|*":                           7,
+							},
+							Schema: openrtb_ext.PriceFloorSchema{
+								Fields:    []string{"mediaType", "size", "domain"},
+								Delimiter: "|",
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "Dynamic Fetch Enabled but price floor fetcher is nil and request has no floors",
+			bidRequestWrapper: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Site: &openrtb2.Site{
+						Publisher: &openrtb2.Publisher{Domain: "www.website.com"},
+					},
+					Imp: []openrtb2.Imp{{ID: "1234", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}}},
+					Ext: json.RawMessage(`{"prebid":{}}`),
+				},
+			},
+			account: config.Account{
+				PriceFloors: config.AccountPriceFloors{
+					Enabled:        true,
+					UseDynamicData: true,
+				},
+			},
+			fetcher: nil,
+			expFloors: &openrtb_ext.PriceFloorRules{
+				FetchStatus:        openrtb_ext.FetchNone,
+				PriceFloorLocation: openrtb_ext.NoDataLocation,
+			},
+		},
 	}
 
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
-			resolvedFloors, _ := resolveFloors(tc.account, tc.bidRequestWrapper, getCurrencyRates(rates))
+			resolvedFloors, _ := resolveFloors(tc.account, tc.bidRequestWrapper, getCurrencyRates(rates), tc.fetcher)
 			assert.Equal(t, resolvedFloors, tc.expFloors, tc.name)
 		})
 	}
@@ -738,3 +1051,605 @@ func TestIsPriceFloorsEnabled(t *testing.T) {
 		})
 	}
 }
+
+func TestResolveFloorMin(t *testing.T) {
+	rates := map[string]map[string]float64{
+		"USD": {
+			"INR": 70,
+			"EUR": 0.9,
+			"JPY": 5.09,
+		},
+	}
+
+	tt := []struct {
+		name        string
+		reqFloors   openrtb_ext.PriceFloorRules
+		fetchFloors openrtb_ext.PriceFloorRules
+		conversions currency.Conversions
+		expPrice    Price
+	}{
+		{
+			name: "FloorsMin present in request Floors only",
+			reqFloors: openrtb_ext.PriceFloorRules{
+				FloorMin:    10,
+				FloorMinCur: "JPY",
+			},
+			fetchFloors: openrtb_ext.PriceFloorRules{},
+			expPrice:    Price{FloorMin: 10, FloorMinCur: "JPY"},
+		},
+		{
+			name: "FloorsMin, FloorMinCur and data currency present in request Floors",
+			reqFloors: openrtb_ext.PriceFloorRules{
+				FloorMin:    10,
+				FloorMinCur: "JPY",
+				Data: &openrtb_ext.PriceFloorData{
+					Currency: "USD",
+				},
+			},
+			fetchFloors: openrtb_ext.PriceFloorRules{},
+			expPrice:    Price{FloorMin: 10, FloorMinCur: "JPY"},
+		},
+		{
+			name: "FloorsMin and data currency present in request Floors",
+			reqFloors: openrtb_ext.PriceFloorRules{
+				FloorMin: 10,
+				Data: &openrtb_ext.PriceFloorData{
+					Currency: "USD",
+				},
+			},
+			fetchFloors: openrtb_ext.PriceFloorRules{},
+			expPrice:    Price{FloorMin: 10, FloorMinCur: "USD"},
+		},
+		{
+			name: "FloorsMin and FloorMinCur present in request Floors and fetched floors",
+			reqFloors: openrtb_ext.PriceFloorRules{
+				FloorMin:    10,
+				FloorMinCur: "USD",
+			},
+			fetchFloors: openrtb_ext.PriceFloorRules{
+				FloorMin:    15,
+				FloorMinCur: "INR",
+			},
+			expPrice: Price{FloorMin: 10, FloorMinCur: "USD"},
+		},
+		{
+			name:      "FloorsMin present fetched floors only",
+			reqFloors: openrtb_ext.PriceFloorRules{},
+			fetchFloors: openrtb_ext.PriceFloorRules{
+				FloorMin:    15,
+				FloorMinCur: "EUR",
+			},
+			expPrice: Price{FloorMin: 15, FloorMinCur: "EUR"},
+		},
+		{
+			name: "FloorMinCur present in reqFloors And FloorsMin, FloorMinCur present in fetched floors (Same Currency)",
+			reqFloors: openrtb_ext.PriceFloorRules{
+				FloorMinCur: "EUR",
+			},
+			fetchFloors: openrtb_ext.PriceFloorRules{
+				FloorMin:    15,
+				FloorMinCur: "EUR",
+			},
+			expPrice: Price{FloorMin: 15, FloorMinCur: "EUR"},
+		},
+		{
+			name: "FloorMinCur present in reqFloors And FloorsMin, FloorMinCur present in fetched floors (Different Currency)",
+			reqFloors: openrtb_ext.PriceFloorRules{
+				FloorMinCur: "USD",
+			},
+			fetchFloors: openrtb_ext.PriceFloorRules{
+				FloorMin:    15,
+				FloorMinCur: "EUR",
+			},
+			expPrice: Price{FloorMin: 16.6667, FloorMinCur: "USD"},
+		},
+		{
+			name: "FloorMin present in reqFloors And FloorMinCur present in fetched floors",
+			reqFloors: openrtb_ext.PriceFloorRules{
+				FloorMin: 11,
+			},
+			fetchFloors: openrtb_ext.PriceFloorRules{
+				FloorMinCur: "EUR",
+			},
+			expPrice: Price{FloorMin: 11, FloorMinCur: "EUR"},
+		},
+		{
+			name: "FloorMinCur present in reqFloors And FloorMin present in fetched floors",
+			reqFloors: openrtb_ext.PriceFloorRules{
+				FloorMinCur: "INR",
+			},
+			fetchFloors: openrtb_ext.PriceFloorRules{
+				FloorMin: 12,
+			},
+			expPrice: Price{FloorMin: 12, FloorMinCur: "INR"},
+		},
+		{
+			name: "FloorMinCur present in reqFloors And FloorMin, data currency present in fetched floors",
+			reqFloors: openrtb_ext.PriceFloorRules{
+				FloorMinCur: "INR",
+			},
+			fetchFloors: openrtb_ext.PriceFloorRules{
+				FloorMin: 1,
+				Data:     &openrtb_ext.PriceFloorData{Currency: "USD"},
+			},
+			expPrice: Price{FloorMin: 70, FloorMinCur: "INR"},
+		},
+		{
+			name: "FloorMinCur present in fetched Floors And data currency present in reqFloors",
+			reqFloors: openrtb_ext.PriceFloorRules{
+				FloorMin: 2,
+			},
+			fetchFloors: openrtb_ext.PriceFloorRules{
+				Data: &openrtb_ext.PriceFloorData{Currency: "USD"},
+			},
+			expPrice: Price{FloorMin: 2, FloorMinCur: "USD"},
+		},
+		{
+			name:      "Data currency and FloorMin present in fetched floors",
+			reqFloors: openrtb_ext.PriceFloorRules{},
+			fetchFloors: openrtb_ext.PriceFloorRules{
+				FloorMin: 12,
+				Data:     &openrtb_ext.PriceFloorData{Currency: "USD"},
+			},
+			expPrice: Price{FloorMin: 12, FloorMinCur: "USD"},
+		},
+		{
+			name:        "Empty reqFloors And Empty fetched floors",
+			reqFloors:   openrtb_ext.PriceFloorRules{},
+			fetchFloors: openrtb_ext.PriceFloorRules{},
+			expPrice:    Price{FloorMin: 0.0, FloorMinCur: ""},
+		},
+	}
+	for _, tc := range tt {
+		t.Run(tc.name, func(t *testing.T) {
+			price := resolveFloorMin(&tc.reqFloors, &tc.fetchFloors, getCurrencyRates(rates))
+			if !reflect.DeepEqual(price.FloorMin, tc.expPrice.FloorMin) {
+				t.Errorf("Floor Value error: \nreturn:\t%v\nwant:\t%v", price.FloorMin, tc.expPrice.FloorMin)
+			}
+			if !reflect.DeepEqual(price.FloorMinCur, tc.expPrice.FloorMinCur) {
+				t.Errorf("Floor Currency error: \nreturn:\t%v\nwant:\t%v", price.FloorMinCur, tc.expPrice.FloorMinCur)
+			}
+
+		})
+	}
+}
+
+func TestMergeFloors(t *testing.T) {
+
+	rates := map[string]map[string]float64{
+		"USD": {
+			"INR": 70,
+			"EUR": 0.9,
+			"JPY": 5.09,
+		},
+	}
+
+	type args struct {
+		reqFloors   *openrtb_ext.PriceFloorRules
+		fetchFloors *openrtb_ext.PriceFloorRules
+	}
+	tests := []struct {
+		name string
+		args args
+		want *openrtb_ext.PriceFloorRules
+	}{
+		{
+			name: "Fetched Floors are present and request Floors are empty",
+			args: args{
+				reqFloors: nil,
+				fetchFloors: &openrtb_ext.PriceFloorRules{
+					Enabled: getTrue(),
+					Data: &openrtb_ext.PriceFloorData{
+						Currency: "INR",
+						SkipRate: 0,
+						ModelGroups: []openrtb_ext.PriceFloorModelGroup{
+							{
+								ModelVersion: "Version 1",
+								Currency:     "INR",
+								Values: map[string]float64{
+									"banner|300x600|www.website5.com": 20,
+									"*|*|*":                           50,
+								},
+								Schema: openrtb_ext.PriceFloorSchema{
+									Fields:    []string{"mediaType", "size", "domain"},
+									Delimiter: "|",
+								},
+							},
+						},
+					},
+				},
+			},
+			want: &openrtb_ext.PriceFloorRules{
+				Enabled: getTrue(),
+				Data: &openrtb_ext.PriceFloorData{
+					Currency: "INR",
+					SkipRate: 0,
+					ModelGroups: []openrtb_ext.PriceFloorModelGroup{
+						{
+							ModelVersion: "Version 1",
+							Currency:     "INR",
+							Values: map[string]float64{
+								"banner|300x600|www.website5.com": 20,
+								"*|*|*":                           50,
+							},
+							Schema: openrtb_ext.PriceFloorSchema{
+								Fields:    []string{"mediaType", "size", "domain"},
+								Delimiter: "|",
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "Fetched Floors are present and request Floors has floors disabled",
+			args: args{
+				reqFloors: &openrtb_ext.PriceFloorRules{
+					Enabled: getFalse(),
+				},
+				fetchFloors: &openrtb_ext.PriceFloorRules{
+					Enabled: getTrue(),
+					Data: &openrtb_ext.PriceFloorData{
+						Currency: "INR",
+						SkipRate: 0,
+						ModelGroups: []openrtb_ext.PriceFloorModelGroup{
+							{
+								ModelVersion: "Version 1",
+								Currency:     "INR",
+								Values: map[string]float64{
+									"banner|300x600|www.website5.com": 20,
+									"*|*|*":                           50,
+								},
+								Schema: openrtb_ext.PriceFloorSchema{
+									Fields:    []string{"mediaType", "size", "domain"},
+									Delimiter: "|",
+								},
+							},
+						},
+					},
+				},
+			},
+			want: &openrtb_ext.PriceFloorRules{
+				Enabled: getFalse(),
+				Data: &openrtb_ext.PriceFloorData{
+					Currency: "INR",
+					SkipRate: 0,
+					ModelGroups: []openrtb_ext.PriceFloorModelGroup{
+						{
+							ModelVersion: "Version 1",
+							Currency:     "INR",
+							Values: map[string]float64{
+								"banner|300x600|www.website5.com": 20,
+								"*|*|*":                           50,
+							},
+							Schema: openrtb_ext.PriceFloorSchema{
+								Fields:    []string{"mediaType", "size", "domain"},
+								Delimiter: "|",
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "Fetched Floors are present and request Floors has enforcement (enforcepbs = true)",
+			args: args{
+				reqFloors: &openrtb_ext.PriceFloorRules{
+					Enabled: getTrue(),
+					Enforcement: &openrtb_ext.PriceFloorEnforcement{
+						EnforceRate: 50,
+						EnforcePBS:  getTrue(),
+					},
+				},
+				fetchFloors: &openrtb_ext.PriceFloorRules{
+					Enabled: getTrue(),
+					Data: &openrtb_ext.PriceFloorData{
+						Currency: "INR",
+						SkipRate: 0,
+						ModelGroups: []openrtb_ext.PriceFloorModelGroup{
+							{
+								ModelVersion: "Version 1",
+								Currency:     "INR",
+								Values: map[string]float64{
+									"banner|300x600|www.website5.com": 20,
+									"*|*|*":                           50,
+								},
+								Schema: openrtb_ext.PriceFloorSchema{
+									Fields:    []string{"mediaType", "size", "domain"},
+									Delimiter: "|",
+								},
+							},
+						},
+					},
+				},
+			},
+			want: &openrtb_ext.PriceFloorRules{
+				Enabled: getTrue(),
+				Data: &openrtb_ext.PriceFloorData{
+					Currency: "INR",
+					SkipRate: 0,
+					ModelGroups: []openrtb_ext.PriceFloorModelGroup{
+						{
+							ModelVersion: "Version 1",
+							Currency:     "INR",
+							Values: map[string]float64{
+								"banner|300x600|www.website5.com": 20,
+								"*|*|*":                           50,
+							},
+							Schema: openrtb_ext.PriceFloorSchema{
+								Fields:    []string{"mediaType", "size", "domain"},
+								Delimiter: "|",
+							},
+						},
+					},
+				},
+				Enforcement: &openrtb_ext.PriceFloorEnforcement{
+					EnforceRate: 50,
+					EnforcePBS:  getTrue(),
+				},
+			},
+		},
+		{
+			name: "Fetched Floors are present and request Floors has enforcement (enforcepbs = false)",
+			args: args{
+				reqFloors: &openrtb_ext.PriceFloorRules{
+					Enabled: getTrue(),
+					Enforcement: &openrtb_ext.PriceFloorEnforcement{
+						EnforceRate: 50,
+						EnforcePBS:  getFalse(),
+					},
+				},
+				fetchFloors: &openrtb_ext.PriceFloorRules{
+					Enabled: getTrue(),
+					Data: &openrtb_ext.PriceFloorData{
+						Currency: "INR",
+						SkipRate: 0,
+						ModelGroups: []openrtb_ext.PriceFloorModelGroup{
+							{
+								ModelVersion: "Version 1",
+								Currency:     "INR",
+								Values: map[string]float64{
+									"banner|300x600|www.website5.com": 20,
+									"*|*|*":                           50,
+								},
+								Schema: openrtb_ext.PriceFloorSchema{
+									Fields:    []string{"mediaType", "size", "domain"},
+									Delimiter: "|",
+								},
+							},
+						},
+					},
+				},
+			},
+			want: &openrtb_ext.PriceFloorRules{
+				Enabled: getTrue(),
+				Data: &openrtb_ext.PriceFloorData{
+					Currency: "INR",
+					SkipRate: 0,
+					ModelGroups: []openrtb_ext.PriceFloorModelGroup{
+						{
+							ModelVersion: "Version 1",
+							Currency:     "INR",
+							Values: map[string]float64{
+								"banner|300x600|www.website5.com": 20,
+								"*|*|*":                           50,
+							},
+							Schema: openrtb_ext.PriceFloorSchema{
+								Fields:    []string{"mediaType", "size", "domain"},
+								Delimiter: "|",
+							},
+						},
+					},
+				},
+				Enforcement: &openrtb_ext.PriceFloorEnforcement{
+					EnforceRate: 50,
+					EnforcePBS:  getFalse(),
+				},
+			},
+		},
+		{
+			name: "Fetched Floors are present and request Floors has Floormin",
+			args: args{
+				reqFloors: &openrtb_ext.PriceFloorRules{
+					Enabled: getTrue(),
+					Enforcement: &openrtb_ext.PriceFloorEnforcement{
+						EnforceRate: 50,
+						EnforcePBS:  getFalse(),
+					},
+					FloorMin:    5,
+					FloorMinCur: "INR",
+				},
+				fetchFloors: &openrtb_ext.PriceFloorRules{
+					Enabled: getTrue(),
+					Data: &openrtb_ext.PriceFloorData{
+						Currency: "INR",
+						SkipRate: 0,
+						ModelGroups: []openrtb_ext.PriceFloorModelGroup{
+							{
+								ModelVersion: "Version 1",
+								Currency:     "INR",
+								Values: map[string]float64{
+									"banner|300x600|www.website5.com": 20,
+									"*|*|*":                           50,
+								},
+								Schema: openrtb_ext.PriceFloorSchema{
+									Fields:    []string{"mediaType", "size", "domain"},
+									Delimiter: "|",
+								},
+							},
+						},
+					},
+				},
+			},
+			want: &openrtb_ext.PriceFloorRules{
+				Enabled: getTrue(),
+				Data: &openrtb_ext.PriceFloorData{
+					Currency: "INR",
+					SkipRate: 0,
+					ModelGroups: []openrtb_ext.PriceFloorModelGroup{
+						{
+							ModelVersion: "Version 1",
+							Currency:     "INR",
+							Values: map[string]float64{
+								"banner|300x600|www.website5.com": 20,
+								"*|*|*":                           50,
+							},
+							Schema: openrtb_ext.PriceFloorSchema{
+								Fields:    []string{"mediaType", "size", "domain"},
+								Delimiter: "|",
+							},
+						},
+					},
+				},
+				Enforcement: &openrtb_ext.PriceFloorEnforcement{
+					EnforceRate: 50,
+					EnforcePBS:  getFalse(),
+				},
+				FloorMin:    5,
+				FloorMinCur: "INR",
+			},
+		},
+		{
+			name: "Fetched Floors are present and request Floors has URL",
+			args: args{
+				reqFloors: &openrtb_ext.PriceFloorRules{
+					Enabled: getTrue(),
+					Enforcement: &openrtb_ext.PriceFloorEnforcement{
+						EnforceRate: 50,
+						EnforcePBS:  getFalse(),
+					},
+					FloorMin:    5,
+					FloorMinCur: "INR",
+					Location: &openrtb_ext.PriceFloorEndpoint{
+						URL: "https://test.com/floors",
+					},
+				},
+				fetchFloors: &openrtb_ext.PriceFloorRules{
+					Enabled: getTrue(),
+					Data: &openrtb_ext.PriceFloorData{
+						Currency: "INR",
+						SkipRate: 0,
+						ModelGroups: []openrtb_ext.PriceFloorModelGroup{
+							{
+								ModelVersion: "Version 1",
+								Currency:     "INR",
+								Values: map[string]float64{
+									"banner|300x600|www.website5.com": 20,
+									"*|*|*":                           50,
+								},
+								Schema: openrtb_ext.PriceFloorSchema{
+									Fields:    []string{"mediaType", "size", "domain"},
+									Delimiter: "|",
+								},
+							},
+						},
+					},
+				},
+			},
+			want: &openrtb_ext.PriceFloorRules{
+				Enabled: getTrue(),
+				Data: &openrtb_ext.PriceFloorData{
+					Currency: "INR",
+					SkipRate: 0,
+					ModelGroups: []openrtb_ext.PriceFloorModelGroup{
+						{
+							ModelVersion: "Version 1",
+							Currency:     "INR",
+							Values: map[string]float64{
+								"banner|300x600|www.website5.com": 20,
+								"*|*|*":                           50,
+							},
+							Schema: openrtb_ext.PriceFloorSchema{
+								Fields:    []string{"mediaType", "size", "domain"},
+								Delimiter: "|",
+							},
+						},
+					},
+				},
+				Enforcement: &openrtb_ext.PriceFloorEnforcement{
+					EnforceRate: 50,
+					EnforcePBS:  getFalse(),
+				},
+				FloorMin:    5,
+				FloorMinCur: "INR",
+				Location: &openrtb_ext.PriceFloorEndpoint{
+					URL: "https://test.com/floors",
+				},
+			},
+		},
+		{
+			name: "Fetched Floors has no enable atrribute are present and request Floors has URL",
+			args: args{
+				reqFloors: &openrtb_ext.PriceFloorRules{
+					Enabled: getTrue(),
+					Enforcement: &openrtb_ext.PriceFloorEnforcement{
+						EnforceRate: 50,
+						EnforcePBS:  getFalse(),
+					},
+					FloorMin:    5,
+					FloorMinCur: "INR",
+					Location: &openrtb_ext.PriceFloorEndpoint{
+						URL: "https://test.com/floors",
+					},
+				},
+				fetchFloors: &openrtb_ext.PriceFloorRules{
+					Data: &openrtb_ext.PriceFloorData{
+						Currency: "INR",
+						SkipRate: 0,
+						ModelGroups: []openrtb_ext.PriceFloorModelGroup{
+							{
+								ModelVersion: "Version 1",
+								Currency:     "INR",
+								Values: map[string]float64{
+									"banner|300x600|www.website5.com": 20,
+									"*|*|*":                           50,
+								},
+								Schema: openrtb_ext.PriceFloorSchema{
+									Fields:    []string{"mediaType", "size", "domain"},
+									Delimiter: "|",
+								},
+							},
+						},
+					},
+				},
+			},
+			want: &openrtb_ext.PriceFloorRules{
+				Enabled: getTrue(),
+				Data: &openrtb_ext.PriceFloorData{
+					Currency: "INR",
+					SkipRate: 0,
+					ModelGroups: []openrtb_ext.PriceFloorModelGroup{
+						{
+							ModelVersion: "Version 1",
+							Currency:     "INR",
+							Values: map[string]float64{
+								"banner|300x600|www.website5.com": 20,
+								"*|*|*":                           50,
+							},
+							Schema: openrtb_ext.PriceFloorSchema{
+								Fields:    []string{"mediaType", "size", "domain"},
+								Delimiter: "|",
+							},
+						},
+					},
+				},
+				Enforcement: &openrtb_ext.PriceFloorEnforcement{
+					EnforceRate: 50,
+					EnforcePBS:  getFalse(),
+				},
+				FloorMin:    5,
+				FloorMinCur: "INR",
+				Location: &openrtb_ext.PriceFloorEndpoint{
+					URL: "https://test.com/floors",
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := mergeFloors(tt.args.reqFloors, tt.args.fetchFloors, getCurrencyRates(rates)); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("mergeFloors() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/hooks/hookexecution/executor_test.go b/hooks/hookexecution/executor_test.go
index 6f50e089..90fa09e3 100644
--- a/hooks/hookexecution/executor_test.go
+++ b/hooks/hookexecution/executor_test.go
@@ -37,7 +37,7 @@ func TestEmptyHookExecutor(t *testing.T) {
 	entrypointBody, entrypointRejectErr := executor.ExecuteEntrypointStage(req, body)
 	rawAuctionBody, rawAuctionRejectErr := executor.ExecuteRawAuctionStage(body)
 	processedAuctionRejectErr := executor.ExecuteProcessedAuctionStage(&openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}})
-	bidderRequestRejectErr := executor.ExecuteBidderRequestStage(bidderRequest, "bidder-name")
+	bidderRequestRejectErr := executor.ExecuteBidderRequestStage(&openrtb_ext.RequestWrapper{BidRequest: bidderRequest}, "bidder-name")
 	executor.ExecuteAuctionResponseStage(&openrtb2.BidResponse{})
 
 	outcomes := executor.GetOutcomes()
@@ -1171,7 +1171,7 @@ func TestExecuteBidderRequestStage(t *testing.T) {
 			exec := NewHookExecutor(test.givenPlanBuilder, EndpointAuction, &metricsConfig.NilMetricsEngine{})
 			exec.SetAccount(test.givenAccount)
 
-			reject := exec.ExecuteBidderRequestStage(test.givenBidderRequest, bidderName)
+			reject := exec.ExecuteBidderRequestStage(&openrtb_ext.RequestWrapper{BidRequest: test.givenBidderRequest}, bidderName)
 
 			assert.Equal(t, test.expectedReject, reject, "Unexpected stage reject.")
 			assert.Equal(t, test.expectedBidderRequest, test.givenBidderRequest, "Incorrect bidder request.")
diff --git a/hooks/hookexecution/mocks_test.go b/hooks/hookexecution/mocks_test.go
index 420a44e8..c47e2892 100644
--- a/hooks/hookexecution/mocks_test.go
+++ b/hooks/hookexecution/mocks_test.go
@@ -131,7 +131,7 @@ func (e mockTimeoutHook) HandleProcessedAuctionHook(_ context.Context, _ hooksta
 	time.Sleep(20 * time.Millisecond)
 	c := hookstage.ChangeSet[hookstage.ProcessedAuctionRequestPayload]{}
 	c.AddMutation(func(payload hookstage.ProcessedAuctionRequestPayload) (hookstage.ProcessedAuctionRequestPayload, error) {
-		payload.RequestWrapper.User.CustomData = "some-custom-data"
+		payload.Request.User.CustomData = "some-custom-data"
 		return payload, nil
 	}, hookstage.MutationUpdate, "bidRequest", "user.customData")
 
@@ -142,7 +142,7 @@ func (e mockTimeoutHook) HandleBidderRequestHook(_ context.Context, _ hookstage.
 	time.Sleep(20 * time.Millisecond)
 	c := hookstage.ChangeSet[hookstage.BidderRequestPayload]{}
 	c.AddMutation(func(payload hookstage.BidderRequestPayload) (hookstage.BidderRequestPayload, error) {
-		payload.BidRequest.User.CustomData = "some-custom-data"
+		payload.Request.User.CustomData = "some-custom-data"
 		return payload, nil
 	}, hookstage.MutationUpdate, "bidRequest", "user.customData")
 
@@ -305,8 +305,8 @@ func (e mockUpdateBidRequestHook) HandleProcessedAuctionHook(_ context.Context,
 	c := hookstage.ChangeSet[hookstage.ProcessedAuctionRequestPayload]{}
 	c.AddMutation(
 		func(payload hookstage.ProcessedAuctionRequestPayload) (hookstage.ProcessedAuctionRequestPayload, error) {
-			payload.RequestWrapper.User.Yob = 2000
-			userExt, err := payload.RequestWrapper.GetUserExt()
+			payload.Request.User.Yob = 2000
+			userExt, err := payload.Request.GetUserExt()
 			if err != nil {
 				return payload, err
 			}
@@ -318,7 +318,7 @@ func (e mockUpdateBidRequestHook) HandleProcessedAuctionHook(_ context.Context,
 		}, hookstage.MutationUpdate, "bidRequest", "user.yob",
 	).AddMutation(
 		func(payload hookstage.ProcessedAuctionRequestPayload) (hookstage.ProcessedAuctionRequestPayload, error) {
-			payload.RequestWrapper.User.Consent = "true"
+			payload.Request.User.Consent = "true"
 			return payload, nil
 		}, hookstage.MutationUpdate, "bidRequest", "user.consent",
 	)
@@ -330,12 +330,12 @@ func (e mockUpdateBidRequestHook) HandleBidderRequestHook(_ context.Context, _ h
 	c := hookstage.ChangeSet[hookstage.BidderRequestPayload]{}
 	c.AddMutation(
 		func(payload hookstage.BidderRequestPayload) (hookstage.BidderRequestPayload, error) {
-			payload.BidRequest.User.Yob = 2000
+			payload.Request.User.Yob = 2000
 			return payload, nil
 		}, hookstage.MutationUpdate, "bidRequest", "user.yob",
 	).AddMutation(
 		func(payload hookstage.BidderRequestPayload) (hookstage.BidderRequestPayload, error) {
-			payload.BidRequest.User.Consent = "true"
+			payload.Request.User.Consent = "true"
 			return payload, nil
 		}, hookstage.MutationUpdate, "bidRequest", "user.consent",
 	)
diff --git a/modules/prebid/ortb2blocking/module_test.go b/modules/prebid/ortb2blocking/module_test.go
index b4499a4b..fc5cdcc7 100644
--- a/modules/prebid/ortb2blocking/module_test.go
+++ b/modules/prebid/ortb2blocking/module_test.go
@@ -13,6 +13,7 @@ import (
 	"github.com/prebid/prebid-server/v2/hooks/hookexecution"
 	"github.com/prebid/prebid-server/v2/hooks/hookstage"
 	"github.com/prebid/prebid-server/v2/modules/moduledeps"
+	"github.com/prebid/prebid-server/v2/openrtb_ext"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -497,7 +498,7 @@ func TestHandleBidderRequestHook(t *testing.T) {
 			bidRequest:         nil,
 			expectedBidRequest: nil,
 			expectedHookResult: hookstage.HookResult[hookstage.BidderRequestPayload]{},
-			expectedError:      hookexecution.NewFailure("empty BidRequest provided"),
+			expectedError:      hookexecution.NewFailure("payload contains a nil bid request"),
 		},
 		{
 			description:        "Expect baadv error if bidders and media_types not defined in config conditions",
@@ -566,7 +567,8 @@ func TestHandleBidderRequestHook(t *testing.T) {
 
 	for _, test := range testCases {
 		t.Run(test.description, func(t *testing.T) {
-			payload := hookstage.BidderRequestPayload{Bidder: test.bidder, BidRequest: test.bidRequest}
+			brw := openrtb_ext.RequestWrapper{BidRequest: test.bidRequest}
+			payload := hookstage.BidderRequestPayload{Bidder: test.bidder, Request: &brw}
 
 			result, err := Builder(nil, moduledeps.ModuleDeps{})
 			assert.NoError(t, err, "Failed to build module.")
@@ -590,7 +592,8 @@ func TestHandleBidderRequestHook(t *testing.T) {
 				_, err := mut.Apply(payload)
 				assert.NoError(t, err)
 			}
-			assert.Equal(t, test.expectedBidRequest, payload.BidRequest, "Invalid BidRequest after executing BidderRequestHook.")
+
+			assert.Equal(t, test.expectedBidRequest, payload.Request.BidRequest, "Invalid BidRequest after executing BidderRequestHook.")
 
 			// reset ChangeSet not to break hookResult assertion, we validated ChangeSet separately
 			hookResult.ChangeSet = hookstage.ChangeSet[hookstage.BidderRequestPayload]{}
diff --git a/openrtb_ext/floors_test.go b/openrtb_ext/floors_test.go
index 20247736..d6c35b9f 100644
--- a/openrtb_ext/floors_test.go
+++ b/openrtb_ext/floors_test.go
@@ -1,8 +1,10 @@
 package openrtb_ext
 
 import (
+	"reflect"
 	"testing"
 
+	"github.com/prebid/prebid-server/v2/util/ptrutil"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -218,3 +220,263 @@ func TestPriceFloorRulesGetEnabled(t *testing.T) {
 		})
 	}
 }
+
+func TestPriceFloorRulesDeepCopy(t *testing.T) {
+	type fields struct {
+		FloorMin           float64
+		FloorMinCur        string
+		SkipRate           int
+		Location           *PriceFloorEndpoint
+		Data               *PriceFloorData
+		Enforcement        *PriceFloorEnforcement
+		Enabled            *bool
+		Skipped            *bool
+		FloorProvider      string
+		FetchStatus        string
+		PriceFloorLocation string
+	}
+	tests := []struct {
+		name   string
+		fields fields
+	}{
+		{
+			name: "DeepCopy does not share same reference",
+			fields: fields{
+				FloorMin:    10,
+				FloorMinCur: "INR",
+				SkipRate:    0,
+				Location: &PriceFloorEndpoint{
+					URL: "https://test/floors",
+				},
+				Data: &PriceFloorData{
+					Currency: "INR",
+					SkipRate: 0,
+					ModelGroups: []PriceFloorModelGroup{
+						{
+							Currency:    "INR",
+							ModelWeight: ptrutil.ToPtr(1),
+							SkipRate:    0,
+							Values: map[string]float64{
+								"banner|300x600|www.website5.com": 20,
+								"*|*|*":                           50,
+							},
+							Schema: PriceFloorSchema{
+								Fields:    []string{"mediaType", "size", "domain"},
+								Delimiter: "|",
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			pf := &PriceFloorRules{
+				FloorMin:           tt.fields.FloorMin,
+				FloorMinCur:        tt.fields.FloorMinCur,
+				SkipRate:           tt.fields.SkipRate,
+				Location:           tt.fields.Location,
+				Data:               tt.fields.Data,
+				Enforcement:        tt.fields.Enforcement,
+				Enabled:            tt.fields.Enabled,
+				Skipped:            tt.fields.Skipped,
+				FloorProvider:      tt.fields.FloorProvider,
+				FetchStatus:        tt.fields.FetchStatus,
+				PriceFloorLocation: tt.fields.PriceFloorLocation,
+			}
+			got := pf.DeepCopy()
+			if got == pf {
+				t.Errorf("Rules reference are same")
+			}
+			if got.Data == pf.Data {
+				t.Errorf("Floor data reference is same")
+			}
+		})
+	}
+}
+
+func TestFloorRulesDeepCopy(t *testing.T) {
+	type fields struct {
+		FloorMin           float64
+		FloorMinCur        string
+		SkipRate           int
+		Location           *PriceFloorEndpoint
+		Data               *PriceFloorData
+		Enforcement        *PriceFloorEnforcement
+		Enabled            *bool
+		Skipped            *bool
+		FloorProvider      string
+		FetchStatus        string
+		PriceFloorLocation string
+	}
+	tests := []struct {
+		name   string
+		fields fields
+		want   *PriceFloorRules
+	}{
+		{
+			name: "Copy entire floors object",
+			fields: fields{
+				FloorMin:    10,
+				FloorMinCur: "INR",
+				SkipRate:    0,
+				Location: &PriceFloorEndpoint{
+					URL: "http://prebid.com/floor",
+				},
+				Data: &PriceFloorData{
+					Currency:            "INR",
+					SkipRate:            0,
+					FloorsSchemaVersion: "2",
+					ModelTimestamp:      123,
+					ModelGroups: []PriceFloorModelGroup{
+						{
+							Currency:     "INR",
+							ModelWeight:  ptrutil.ToPtr(50),
+							ModelVersion: "version 1",
+							SkipRate:     0,
+							Schema: PriceFloorSchema{
+								Fields:    []string{"a", "b", "c"},
+								Delimiter: "|",
+							},
+							Values: map[string]float64{
+								"*|*|*": 20,
+							},
+							Default: 1,
+						},
+					},
+					FloorProvider: "prebid",
+				},
+				Enforcement: &PriceFloorEnforcement{
+					EnforceJS:     ptrutil.ToPtr(true),
+					EnforcePBS:    ptrutil.ToPtr(true),
+					FloorDeals:    ptrutil.ToPtr(true),
+					BidAdjustment: ptrutil.ToPtr(true),
+					EnforceRate:   100,
+				},
+				Enabled:            ptrutil.ToPtr(true),
+				Skipped:            ptrutil.ToPtr(false),
+				FloorProvider:      "Prebid",
+				FetchStatus:        "success",
+				PriceFloorLocation: "fetch",
+			},
+			want: &PriceFloorRules{
+				FloorMin:    10,
+				FloorMinCur: "INR",
+				SkipRate:    0,
+				Location: &PriceFloorEndpoint{
+					URL: "http://prebid.com/floor",
+				},
+				Data: &PriceFloorData{
+					Currency:            "INR",
+					SkipRate:            0,
+					FloorsSchemaVersion: "2",
+					ModelTimestamp:      123,
+					ModelGroups: []PriceFloorModelGroup{
+						{
+							Currency:     "INR",
+							ModelWeight:  ptrutil.ToPtr(50),
+							ModelVersion: "version 1",
+							SkipRate:     0,
+							Schema: PriceFloorSchema{
+								Fields:    []string{"a", "b", "c"},
+								Delimiter: "|",
+							},
+							Values: map[string]float64{
+								"*|*|*": 20,
+							},
+							Default: 1,
+						},
+					},
+					FloorProvider: "prebid",
+				},
+				Enforcement: &PriceFloorEnforcement{
+					EnforceJS:     ptrutil.ToPtr(true),
+					EnforcePBS:    ptrutil.ToPtr(true),
+					FloorDeals:    ptrutil.ToPtr(true),
+					BidAdjustment: ptrutil.ToPtr(true),
+					EnforceRate:   100,
+				},
+				Enabled:            ptrutil.ToPtr(true),
+				Skipped:            ptrutil.ToPtr(false),
+				FloorProvider:      "Prebid",
+				FetchStatus:        "success",
+				PriceFloorLocation: "fetch",
+			},
+		},
+		{
+			name: "Copy entire floors object",
+			fields: fields{
+				FloorMin:    10,
+				FloorMinCur: "INR",
+				SkipRate:    0,
+				Location: &PriceFloorEndpoint{
+					URL: "http://prebid.com/floor",
+				},
+				Data: nil,
+				Enforcement: &PriceFloorEnforcement{
+					EnforceJS:     ptrutil.ToPtr(true),
+					EnforcePBS:    ptrutil.ToPtr(true),
+					FloorDeals:    ptrutil.ToPtr(true),
+					BidAdjustment: ptrutil.ToPtr(true),
+					EnforceRate:   100,
+				},
+				Enabled:            ptrutil.ToPtr(true),
+				Skipped:            ptrutil.ToPtr(false),
+				FloorProvider:      "Prebid",
+				FetchStatus:        "success",
+				PriceFloorLocation: "fetch",
+			},
+			want: &PriceFloorRules{
+				FloorMin:    10,
+				FloorMinCur: "INR",
+				SkipRate:    0,
+				Location: &PriceFloorEndpoint{
+					URL: "http://prebid.com/floor",
+				},
+				Data: nil,
+				Enforcement: &PriceFloorEnforcement{
+					EnforceJS:     ptrutil.ToPtr(true),
+					EnforcePBS:    ptrutil.ToPtr(true),
+					FloorDeals:    ptrutil.ToPtr(true),
+					BidAdjustment: ptrutil.ToPtr(true),
+					EnforceRate:   100,
+				},
+				Enabled:            ptrutil.ToPtr(true),
+				Skipped:            ptrutil.ToPtr(false),
+				FloorProvider:      "Prebid",
+				FetchStatus:        "success",
+				PriceFloorLocation: "fetch",
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			pf := &PriceFloorRules{
+				FloorMin:           tt.fields.FloorMin,
+				FloorMinCur:        tt.fields.FloorMinCur,
+				SkipRate:           tt.fields.SkipRate,
+				Location:           tt.fields.Location,
+				Data:               tt.fields.Data,
+				Enforcement:        tt.fields.Enforcement,
+				Enabled:            tt.fields.Enabled,
+				Skipped:            tt.fields.Skipped,
+				FloorProvider:      tt.fields.FloorProvider,
+				FetchStatus:        tt.fields.FetchStatus,
+				PriceFloorLocation: tt.fields.PriceFloorLocation,
+			}
+			if got := pf.DeepCopy(); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("PriceFloorRules.DeepCopy() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestFloorRuleDeepCopyNil(t *testing.T) {
+	var priceFloorRule *PriceFloorRules
+	got := priceFloorRule.DeepCopy()
+
+	if got != nil {
+		t.Errorf("PriceFloorRules.DeepCopy() = %v, want %v", got, nil)
+	}
+}