Repository: prebid/prebid-server
Pull request #2732 (2023-04-26 14:53:28)

Floors: Dynamic fetch
Design document of dynamic fetch mechanism
https://docs.google.com/document/d/1Lb302nt6nDYQxeflzISjtIWpE6ifExdoT8PSPm8YSKk/edit#heading=h.bxed3ptmj442

Issue + Specs: https://github.com/prebid/prebid-server/issues/1162

AlexBVolcy: This looks good! I will wait for @bsardo's review before approving. 
pm-nikhil-vaidya: @SyntaxNode
In the last conversation on the design implementation, we have decided that we can implement the normal go routine.
As we are going to modify the fetcher in later code optimisation i.e fetcher 2.0, can we go head with this implementation instead of changing this again. Even if we modify the implementation the code will be short lived so we can do the changes at the time of fetcher 2.0
Let me know your thoughts on this
hhhjort: Please fix the merge conflict.
pm-nikhil-vaidya: Modified the code which addresses below points

1. Switched from go-cache to freecache
2. introduced httpclient and its config instead of using default http client
3. Used time interface from timeutil
4. introduced metrics engine instance in fetcher
guscarreon: For the testing purposes, I wrote the very basic price floor server shown below:
```
var port string = "9090"

func main() {
	http.HandleFunc("/", usersHandleFunc)
	log.Printf("Mock floor bid server starts at: http://localhost:" + port)
	log.Fatal(http.ListenAndServe(":"+port, nil))
}

func usersHandleFunc(w http.ResponseWriter, r *http.Request) {
	mockResponse := getMockResponse()

	w.Write(mockResponse)
	return
}

func getMockResponse() []byte {
	return []byte(`{
  "floormin": 5,
  "floormincur": "USD",
  "skiprate": 1,
  "data": {
    "currency": "USD",
    "skiprate": 1,
    "floorsschemaversion": "1.0",
    "modeltimestamp": 10,
    "modelgroups": [
      {
        "currency": "USD",
        "modelversion": "Version 101",
        "skiprate": 1,
        "schema": {
          "fields": [
            "mediaType",
            "size",
            "domain"
          ],
          "delimiter": "|"
        },
        "values": {
          "banner|300x600|www.website5.com": 15,
          "*|*|*": 25
        }
      }
    ],
    "floorprovider": "appnexus"
  }
}`)
}
```
And found the unmarshalling bug mentioned in a previous comment above but, I believe we need further testing end to end testing in a pod

pm-nikhil-vaidya: @guscarreon 
I have tested this mechanism end to end
Let me know any issues you encountered while testing end to end if by chance i missed something
AlexBVolcy: This is looking good to me, but I'm going to wait to see what other comments @guscarreon makes before approving.
guscarreon: Aside from the last comment above, we also need to address the merge conflicts.
pm-nikhil-vaidya: @guscarreon 
Added retry mechanism if URL is url is long dead

Max retries are configurable through config file
```go
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"`
}
```

This config is consumed when floor fetcher is initialised
```go
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,
	}
```
The worker will try to fetch the floor data from url and if fetch fails then it will increment the retry count in fetchInfo else it will reset the count value
```go
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
	}
}
```
pm-nikhil-vaidya: @guscarreon @SyntaxNode @bsardo @hhhjort @AlexBVolcy 

I have addressed all the review comments. Kindly review and let me know if anything needs to be addressed so that we can finalise this implementation. 
guscarreon: > Also what should be default value for retries?

I'm sorry I missed your question. Sure, 10 is fine
pm-nikhil-vaidya: The refetch mechanism is mainly introduced as this hosted floor data will be updated by ML algorithms , ML will constantly learn by applying particular floor value to request and based on bids it will increase or decrease the floor value, so yes we will require refetch mechanism.
pm-nikhil-vaidya: PRD also mentions about refetch mechanism
https://docs.google.com/document/d/1YAt00RfQaU2Ugciqw1LnirLgjyUz9ibfyjfxWUHR_P0/edit#heading=h.yglzagui7gzp
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.
pm-nikhil-vaidya: @SyntaxNode @guscarreon 

Updated to V2

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..610122fb 100644
--- a/exchange/bidder.go
+++ b/exchange/bidder.go
@@ -406,7 +406,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 +429,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/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..a537054c 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{
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/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)
+	}
+}