Repository: prebid/prebid-server
Pull request #2068 (2021-11-02 14:31:15)

Triplelift: adds iframe user sync to yaml
Adds iframe user sync option to yaml.

Any existing Prebid Server host is already allowlisted and so both of these sync urls will continue to work for them. Future hosts will need to reach out as a non-allowlisted `redir` will result in a 500 error. 


Patch:
diff --git a/adapters/beachfront/beachfront.go b/adapters/beachfront/beachfront.go
index 8af61eb9..3e3ff62b 100644
--- a/adapters/beachfront/beachfront.go
+++ b/adapters/beachfront/beachfront.go
@@ -269,10 +269,6 @@ func getSchain(request *openrtb2.BidRequest) (openrtb_ext.ExtRequestPrebidSChain
 	return schain, json.Unmarshal(request.Source.Ext, &schain)
 }
 
-/*
-getBannerRequest, singular. A "Slot" is an "imp," and each Slot can have an AppId, so just one
-request to the beachfront banner endpoint gets all banner Imps.
-*/
 func getBannerRequest(request *openrtb2.BidRequest) (beachfrontBannerRequest, []error) {
 	var bfr beachfrontBannerRequest
 	var errs = make([]error, 0, len(request.Imp))
@@ -294,7 +290,12 @@ func getBannerRequest(request *openrtb2.BidRequest) (beachfrontBannerRequest, []
 			continue
 		}
 
-		setBidFloor(&beachfrontExt, &request.Imp[i])
+		err = setBidFloor(&beachfrontExt, &request.Imp[i])
+
+		if err != nil {
+			errs = append(errs, err)
+			continue
+		}
 
 		slot := beachfrontSlot{
 			Id:       appid,
@@ -467,7 +468,11 @@ func getVideoRequests(request *openrtb2.BidRequest) ([]beachfrontVideoRequest, [
 		imp.Banner = nil
 		imp.Ext = nil
 		imp.Secure = &secure
-		setBidFloor(&beachfrontExt, &imp)
+		if err := setBidFloor(&beachfrontExt, &imp); err != nil {
+			errs = append(errs, err)
+			failedRequestIndicies = append(failedRequestIndicies, i)
+			continue
+		}
 
 		if imp.Video.H == 0 && imp.Video.W == 0 {
 			imp.Video.W = defaultVideoWidth
@@ -563,7 +568,7 @@ func (a *BeachfrontAdapter) MakeBids(internalRequest *openrtb2.BidRequest, exter
 	return bidResponse, errs
 }
 
-func setBidFloor(ext *openrtb_ext.ExtImpBeachfront, imp *openrtb2.Imp) {
+func setBidFloor(ext *openrtb_ext.ExtImpBeachfront, imp *openrtb2.Imp) error {
 	var floor float64
 
 	if imp.BidFloor > 0 {
@@ -574,11 +579,18 @@ func setBidFloor(ext *openrtb_ext.ExtImpBeachfront, imp *openrtb2.Imp) {
 		floor = minBidFloor
 	}
 
+	if floor >= 0 && imp.BidFloorCur != "" && strings.ToUpper(imp.BidFloorCur) != "USD" {
+		return &errortypes.BadInput{
+			Message: fmt.Sprintf("unsupported bid currency, %s. bids are currently accepted in USD only.", imp.BidFloorCur),
+		}
+	}
+
 	if floor <= minBidFloor {
 		floor = 0
 	}
 
 	imp.BidFloor = floor
+	return nil
 }
 
 func (a *BeachfrontAdapter) getBidType(externalRequest *adapters.RequestData) openrtb_ext.BidType {
diff --git a/config/config.go b/config/config.go
index 49ca9589..5a20e257 100644
--- a/config/config.go
+++ b/config/config.go
@@ -877,6 +877,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.somoaudience.endpoint", "http://publisher-east.mobileadtrading.com/rtb/bid")
 	v.SetDefault("adapters.sonobi.endpoint", "https://apex.go.sonobi.com/prebid?partnerid=71d9d3d8af")
 	v.SetDefault("adapters.sovrn.endpoint", "http://ap.lijit.com/rtb/bid?src=prebid_server")
+	v.SetDefault("adapters.streamkey.endpoint", "http://ghb.hb.streamkey.net/pbs/ortb")
 	v.SetDefault("adapters.synacormedia.endpoint", "http://{{.Host}}.technoratimedia.com/openrtb/bids/{{.Host}}")
 	v.SetDefault("adapters.tappx.endpoint", "http://{{.Host}}")
 	v.SetDefault("adapters.telaria.endpoint", "https://ads.tremorhub.com/ad/rtb/prebid")
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index eb6f17f2..cd383c25 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"github.com/prebid/prebid-server/firstpartydata"
 	"io"
 	"io/ioutil"
 	"net/http"
@@ -14,7 +15,7 @@ import (
 	"time"
 
 	"github.com/buger/jsonparser"
-	jsonpatch "github.com/evanphx/json-patch"
+	"github.com/evanphx/json-patch"
 	"github.com/gofrs/uuid"
 	"github.com/golang/glog"
 	"github.com/julienschmidt/httprouter"
@@ -140,10 +141,17 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 	}()
 
 	req, impExtInfoMap, errL := deps.parseRequest(r)
-
 	if errortypes.ContainsFatalError(errL) && writeError(errL, w, &labels) {
 		return
 	}
+
+	resolvedFPD, fpdErrors := firstpartydata.ExtractFPDForBidders(req)
+	if len(fpdErrors) > 0 {
+		if errortypes.ContainsFatalError(fpdErrors) && writeError(fpdErrors, w, &labels) {
+			return
+		}
+		errL = append(errL, fpdErrors...)
+	}
 	warnings := errortypes.WarningOnly(errL)
 
 	ctx := context.Background()
@@ -197,6 +205,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 		Warnings:                   warnings,
 		GlobalPrivacyControlHeader: secGPC,
 		ImpExtInfoMap:              impExtInfoMap,
+		FirstPartyData:             resolvedFPD,
 	}
 
 	response, err := deps.ex.HoldAuction(ctx, auctionRequest, nil)
diff --git a/endpoints/openrtb2/sample-requests/first-party-data/valid-fpd-allowed-with-ext-bidder.json b/endpoints/openrtb2/sample-requests/first-party-data/valid-fpd-allowed-with-ext-bidder.json
deleted file mode 100644
index a4b716b2..00000000
--- a/endpoints/openrtb2/sample-requests/first-party-data/valid-fpd-allowed-with-ext-bidder.json
+++ /dev/null
@@ -1,50 +0,0 @@
-{
-  "description": "The imp.ext.context field is valid for First Party Data and should be exempted from bidder name validation.",
-
-  "mockBidRequest": {
-    "id": "some-request-id",
-    "site": {
-      "page": "test.somepage.com"
-    },
-    "imp": [{
-      "id": "some-imp-id",
-      "banner": {
-        "format": [{
-          "w": 600,
-          "h": 500
-        }, {
-          "w": 300,
-          "h": 600
-        }]
-      },
-      "ext": {
-        "appnexus": {
-          "placementId": 12883451
-        },
-        "data": {
-          "keywords": "prebid server example"
-        },
-        "context": {
-          "data": {
-            "keywords": "another prebid server example"
-          }
-        }
-      }
-    }]
-  },
-  "expectedBidResponse": {
-    "id": "some-request-id",
-    "seatbid": [{
-      "bid": [{
-        "id": "appnexus-bid",
-        "impid": "",
-        "price": 0
-      }],
-      "seat": "appnexus-bids"
-    }],
-    "bidid": "test bid id",
-    "cur": "USD",
-    "nbr": 0
-  },
-  "expectedReturnCode": 200
-}
diff --git a/endpoints/openrtb2/sample-requests/first-party-data/valid-fpd-allowed-with-prebid-bidder.json b/endpoints/openrtb2/sample-requests/first-party-data/valid-fpd-allowed-with-prebid-bidder.json
deleted file mode 100644
index 27e8c46d..00000000
--- a/endpoints/openrtb2/sample-requests/first-party-data/valid-fpd-allowed-with-prebid-bidder.json
+++ /dev/null
@@ -1,54 +0,0 @@
-{
-  "description": "The imp.ext.context field is valid for First Party Data and should be exempted from bidder name validation.",
-
-  "mockBidRequest": {
-    "id": "some-request-id",
-    "site": {
-      "page": "test.somepage.com"
-    },
-    "imp": [{
-      "id": "some-imp-id",
-      "banner": {
-        "format": [{
-          "w": 600,
-          "h": 500
-        }, {
-          "w": 300,
-          "h": 600
-        }]
-      },
-      "ext": {
-        "prebid": {
-          "bidder": {
-            "appnexus": {
-              "placementId": 12883451
-            }
-          }
-        },
-        "data": {
-          "keywords": "prebid server example"
-        },
-        "context": {
-          "data": {
-            "keywords": "another prebid server example"
-          }
-        }
-      }
-    }]
-  },
-  "expectedBidResponse": {
-    "id": "some-request-id",
-    "seatbid": [{
-      "bid": [{
-        "id": "appnexus-bid",
-        "impid": "",
-        "price": 0
-      }],
-      "seat": "appnexus-bids"
-    }],
-    "bidid": "test bid id",
-    "cur": "USD",
-    "nbr": 0
-  },
-  "expectedReturnCode": 200
-}
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/first-party-data.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/first-party-data.json
new file mode 100644
index 00000000..4552b883
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/first-party-data.json
@@ -0,0 +1,156 @@
+{
+  "description": "Valid request with First party data for two bidders, both have global and bidder specific fpd",
+  "mockBidRequest": {
+    "id": "bid_req_id",
+    "imp": [
+      {
+        "id": "1_0",
+        "video": {
+          "mimes": [
+            "video/mp4"
+          ],
+          "maxduration": 30,
+          "protocols": [
+            2,
+            3
+          ],
+          "w": 640,
+          "h": 480
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 123
+          },
+          "telaria": {
+            "seatCode": "test",
+            "adCode": "test"
+          }
+        }
+      }
+    ],
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "publisher": {
+        "id": "1"
+      },
+      "content": {
+        "episode": 6,
+        "title": "episodeName",
+        "series": "TvName",
+        "season": "season3",
+        "len": 900,
+        "data": [
+          {
+            "id": "reqContentDataSiteId1",
+            "name": "reqContentDataSiteName1"
+          }
+        ]
+      },
+      "ext": {
+        "amp": 1,
+        "data": {
+          "somesitefpd": "fpdSite"
+        }
+      }
+    },
+    "user": {
+      "id": "reqUserID",
+      "keywords": "userKeyword",
+      "yob": 1982,
+      "gender": "M",
+      "data": [
+        {
+          "id": "reqDataUserId1",
+          "name": "reqDataUserName1"
+        },
+        {
+          "id": "reqDataUserId2",
+          "name": "reqDataUserName2"
+        }
+      ],
+      "ext": {
+        "data": {
+          "moreuserdata": "morefpduserdata"
+        }
+      }
+    },
+    "test": 1,
+    "at": 1,
+    "tmax": 5000,
+    "ext": {
+      "prebid": {
+        "data": {
+          "bidders": [
+            "bidder1",
+            "bidder2"
+          ]
+        },
+        "bidderconfig": [
+          {
+            "bidders": [
+              "bidder1"
+            ],
+            "config": {
+              "ortb2": {
+                "site": {
+                  "id": "fpdSiteId",
+                  "keywords": "fpd keywords!",
+                  "data": [
+                    {
+                      "id": "siteId1",
+                      "name": true
+                    },
+                    {
+                      "id": "siteId2",
+                      "name": false
+                    }
+                  ],
+                  "page": "",
+                  "ext": {
+                    "data": {
+                      "morefpdData": "morefpddata",
+                      "sitefpddata": "mysitefpddata"
+                    }
+                  }
+                }
+              }
+            }
+          },
+          {
+            "bidders": [
+              "bidder2"
+            ],
+            "config": {
+              "ortb2": {
+                "user": {
+                  "id": "fpdUserId",
+                  "yob": 2011,
+                  "gender": "F",
+                  "keywords": "fpd keywords",
+                  "data": [
+                    {
+                      "id": "fpdUserDataId1",
+                      "name": "fpdUserDataName1"
+                    },
+                    {
+                      "id": "fpdUserDataId2",
+                      "name": "fpdUserDataName2"
+                    }
+                  ],
+                  "ext": {
+                    "data": {
+                      "userdata": "biddermyfpduserdata"
+                    }
+                  }
+                }
+              }
+            }
+          }
+        ]
+      }
+    }
+  },
+  "expectedBidResponse": {},
+  "expectedReturnCode": 200
+}
diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go
index db55e733..33d662c2 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -249,6 +249,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderSomoaudience:      somoaudience.Builder,
 		openrtb_ext.BidderSonobi:            sonobi.Builder,
 		openrtb_ext.BidderSovrn:             sovrn.Builder,
+		openrtb_ext.BidderStreamkey:         adtelligent.Builder,
 		openrtb_ext.BidderSynacormedia:      synacormedia.Builder,
 		openrtb_ext.BidderTappx:             tappx.Builder,
 		openrtb_ext.BidderTelaria:           telaria.Builder,
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 8b4b49e7..6298d88a 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -18,6 +18,7 @@ import (
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/currency"
 	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/firstpartydata"
 	"github.com/prebid/prebid-server/gdpr"
 	"github.com/prebid/prebid-server/metrics"
 	"github.com/prebid/prebid-server/openrtb_ext"
@@ -159,7 +160,8 @@ type AuctionRequest struct {
 
 	// LegacyLabels is included here for temporary compatability with cleanOpenRTBRequests
 	// in HoldAuction until we get to factoring it away. Do not use for anything new.
-	LegacyLabels metrics.Labels
+	LegacyLabels   metrics.Labels
+	FirstPartyData map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData
 }
 
 // BidderRequest holds the bidder specific request and all other
diff --git a/exchange/utils.go b/exchange/utils.go
index 59cc7f1e..5a68ba45 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -12,6 +12,7 @@ import (
 
 	"github.com/buger/jsonparser"
 	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/firstpartydata"
 	"github.com/prebid/prebid-server/gdpr"
 	"github.com/prebid/prebid-server/metrics"
 	"github.com/prebid/prebid-server/openrtb_ext"
@@ -154,6 +155,10 @@ func cleanOpenRTBRequests(ctx context.Context,
 			}
 		}
 
+		if req.FirstPartyData != nil && req.FirstPartyData[bidderRequest.BidderName] != nil {
+			applyFPD(req.FirstPartyData[bidderRequest.BidderName], bidderRequest.BidRequest)
+		}
+
 		if bidRequestAllowed {
 			privacyEnforcement.Apply(bidderRequest.BidRequest)
 			allowedBidderRequests = append(allowedBidderRequests, bidderRequest)
@@ -760,3 +765,15 @@ func writeNameVersionRecord(sb *strings.Builder, name, version string) {
 	sb.WriteString("/")
 	sb.WriteString(version)
 }
+
+func applyFPD(fpd *firstpartydata.ResolvedFirstPartyData, bidReq *openrtb2.BidRequest) {
+	if fpd.Site != nil {
+		bidReq.Site = fpd.Site
+	}
+	if fpd.App != nil {
+		bidReq.App = fpd.App
+	}
+	if fpd.User != nil {
+		bidReq.User = fpd.User
+	}
+}
diff --git a/firstpartydata/first_party_data.go b/firstpartydata/first_party_data.go
new file mode 100644
index 00000000..dc5b6f31
--- /dev/null
+++ b/firstpartydata/first_party_data.go
@@ -0,0 +1,641 @@
+package firstpartydata
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/evanphx/json-patch"
+	"github.com/mxmCherry/openrtb/v15/openrtb2"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+const (
+	siteKey = "site"
+	appKey  = "app"
+	userKey = "user"
+	dataKey = "data"
+	extKey  = "ext"
+
+	userDataKey        = "userData"
+	appContentDataKey  = "appContentData"
+	siteContentDataKey = "siteContentData"
+
+	keywordsKey   = "keywords"
+	genderKey     = "gender"
+	yobKey        = "yob"
+	pageKey       = "page"
+	nameKey       = "name"
+	domainKey     = "domain"
+	catKey        = "cat"
+	sectionCatKey = "sectioncat"
+	pageCatKey    = "pagecat"
+	searchKey     = "search"
+	refKey        = "ref"
+	bundleKey     = "bundle"
+	storeUrlKey   = "storeurl"
+	verKey        = "ver"
+)
+
+type ResolvedFirstPartyData struct {
+	Site *openrtb2.Site
+	App  *openrtb2.App
+	User *openrtb2.User
+}
+
+// ExtractGlobalFPD extracts request level FPD from the request and removes req.{site,app,user}.ext.data if exists
+func ExtractGlobalFPD(req *openrtb_ext.RequestWrapper) (map[string][]byte, error) {
+
+	fpdReqData := make(map[string][]byte, 3)
+
+	siteExt, err := req.GetSiteExt()
+	if err != nil {
+		return nil, err
+	}
+	refreshExt := false
+
+	if len(siteExt.GetExt()[dataKey]) > 0 {
+		newSiteExt := siteExt.GetExt()
+		fpdReqData[siteKey] = newSiteExt[dataKey]
+		delete(newSiteExt, dataKey)
+		siteExt.SetExt(newSiteExt)
+		refreshExt = true
+	}
+
+	appExt, err := req.GetAppExt()
+	if err != nil {
+		return nil, err
+	}
+	if len(appExt.GetExt()[dataKey]) > 0 {
+		newAppExt := appExt.GetExt()
+		fpdReqData[appKey] = newAppExt[dataKey]
+		delete(newAppExt, dataKey)
+		appExt.SetExt(newAppExt)
+		refreshExt = true
+	}
+
+	userExt, err := req.GetUserExt()
+	if err != nil {
+		return nil, err
+	}
+	if len(userExt.GetExt()[dataKey]) > 0 {
+		newUserExt := userExt.GetExt()
+		fpdReqData[userKey] = newUserExt[dataKey]
+		delete(newUserExt, dataKey)
+		userExt.SetExt(newUserExt)
+		refreshExt = true
+	}
+	if refreshExt {
+		//need to keep site/app/user ext clean in case bidder is not in global fpd bidder list
+		// rebuild/resync the request in the request wrapper.
+		if err := req.RebuildRequest(); err != nil {
+			return nil, err
+		}
+	}
+
+	return fpdReqData, nil
+}
+
+//ExtractOpenRtbGlobalFPD extracts and deletes user.data and {app/site}.content.data from request
+func ExtractOpenRtbGlobalFPD(bidRequest *openrtb2.BidRequest) map[string][]openrtb2.Data {
+
+	openRtbGlobalFPD := make(map[string][]openrtb2.Data, 3)
+	if bidRequest.User != nil && len(bidRequest.User.Data) > 0 {
+		openRtbGlobalFPD[userDataKey] = bidRequest.User.Data
+		bidRequest.User.Data = nil
+	}
+
+	if bidRequest.Site != nil && bidRequest.Site.Content != nil && len(bidRequest.Site.Content.Data) > 0 {
+		openRtbGlobalFPD[siteContentDataKey] = bidRequest.Site.Content.Data
+		bidRequest.Site.Content.Data = nil
+	}
+
+	if bidRequest.App != nil && bidRequest.App.Content != nil && len(bidRequest.App.Content.Data) > 0 {
+		openRtbGlobalFPD[appContentDataKey] = bidRequest.App.Content.Data
+		bidRequest.App.Content.Data = nil
+	}
+
+	return openRtbGlobalFPD
+
+}
+
+//ResolveFPD consolidates First Party Data from different sources and returns valid FPD that will be applied to bidders later or returns errors
+func ResolveFPD(bidRequest *openrtb2.BidRequest, fpdBidderConfigData map[openrtb_ext.BidderName]*openrtb_ext.ORTB2, globalFPD map[string][]byte, openRtbGlobalFPD map[string][]openrtb2.Data, biddersWithGlobalFPD []string) (map[openrtb_ext.BidderName]*ResolvedFirstPartyData, []error) {
+	var errL []error
+
+	resolvedFpd := make(map[openrtb_ext.BidderName]*ResolvedFirstPartyData)
+
+	allBiddersTable := make(map[string]struct{})
+
+	if biddersWithGlobalFPD == nil {
+		//add all bidders in bidder configs to receive global data and bidder specific data
+		for bidderName := range fpdBidderConfigData {
+			if _, present := allBiddersTable[string(bidderName)]; !present {
+				allBiddersTable[string(bidderName)] = struct{}{}
+			}
+		}
+	} else {
+		//only bidders in global bidder list will receive global data and bidder specific data
+		for _, bidderName := range biddersWithGlobalFPD {
+			if _, present := allBiddersTable[string(bidderName)]; !present {
+				allBiddersTable[string(bidderName)] = struct{}{}
+			}
+		}
+	}
+
+	for bidderName := range allBiddersTable {
+
+		fpdConfig := fpdBidderConfigData[openrtb_ext.BidderName(bidderName)]
+
+		resolvedFpdConfig := &ResolvedFirstPartyData{}
+
+		newUser, err := resolveUser(fpdConfig, bidRequest.User, globalFPD, openRtbGlobalFPD, bidderName)
+		if err != nil {
+			errL = append(errL, err)
+		}
+		resolvedFpdConfig.User = newUser
+
+		newApp, err := resolveApp(fpdConfig, bidRequest.App, globalFPD, openRtbGlobalFPD, bidderName)
+		if err != nil {
+			errL = append(errL, err)
+		}
+		resolvedFpdConfig.App = newApp
+
+		newSite, err := resolveSite(fpdConfig, bidRequest.Site, globalFPD, openRtbGlobalFPD, bidderName)
+		if err != nil {
+			errL = append(errL, err)
+		}
+		resolvedFpdConfig.Site = newSite
+
+		if len(errL) == 0 {
+			resolvedFpd[openrtb_ext.BidderName(bidderName)] = resolvedFpdConfig
+		}
+	}
+	return resolvedFpd, errL
+}
+
+func resolveUser(fpdConfig *openrtb_ext.ORTB2, bidRequestUser *openrtb2.User, globalFPD map[string][]byte, openRtbGlobalFPD map[string][]openrtb2.Data, bidderName string) (*openrtb2.User, error) {
+	var fpdConfigUser map[string]json.RawMessage
+
+	if fpdConfig != nil && fpdConfig.User != nil {
+		fpdConfigUser = fpdConfig.User
+	}
+
+	if bidRequestUser == nil && fpdConfigUser == nil {
+		return nil, nil
+	}
+
+	if bidRequestUser == nil && fpdConfigUser != nil {
+		return nil, &errortypes.BadInput{
+			Message: fmt.Sprintf("incorrect First Party Data for bidder %s: User object is not defined in request, but defined in FPD config", bidderName),
+		}
+	}
+
+	newUser := *bidRequestUser
+	var err error
+
+	//apply global fpd
+	if len(globalFPD[userKey]) > 0 {
+		extData := buildExtData(globalFPD[userKey])
+		if len(newUser.Ext) > 0 {
+			newUser.Ext, err = jsonpatch.MergePatch(newUser.Ext, extData)
+		} else {
+			newUser.Ext = extData
+		}
+	}
+	if openRtbGlobalFPD != nil && len(openRtbGlobalFPD[userDataKey]) > 0 {
+		newUser.Data = openRtbGlobalFPD[userDataKey]
+	}
+	if fpdConfigUser != nil {
+		//apply bidder specific fpd if present
+		newUser, err = mergeUsers(&newUser, fpdConfigUser)
+	}
+
+	return &newUser, err
+}
+
+func unmarshalJSONToInt64(input json.RawMessage) (int64, error) {
+	var num json.Number
+	err := json.Unmarshal(input, &num)
+	if err != nil {
+		return -1, err
+	}
+	resNum, err := num.Int64()
+	return resNum, err
+}
+
+func unmarshalJSONToString(input json.RawMessage) (string, error) {
+	var inputString string
+	err := json.Unmarshal(input, &inputString)
+	return inputString, err
+}
+
+func unmarshalJSONToStringArray(input json.RawMessage) ([]string, error) {
+	var inputString []string
+	err := json.Unmarshal(input, &inputString)
+	return inputString, err
+}
+
+//resolveExtension inserts remaining {site/app/user} attributes back to {site/app/user}.ext.data
+func resolveExtension(fpdConfig map[string]json.RawMessage, originalExt json.RawMessage) ([]byte, error) {
+	resExt := originalExt
+	var err error
+
+	if resExt == nil && len(fpdConfig) > 0 {
+		fpdExt, err := json.Marshal(fpdConfig)
+		return buildExtData(fpdExt), err
+	}
+
+	fpdConfigExt, present := fpdConfig[extKey]
+	if present {
+		delete(fpdConfig, extKey)
+		resExt, err = jsonpatch.MergePatch(resExt, fpdConfigExt)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if len(fpdConfig) > 0 {
+		fpdData, err := json.Marshal(fpdConfig)
+		if err != nil {
+			return nil, err
+		}
+		data := buildExtData(fpdData)
+		return jsonpatch.MergePatch(resExt, data)
+	}
+	return resExt, nil
+}
+
+func mergeUsers(original *openrtb2.User, fpdConfigUser map[string]json.RawMessage) (openrtb2.User, error) {
+
+	var err error
+	newUser := *original
+
+	if keywords, present := fpdConfigUser[keywordsKey]; present {
+		newUser.Keywords, err = unmarshalJSONToString(keywords)
+		if err != nil {
+			return newUser, err
+		}
+		delete(fpdConfigUser, keywordsKey)
+	}
+	if gender, present := fpdConfigUser[genderKey]; present {
+		newUser.Gender, err = unmarshalJSONToString(gender)
+		if err != nil {
+			return newUser, err
+		}
+		delete(fpdConfigUser, genderKey)
+	}
+	if yob, present := fpdConfigUser[yobKey]; present {
+		newUser.Yob, err = unmarshalJSONToInt64(yob)
+		if err != nil {
+			return newUser, err
+		}
+		delete(fpdConfigUser, yobKey)
+	}
+
+	if len(fpdConfigUser) > 0 {
+		newUser.Ext, err = resolveExtension(fpdConfigUser, original.Ext)
+	}
+
+	return newUser, err
+}
+
+func resolveSite(fpdConfig *openrtb_ext.ORTB2, bidRequestSite *openrtb2.Site, globalFPD map[string][]byte, openRtbGlobalFPD map[string][]openrtb2.Data, bidderName string) (*openrtb2.Site, error) {
+	var fpdConfigSite map[string]json.RawMessage
+
+	if fpdConfig != nil && fpdConfig.Site != nil {
+		fpdConfigSite = fpdConfig.Site
+	}
+
+	if bidRequestSite == nil && fpdConfigSite == nil {
+		return nil, nil
+	}
+	if bidRequestSite == nil && fpdConfigSite != nil {
+		return nil, &errortypes.BadInput{
+			Message: fmt.Sprintf("incorrect First Party Data for bidder %s: Site object is not defined in request, but defined in FPD config", bidderName),
+		}
+	}
+
+	newSite := *bidRequestSite
+	var err error
+
+	//apply global fpd
+	if len(globalFPD[siteKey]) > 0 {
+		extData := buildExtData(globalFPD[siteKey])
+		if len(newSite.Ext) > 0 {
+			newSite.Ext, err = jsonpatch.MergePatch(newSite.Ext, extData)
+		} else {
+			newSite.Ext = extData
+		}
+	}
+	if openRtbGlobalFPD != nil && len(openRtbGlobalFPD[siteContentDataKey]) > 0 {
+		if newSite.Content != nil {
+			contentCopy := *newSite.Content
+			contentCopy.Data = openRtbGlobalFPD[siteContentDataKey]
+			newSite.Content = &contentCopy
+		} else {
+			newSite.Content = &openrtb2.Content{Data: openRtbGlobalFPD[siteContentDataKey]}
+		}
+	}
+
+	if fpdConfigSite != nil {
+		newSite, err = mergeSites(&newSite, fpdConfigSite, bidderName)
+	}
+	return &newSite, err
+
+}
+func mergeSites(originalSite *openrtb2.Site, fpdConfigSite map[string]json.RawMessage, bidderName string) (openrtb2.Site, error) {
+
+	var err error
+	newSite := *originalSite
+
+	if page, present := fpdConfigSite[pageKey]; present {
+		sitePage, err := unmarshalJSONToString(page)
+		if err != nil {
+			return newSite, err
+		}
+		//apply bidder specific fpd if present
+		//result site should have ID or Page, fpd becomes incorrect if it overwrites page to empty one and ID is empty in original site
+		if sitePage == "" && newSite.Page != "" && newSite.ID == "" {
+			return newSite, &errortypes.BadInput{
+				Message: fmt.Sprintf("incorrect First Party Data for bidder %s: Site object cannot set empty page if req.site.id is empty", bidderName),
+			}
+
+		}
+		newSite.Page = sitePage
+		delete(fpdConfigSite, pageKey)
+	}
+	if name, present := fpdConfigSite[nameKey]; present {
+		newSite.Name, err = unmarshalJSONToString(name)
+		if err != nil {
+			return newSite, err
+		}
+		delete(fpdConfigSite, nameKey)
+	}
+	if domain, present := fpdConfigSite[domainKey]; present {
+		newSite.Domain, err = unmarshalJSONToString(domain)
+		if err != nil {
+			return newSite, err
+		}
+		delete(fpdConfigSite, domainKey)
+	}
+	if cat, present := fpdConfigSite[catKey]; present {
+		newSite.Cat, err = unmarshalJSONToStringArray(cat)
+		if err != nil {
+			return newSite, err
+		}
+		delete(fpdConfigSite, catKey)
+	}
+	if sectionCat, present := fpdConfigSite[sectionCatKey]; present {
+		newSite.SectionCat, err = unmarshalJSONToStringArray(sectionCat)
+		if err != nil {
+			return newSite, err
+		}
+		delete(fpdConfigSite, sectionCatKey)
+	}
+	if pageCat, present := fpdConfigSite[pageCatKey]; present {
+		newSite.PageCat, err = unmarshalJSONToStringArray(pageCat)
+		if err != nil {
+			return newSite, err
+		}
+		delete(fpdConfigSite, pageCatKey)
+	}
+	if search, present := fpdConfigSite[searchKey]; present {
+		newSite.Search, err = unmarshalJSONToString(search)
+		if err != nil {
+			return newSite, err
+		}
+		delete(fpdConfigSite, searchKey)
+	}
+	if keywords, present := fpdConfigSite[keywordsKey]; present {
+		newSite.Keywords, err = unmarshalJSONToString(keywords)
+		if err != nil {
+			return newSite, err
+		}
+		delete(fpdConfigSite, keywordsKey)
+	}
+	if ref, present := fpdConfigSite[refKey]; present {
+		newSite.Ref, err = unmarshalJSONToString(ref)
+		if err != nil {
+			return newSite, err
+		}
+		delete(fpdConfigSite, refKey)
+	}
+
+	if len(fpdConfigSite) > 0 {
+		newSite.Ext, err = resolveExtension(fpdConfigSite, originalSite.Ext)
+	}
+
+	return newSite, err
+}
+
+func resolveApp(fpdConfig *openrtb_ext.ORTB2, bidRequestApp *openrtb2.App, globalFPD map[string][]byte, openRtbGlobalFPD map[string][]openrtb2.Data, bidderName string) (*openrtb2.App, error) {
+
+	var fpdConfigApp map[string]json.RawMessage
+
+	if fpdConfig != nil && fpdConfig.App != nil {
+		fpdConfigApp = fpdConfig.App
+	}
+
+	if bidRequestApp == nil && fpdConfigApp == nil {
+		return nil, nil
+	}
+
+	if bidRequestApp == nil && fpdConfigApp != nil {
+		return nil, &errortypes.BadInput{
+			Message: fmt.Sprintf("incorrect First Party Data for bidder %s: App object is not defined in request, but defined in FPD config", bidderName),
+		}
+	}
+
+	newApp := *bidRequestApp
+	var err error
+
+	//apply global fpd if exists
+	if len(globalFPD[appKey]) > 0 {
+		extData := buildExtData(globalFPD[appKey])
+		if len(newApp.Ext) > 0 {
+			newApp.Ext, err = jsonpatch.MergePatch(newApp.Ext, extData)
+		} else {
+			newApp.Ext = extData
+		}
+	}
+	if openRtbGlobalFPD != nil && len(openRtbGlobalFPD[appContentDataKey]) > 0 {
+		if newApp.Content != nil {
+			contentCopy := *newApp.Content
+			contentCopy.Data = openRtbGlobalFPD[appContentDataKey]
+			newApp.Content = &contentCopy
+		} else {
+			newApp.Content = &openrtb2.Content{Data: openRtbGlobalFPD[appContentDataKey]}
+		}
+	}
+
+	if fpdConfigApp != nil {
+		//apply bidder specific fpd if present
+		newApp, err = mergeApps(&newApp, fpdConfigApp)
+	}
+
+	return &newApp, err
+}
+
+func mergeApps(originalApp *openrtb2.App, fpdConfigApp map[string]json.RawMessage) (openrtb2.App, error) {
+
+	var err error
+	newApp := *originalApp
+
+	if name, present := fpdConfigApp[nameKey]; present {
+		newApp.Name, err = unmarshalJSONToString(name)
+		if err != nil {
+			return newApp, err
+		}
+		delete(fpdConfigApp, nameKey)
+	}
+	if bundle, present := fpdConfigApp[bundleKey]; present {
+		newApp.Bundle, err = unmarshalJSONToString(bundle)
+		if err != nil {
+			return newApp, err
+		}
+		delete(fpdConfigApp, bundleKey)
+	}
+	if domain, present := fpdConfigApp[domainKey]; present {
+		newApp.Domain, err = unmarshalJSONToString(domain)
+		if err != nil {
+			return newApp, err
+		}
+		delete(fpdConfigApp, domainKey)
+	}
+	if storeUrl, present := fpdConfigApp[storeUrlKey]; present {
+		newApp.StoreURL, err = unmarshalJSONToString(storeUrl)
+		if err != nil {
+			return newApp, err
+		}
+		delete(fpdConfigApp, storeUrlKey)
+	}
+	if cat, present := fpdConfigApp[catKey]; present {
+		newApp.Cat, err = unmarshalJSONToStringArray(cat)
+		if err != nil {
+			return newApp, err
+		}
+		delete(fpdConfigApp, catKey)
+	}
+	if sectionCat, present := fpdConfigApp[sectionCatKey]; present {
+		newApp.SectionCat, err = unmarshalJSONToStringArray(sectionCat)
+		if err != nil {
+			return newApp, err
+		}
+		delete(fpdConfigApp, sectionCatKey)
+	}
+	if pageCat, present := fpdConfigApp[pageCatKey]; present {
+		newApp.PageCat, err = unmarshalJSONToStringArray(pageCat)
+		if err != nil {
+			return newApp, err
+		}
+		delete(fpdConfigApp, pageCatKey)
+	}
+	if version, present := fpdConfigApp[verKey]; present {
+		newApp.Ver, err = unmarshalJSONToString(version)
+		if err != nil {
+			return newApp, err
+		}
+		delete(fpdConfigApp, verKey)
+	}
+	if keywords, present := fpdConfigApp[keywordsKey]; present {
+		newApp.Keywords, err = unmarshalJSONToString(keywords)
+		if err != nil {
+			return newApp, err
+		}
+		delete(fpdConfigApp, keywordsKey)
+	}
+
+	if len(fpdConfigApp) > 0 {
+		newApp.Ext, err = resolveExtension(fpdConfigApp, originalApp.Ext)
+	}
+
+	return newApp, err
+}
+
+func buildExtData(data []byte) []byte {
+	res := make([]byte, 0, len(data))
+	res = append(res, []byte(`{"data":`)...)
+	res = append(res, data...)
+	res = append(res, []byte(`}`)...)
+	return res
+}
+
+//ExtractBidderConfigFPD extracts bidder specific configs from req.ext.prebid.bidderconfig
+func ExtractBidderConfigFPD(reqExt *openrtb_ext.RequestExt) (map[openrtb_ext.BidderName]*openrtb_ext.ORTB2, error) {
+
+	fpd := make(map[openrtb_ext.BidderName]*openrtb_ext.ORTB2)
+	reqExtPrebid := reqExt.GetPrebid()
+	if reqExtPrebid != nil {
+		for _, bidderConfig := range reqExtPrebid.BidderConfigs {
+			for _, bidder := range bidderConfig.Bidders {
+				if _, present := fpd[openrtb_ext.BidderName(bidder)]; present {
+					//if bidder has duplicated config - throw an error
+					return nil, &errortypes.BadInput{
+						Message: fmt.Sprintf("multiple First Party Data bidder configs provided for bidder: %s", bidder),
+					}
+				}
+
+				fpdBidderData := &openrtb_ext.ORTB2{}
+
+				if bidderConfig.Config != nil && bidderConfig.Config.ORTB2 != nil {
+					if bidderConfig.Config.ORTB2.Site != nil {
+						fpdBidderData.Site = bidderConfig.Config.ORTB2.Site
+					}
+					if bidderConfig.Config.ORTB2.App != nil {
+						fpdBidderData.App = bidderConfig.Config.ORTB2.App
+					}
+					if bidderConfig.Config.ORTB2.User != nil {
+						fpdBidderData.User = bidderConfig.Config.ORTB2.User
+					}
+				}
+
+				fpd[openrtb_ext.BidderName(bidder)] = fpdBidderData
+			}
+		}
+		reqExtPrebid.BidderConfigs = nil
+		reqExt.SetPrebid(reqExtPrebid)
+	}
+	return fpd, nil
+
+}
+
+//ExtractFPDForBidders extracts FPD data from request if specified
+func ExtractFPDForBidders(req *openrtb_ext.RequestWrapper) (map[openrtb_ext.BidderName]*ResolvedFirstPartyData, []error) {
+
+	reqExt, err := req.GetRequestExt()
+	if err != nil {
+		return nil, []error{err}
+	}
+	if reqExt == nil || reqExt.GetPrebid() == nil {
+		return nil, nil
+	}
+	var biddersWithGlobalFPD []string
+
+	extPrebid := reqExt.GetPrebid()
+	if extPrebid.Data != nil {
+		biddersWithGlobalFPD = extPrebid.Data.Bidders
+		extPrebid.Data.Bidders = nil
+		reqExt.SetPrebid(extPrebid)
+	}
+
+	fbdBidderConfigData, err := ExtractBidderConfigFPD(reqExt)
+	if err != nil {
+		return nil, []error{err}
+	}
+
+	var globalFpd map[string][]byte
+	var openRtbGlobalFPD map[string][]openrtb2.Data
+
+	if biddersWithGlobalFPD != nil {
+		//global fpd data should not be extracted and removed from request if global bidder list is nil.
+		//Bidders that don't have any fpd config should receive request data as is
+		globalFpd, err = ExtractGlobalFPD(req)
+		if err != nil {
+			return nil, []error{err}
+		}
+		openRtbGlobalFPD = ExtractOpenRtbGlobalFPD(req.BidRequest)
+	}
+
+	return ResolveFPD(req.BidRequest, fbdBidderConfigData, globalFpd, openRtbGlobalFPD, biddersWithGlobalFPD)
+
+}
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 23ec7fde..d5c424c5 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -183,6 +183,7 @@ const (
 	BidderSomoaudience      BidderName = "somoaudience"
 	BidderSonobi            BidderName = "sonobi"
 	BidderSovrn             BidderName = "sovrn"
+	BidderStreamkey         BidderName = "streamkey"
 	BidderSynacormedia      BidderName = "synacormedia"
 	BidderTappx             BidderName = "tappx"
 	BidderTelaria           BidderName = "telaria"
@@ -321,6 +322,7 @@ func CoreBidderNames() []BidderName {
 		BidderSomoaudience,
 		BidderSonobi,
 		BidderSovrn,
+		BidderStreamkey,
 		BidderSynacormedia,
 		BidderTappx,
 		BidderTelaria,
diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go
index bf875123..d399715f 100644
--- a/openrtb_ext/request.go
+++ b/openrtb_ext/request.go
@@ -44,6 +44,22 @@ type ExtRequestPrebid struct {
 	NoSale []string `json:"nosale,omitempty"`
 
 	CurrencyConversions *ExtRequestCurrency `json:"currency,omitempty"`
+	BidderConfigs       []BidderConfig      `json:"bidderconfig,omitempty"`
+}
+
+type BidderConfig struct {
+	Bidders []string `json:"bidders,omitempty"`
+	Config  *Config  `json:"config,omitempty"`
+}
+
+type Config struct {
+	ORTB2 *ORTB2 `json:"ortb2,omitempty"`
+}
+
+type ORTB2 struct { //First party data
+	Site map[string]json.RawMessage `json:"site,omitempty"`
+	App  map[string]json.RawMessage `json:"app,omitempty"`
+	User map[string]json.RawMessage `json:"user,omitempty"`
 }
 
 type ExtRequestCurrency struct {
@@ -318,6 +334,7 @@ var priceGranularityAuto = PriceGranularity{
 // ExtRequestPrebidData defines Prebid's First Party Data (FPD) and related bid request options.
 type ExtRequestPrebidData struct {
 	EidPermissions []ExtRequestPrebidDataEidPermission `json:"eidpermissions"`
+	Bidders        []string                            `json:"bidders,omitempty"`
 }
 
 // ExtRequestPrebidDataEidPermission defines a filter rule for filter user.ext.eids
diff --git a/openrtb_ext/request_wrapper.go b/openrtb_ext/request_wrapper.go
index 276f8f5e..9219bbdb 100644
--- a/openrtb_ext/request_wrapper.go
+++ b/openrtb_ext/request_wrapper.go
@@ -210,7 +210,7 @@ func (rw *RequestWrapper) rebuildSiteExt() error {
 		if err != nil {
 			return err
 		}
-		rw.Regs.Ext = siteJson
+		rw.Site.Ext = siteJson
 	}
 	return nil
 }
diff --git a/static/bidder-info/smaato.yaml b/static/bidder-info/smaato.yaml
index b73edb23..a070db1d 100644
--- a/static/bidder-info/smaato.yaml
+++ b/static/bidder-info/smaato.yaml
@@ -10,3 +10,7 @@ capabilities:
     mediaTypes:
       - banner
       - video
+userSync:
+  redirect:
+    url: "https://s.ad.smaato.net/c/?adExInit=p&redir={{.RedirectURL}}"
+    userMacro: "$UID"
diff --git a/static/bidder-info/streamkey.yaml b/static/bidder-info/streamkey.yaml
new file mode 100644
index 00000000..a13b71e5
--- /dev/null
+++ b/static/bidder-info/streamkey.yaml
@@ -0,0 +1,14 @@
+maintainer:
+  email: "contact@streamkey.tv"
+capabilities:
+  app:
+    mediaTypes:
+      - video
+  site:
+    mediaTypes:
+      - video
+userSync:
+  # streamkey supports user syncing, but requires configuration by the host. contact this
+  # bidder directly at the email address in this file to ask about enabling user sync.
+  supports:
+    - iframe
diff --git a/static/bidder-info/triplelift.yaml b/static/bidder-info/triplelift.yaml
index 99026b6f..84eea869 100644
--- a/static/bidder-info/triplelift.yaml
+++ b/static/bidder-info/triplelift.yaml
@@ -11,6 +11,13 @@ capabilities:
       - banner
       - video
 userSync:
+  # Triplelift supports user syncing but requires configuration by the host as the RedirectURL domain must be allowlisted. 
+  # Contact this bidder directly at the email address above to ask about enabling user sync.
+  # 
+  default: iframe
+  iframe:
+    url: "https://eb2.3lift.com/sync?gdpr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir={{.RedirectURL}}"
+    userMacro: $UID
   redirect:
     url: "https://eb2.3lift.com/getuid?gdpr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir={{.RedirectURL}}"
-    userMacro: "$UID"
+    userMacro: "$UID"
\ No newline at end of file
diff --git a/static/bidder-info/triplelift_native.yaml b/static/bidder-info/triplelift_native.yaml
index 32eb07ef..dc60d572 100644
--- a/static/bidder-info/triplelift_native.yaml
+++ b/static/bidder-info/triplelift_native.yaml
@@ -9,6 +9,13 @@ capabilities:
     mediaTypes:
       - native
 userSync:
+  # Triplelift supports user syncing but requires configuration by the host as the RedirectURL domain must be allowlisted. 
+  # Contact this bidder directly at the email address above to ask about enabling user sync.
+  # 
+  default: iframe
+  iframe:
+    url: "https://eb2.3lift.com/sync?gdpr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir={{.RedirectURL}}"
+    userMacro: $UID
   redirect:
     url: "https://eb2.3lift.com/getuid?gdpr={{.GDPR}}&cmp_cs={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir={{.RedirectURL}}"
     userMacro: "$UID"
\ No newline at end of file
diff --git a/static/bidder-params/streamkey.json b/static/bidder-params/streamkey.json
new file mode 100644
index 00000000..ec8e5b1b
--- /dev/null
+++ b/static/bidder-params/streamkey.json
@@ -0,0 +1,26 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "Streamkey Adapter Params",
+  "description": "A schema which validates params accepted by the Streamkey adapter",
+
+  "type": "object",
+  "properties": {
+    "placementId": {
+      "type": "integer",
+      "description": "An ID which identifies this placement of the impression"
+    },
+    "siteId": {
+      "type": "integer",
+      "description": "An ID which identifies the site selling the impression"
+    },
+    "aid": {
+      "type": "integer",
+      "description": "An ID which identifies the channel"
+    },
+    "bidFloor": {
+      "type": "number",
+      "description": "BidFloor, US Dollars"
+    }
+  },
+  "required": ["aid"]
+}
diff --git a/util/jsonutil/jsonutil.go b/util/jsonutil/jsonutil.go
index 4fd56745..d8716297 100644
--- a/util/jsonutil/jsonutil.go
+++ b/util/jsonutil/jsonutil.go
@@ -8,12 +8,18 @@ import (
 
 var comma = []byte(",")[0]
 var colon = []byte(":")[0]
+var sqBracket = []byte("]")[0]
+var openCurlyBracket = []byte("{")[0]
+var closingCurlyBracket = []byte("}")[0]
+var quote = []byte(`"`)[0]
 
-func findElementIndexes(extension []byte, elementName string) (bool, int64, int64, error) {
-	found := false
+//Finds element in json byte array with any level of nesting
+func FindElement(extension []byte, elementNames ...string) (bool, int64, int64, error) {
+	elementName := elementNames[0]
 	buf := bytes.NewBuffer(extension)
 	dec := json.NewDecoder(buf)
-	var startIndex int64
+	found := false
+	var startIndex, endIndex int64
 	var i interface{}
 	for {
 		token, err := dec.Token()
@@ -24,21 +30,18 @@ func findElementIndexes(extension []byte, elementName string) (bool, int64, int6
 		if err != nil {
 			return false, -1, -1, err
 		}
-
 		if token == elementName {
 			err := dec.Decode(&i)
 			if err != nil {
 				return false, -1, -1, err
 			}
-			found = true
-			endIndex := dec.InputOffset()
+			endIndex = dec.InputOffset()
 
 			if dec.More() {
 				//if there were other elements before
 				if extension[startIndex] == comma {
 					startIndex++
 				}
-
 				for {
 					//structure has more elements, need to find index of comma
 					if extension[endIndex] == comma {
@@ -48,43 +51,62 @@ func findElementIndexes(extension []byte, elementName string) (bool, int64, int6
 					endIndex++
 				}
 			}
-			return found, startIndex, endIndex, nil
+			found = true
+			break
 		} else {
 			startIndex = dec.InputOffset()
 		}
-
 	}
-
-	return false, -1, -1, nil
-}
-
-func DropElement(extension []byte, elementName string) ([]byte, error) {
-	found, startIndex, endIndex, err := findElementIndexes(extension, elementName)
 	if found {
-		extension = append(extension[:startIndex], extension[endIndex:]...)
-	}
-	return extension, err
-}
-
-func FindElement(extension []byte, elementName string) (bool, []byte, error) {
+		if len(elementNames) == 1 {
+			return found, startIndex, endIndex, nil
+		} else if len(elementNames) > 1 {
+			for {
+				//find the beginning of nested element
+				if extension[startIndex] == colon {
+					startIndex++
+					break
+				}
+				startIndex++
+			}
+			for {
+				if endIndex == int64(len(extension)) {
+					endIndex--
+				}
 
-	found, startIndex, endIndex, err := findElementIndexes(extension, elementName)
+				//if structure had more elements, need to find index of comma at the end
+				if extension[endIndex] == sqBracket || extension[endIndex] == closingCurlyBracket {
+					break
+				}
 
-	if found && err == nil {
-		element := extension[startIndex:endIndex]
-		index := 0
-		for {
-			if index < len(element) && element[index] != colon {
-				index++
-			} else {
-				index++
-				break
+				if extension[endIndex] == comma {
+					endIndex--
+					break
+				} else {
+					endIndex--
+				}
 			}
+			if found {
+				found, startInd, endInd, err := FindElement(extension[startIndex:endIndex], elementNames[1:]...)
+				return found, startIndex + startInd, startIndex + endInd, err
+			}
+			return found, startIndex, startIndex, nil
 		}
-		element = element[index:]
-		return found, element, err
 	}
+	return found, startIndex, endIndex, nil
+}
 
-	return found, nil, err
-
+//Drops element from json byte array
+// - Doesn't support drop element from json list
+// - Keys in the path can skip levels
+// - First found element will be removed
+func DropElement(extension []byte, elementNames ...string) ([]byte, error) {
+	found, startIndex, endIndex, err := FindElement(extension, elementNames...)
+	if err != nil {
+		return nil, err
+	}
+	if found {
+		extension = append(extension[:startIndex], extension[endIndex:]...)
+	}
+	return extension, nil
 }


Test patch:
diff --git a/adapters/beachfront/beachfronttest/supplemental/adm-video-unaccepted-currency.json b/adapters/beachfront/beachfronttest/supplemental/adm-video-unaccepted-currency.json
new file mode 100644
index 00000000..c26b10ae
--- /dev/null
+++ b/adapters/beachfront/beachfronttest/supplemental/adm-video-unaccepted-currency.json
@@ -0,0 +1,38 @@
+{
+  "mockBidRequest": {
+    "id": "adm-video-unaccepted-curency",
+    "imp": [
+      {
+        "id": "video1",
+        "bidfloorcur": "IRP",
+        "bidfloor": 695.61,
+        "ext": {
+          "bidder": {
+            "bidfloor": 3.01,
+            "appId": "videoAppId1"
+          }
+        },
+        "video": {
+          "mimes": [
+            "video/mp4"
+          ],
+          "context": "instream",
+          "w": 300,
+          "h": 250
+        }
+      }
+    ],
+    "site": {
+      "page": "https://some.domain.us/some/page.html"
+    },
+    "device":{
+      "ip":"192.168.168.168"
+    }
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "unsupported bid currency, IRP. bids are currently accepted in USD only.",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index 20fdd56e..5b234b12 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"github.com/prebid/prebid-server/firstpartydata"
 	"io"
 	"io/ioutil"
 	"net"
@@ -104,10 +105,6 @@ func TestJsonSampleRequests(t *testing.T) {
 			"There are both disabled and non-disabled bidders, we expect a 200",
 			"disabled/good",
 		},
-		{
-			"Requests with first party data context info found in imp[i].ext.prebid.bidder,context",
-			"first-party-data",
-		},
 		{
 			"Assert we correctly use the server conversion rates when needed",
 			"currency-conversion/server-rates/valid",
@@ -2510,6 +2507,56 @@ func TestParseRequestParseImpInfoError(t *testing.T) {
 	assert.Contains(t, errL[0].Error(), "echovideoattrs of type bool", "Incorrect error message")
 }
 
+func TestAuctionFirstPartyData(t *testing.T) {
+	reqBody := validRequest(t, "first-party-data.json")
+	deps := &endpointDeps{
+		fakeUUIDGenerator{},
+		&mockExchangeFPD{},
+		newParamsValidator(t),
+		&mockStoredReqFetcher{},
+		empty_fetcher.EmptyFetcher{},
+		empty_fetcher.EmptyFetcher{},
+		&config.Configuration{MaxRequestSize: int64(len(reqBody))},
+		&metricsConfig.NilMetricsEngine{},
+		analyticsConf.NewPBSAnalytics(&config.Analytics{}),
+		map[string]string{},
+		false,
+		[]byte{},
+		openrtb_ext.BuildBidderMap(),
+		nil,
+		nil,
+		hardcodedResponseIPValidator{response: true},
+	}
+
+	req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(reqBody))
+	recorder := httptest.NewRecorder()
+
+	deps.Auction(recorder, req, nil)
+
+	if recorder.Code != http.StatusOK {
+		t.Errorf("Endpoint should return a 200")
+	}
+	resultRequest := deps.ex.(*mockExchangeFPD).lastRequest
+	resultFPD := deps.ex.(*mockExchangeFPD).firstPartyData
+
+	assert.Len(t, resultFPD, 2, "Result FPD length is incorrect")
+
+	assert.NotNil(t, resultFPD[openrtb_ext.BidderName("bidder1")], "Result FPD for bidder1 is incorrect")
+	assert.NotNil(t, resultFPD[openrtb_ext.BidderName("bidder1")].Site, "Result FPD for bidder1.Site is incorrect")
+	assert.Nil(t, resultFPD[openrtb_ext.BidderName("bidder1")].App, "Result FPD for bidder1.App is incorrect")
+	assert.NotNil(t, resultFPD[openrtb_ext.BidderName("bidder1")].User, "Result FPD for bidder1.User is incorrect")
+
+	assert.NotNil(t, resultFPD[openrtb_ext.BidderName("bidder2")], "Result FPD for bidder2 is incorrect")
+	assert.NotNil(t, resultFPD[openrtb_ext.BidderName("bidder2")].Site, "Result FPD for bidder2.Site is incorrect")
+	assert.Nil(t, resultFPD[openrtb_ext.BidderName("bidder2")].App, "Result FPD for bidder2.App is incorrect")
+	assert.NotNil(t, resultFPD[openrtb_ext.BidderName("bidder2")].User, "Result FPD for bidder2.User is incorrect")
+
+	assert.Nil(t, resultRequest.App, "Result request App should be nil")
+	assert.Nil(t, resultRequest.Site.Content.Data, "Result request Site.Content.Data is incorrect")
+	assert.JSONEq(t, string(resultRequest.Site.Ext), `{"amp": 1}`, "Result request Site.Ext is incorrect")
+	assert.Nil(t, resultRequest.User.Ext, "Result request User.Ext is incorrect")
+}
+
 func TestValidateNativeContextTypes(t *testing.T) {
 	impIndex := 4
 
@@ -3739,6 +3786,17 @@ func (m *mockExchange) HoldAuction(ctx context.Context, r exchange.AuctionReques
 	}, nil
 }
 
+type mockExchangeFPD struct {
+	lastRequest    *openrtb2.BidRequest
+	firstPartyData map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData
+}
+
+func (m *mockExchangeFPD) HoldAuction(ctx context.Context, r exchange.AuctionRequest, debugLog *exchange.DebugLog) (*openrtb2.BidResponse, error) {
+	m.lastRequest = r.BidRequest
+	m.firstPartyData = r.FirstPartyData
+	return &openrtb2.BidResponse{}, nil
+}
+
 func getBidderInfos(cfg map[string]config.Adapter, biddersNames []openrtb_ext.BidderName) config.BidderInfos {
 	biddersInfos := make(config.BidderInfos)
 	for _, name := range biddersNames {
diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go
index 82f05851..401c245d 100644
--- a/exchange/bidder_test.go
+++ b/exchange/bidder_test.go
@@ -1301,7 +1301,7 @@ func TestMobileNativeTypes(t *testing.T) {
 		var actualValue string
 		for _, bid := range seatBids.bids {
 			actualValue = bid.bid.AdM
-			diffJson(t, tc.description, []byte(actualValue), []byte(tc.expectedValue))
+			assert.JSONEq(t, tc.expectedValue, actualValue, tc.description)
 		}
 	}
 }
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index b4fd270d..c7429dff 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"github.com/prebid/prebid-server/firstpartydata"
 	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
@@ -34,7 +35,6 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
 	"github.com/yudai/gojsondiff"
-	"github.com/yudai/gojsondiff/formatter"
 )
 
 func TestNewExchange(t *testing.T) {
@@ -377,7 +377,7 @@ func TestDebugBehaviour(t *testing.T) {
 
 			// If not nil, assert bid extension
 			if test.in.debug {
-				diffJson(t, test.desc, bidRequest.Ext, actualExt.Debug.ResolvedRequest.Ext)
+				assert.JSONEq(t, string(bidRequest.Ext), string(actualExt.Debug.ResolvedRequest.Ext), test.desc)
 			}
 		} else if !test.debugData.bidderLevelDebugAllowed && test.debugData.accountLevelDebugAllowed {
 			assert.Equal(t, len(actualExt.Debug.HttpCalls), 0, "%s. ext.debug.httpcalls array should not be empty", "With bidder level debug disable option http calls should be empty")
@@ -2143,7 +2143,7 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
 	}
 	if spec.IncomingRequest.OrtbRequest.Test == 1 {
 		//compare debug info
-		diffJson(t, "Debug info modified", bid.Ext, spec.Response.Ext)
+		assert.JSONEq(t, string(bid.Ext), string(spec.Response.Ext), "Debug info modified")
 	}
 }
 
@@ -3613,6 +3613,60 @@ func TestMakeBidExtJSON(t *testing.T) {
 	}
 }
 
+func TestFPD(t *testing.T) {
+
+	bidRequest := &openrtb2.BidRequest{
+		ID: "some-request-id",
+		Imp: []openrtb2.Imp{{
+			ID:    "some-impression-id",
+			Video: &openrtb2.Video{W: 100, H: 50},
+			Ext:   json.RawMessage(`{"appnexus": {"placementId": 1}}`),
+		}},
+		Site: &openrtb2.Site{ID: "Req site id", Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)},
+		AT:   1,
+		TMax: 500,
+	}
+
+	fpd := make(map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData)
+
+	apnFpd := firstpartydata.ResolvedFirstPartyData{
+		Site: &openrtb2.Site{ID: "fpdSite"},
+		App:  &openrtb2.App{ID: "fpdApp"},
+		User: &openrtb2.User{ID: "fpdUser"},
+	}
+	fpd[openrtb_ext.BidderName("appnexus")] = &apnFpd
+
+	e := new(exchange)
+	e.adapterMap = map[openrtb_ext.BidderName]adaptedBidder{
+		openrtb_ext.BidderAppnexus: &capturingRequestBidder{},
+	}
+	e.me = &metricsConf.NilMetricsEngine{}
+	e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0))
+
+	ctx := context.Background()
+
+	auctionRequest := AuctionRequest{
+		BidRequest:     bidRequest,
+		UserSyncs:      &emptyUsersync{},
+		FirstPartyData: fpd,
+	}
+
+	debugLog := &DebugLog{DebugOverride: true, DebugEnabledOrOverridden: true}
+
+	outBidResponse, err := e.HoldAuction(ctx, auctionRequest, debugLog)
+
+	assert.NotNilf(t, outBidResponse, "outBidResponse should not be nil")
+	assert.Nil(t, err, "Error should be nil")
+
+	request := e.adapterMap[openrtb_ext.BidderAppnexus].(*capturingRequestBidder).req
+
+	assert.NotNil(t, request, "Bidder request should not be nil")
+	assert.Equal(t, apnFpd.Site, request.Site, "Site is incorrect")
+	assert.Equal(t, apnFpd.App, request.App, "App is incorrect")
+	assert.Equal(t, apnFpd.User, request.User, "User is incorrect")
+
+}
+
 type exchangeSpec struct {
 	GDPREnabled       bool                   `json:"gdpr_enabled"`
 	IncomingRequest   exchangeRequest        `json:"incomingRequest"`
@@ -3733,6 +3787,15 @@ func (b *validatingBidder) requestBid(ctx context.Context, request *openrtb2.Bid
 	return
 }
 
+type capturingRequestBidder struct {
+	req *openrtb2.BidRequest
+}
+
+func (b *capturingRequestBidder) requestBid(ctx context.Context, request *openrtb2.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, accountDebugAllowed, headerDebugAllowed bool) (seatBid *pbsOrtbSeatBid, errs []error) {
+	b.req = request
+	return &pbsOrtbSeatBid{}, nil
+}
+
 func diffOrtbRequests(t *testing.T, description string, expected *openrtb2.BidRequest, actual *openrtb2.BidRequest) {
 	t.Helper()
 	actualJSON, err := json.Marshal(actual)
@@ -3745,7 +3808,7 @@ func diffOrtbRequests(t *testing.T, description string, expected *openrtb2.BidRe
 		t.Fatalf("%s failed to marshal expected BidRequest into JSON. %v", description, err)
 	}
 
-	diffJson(t, description, actualJSON, expectedJSON)
+	assert.JSONEq(t, string(expectedJSON), string(actualJSON), description)
 }
 
 func diffOrtbResponses(t *testing.T, description string, expected *openrtb2.BidResponse, actual *openrtb2.BidResponse) {
@@ -3768,7 +3831,7 @@ func diffOrtbResponses(t *testing.T, description string, expected *openrtb2.BidR
 		t.Fatalf("%s failed to marshal expected BidResponse into JSON. %v", description, err)
 	}
 
-	diffJson(t, description, actualJSON, expectedJSON)
+	assert.JSONEq(t, string(expectedJSON), string(actualJSON), description)
 }
 
 func mapifySeatBids(t *testing.T, context string, seatBids []openrtb2.SeatBid) map[string]*openrtb2.SeatBid {
@@ -3784,32 +3847,6 @@ func mapifySeatBids(t *testing.T, context string, seatBids []openrtb2.SeatBid) m
 	return seatMap
 }
 
-// diffJson compares two JSON byte arrays for structural equality. It will produce an error if either
-// byte array is not actually JSON.
-func diffJson(t *testing.T, description string, actual []byte, expected []byte) {
-	t.Helper()
-	diff, err := gojsondiff.New().Compare(actual, expected)
-	if err != nil {
-		t.Fatalf("%s json diff failed. %v", description, err)
-	}
-
-	if diff.Modified() {
-		var left interface{}
-		if err := json.Unmarshal(actual, &left); err != nil {
-			t.Fatalf("%s json did not match, but unmarshalling failed. %v", description, err)
-		}
-		printer := formatter.NewAsciiFormatter(left, formatter.AsciiFormatterConfig{
-			ShowArrayIndex: true,
-		})
-		output, err := printer.Format(diff)
-		if err != nil {
-			t.Errorf("%s did not match, but diff formatting failed. %v", description, err)
-		} else {
-			t.Errorf("%s json did not match expected.\n\n%s", description, output)
-		}
-	}
-}
-
 func mockHandler(statusCode int, getBody string, postBody string) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		w.WriteHeader(statusCode)
diff --git a/exchange/utils_test.go b/exchange/utils_test.go
index 6360fc3b..ef7d0bdb 100644
--- a/exchange/utils_test.go
+++ b/exchange/utils_test.go
@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"github.com/prebid/prebid-server/firstpartydata"
 	"testing"
 
 	"github.com/mxmCherry/openrtb/v15/openrtb2"
@@ -490,6 +491,70 @@ func TestCleanOpenRTBRequests(t *testing.T) {
 	}
 }
 
+func TestCleanOpenRTBRequestsWithFPD(t *testing.T) {
+	fpd := make(map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData)
+
+	apnFpd := firstpartydata.ResolvedFirstPartyData{
+		Site: &openrtb2.Site{Name: "fpdApnSite"},
+		App:  &openrtb2.App{Name: "fpdApnApp"},
+		User: &openrtb2.User{Keywords: "fpdApnUser"},
+	}
+	fpd[openrtb_ext.BidderName("appnexus")] = &apnFpd
+
+	brightrollFpd := firstpartydata.ResolvedFirstPartyData{
+		Site: &openrtb2.Site{Name: "fpdBrightrollSite"},
+		App:  &openrtb2.App{Name: "fpdBrightrollApp"},
+		User: &openrtb2.User{Keywords: "fpdBrightrollUser"},
+	}
+	fpd[openrtb_ext.BidderName("brightroll")] = &brightrollFpd
+
+	testCases := []struct {
+		description string
+		req         AuctionRequest
+		fpdExpected bool
+	}{
+		{
+			description: "Pass valid FPD data for bidder not found in the request",
+			req:         AuctionRequest{BidRequest: getTestBuildRequest(t), UserSyncs: &emptyUsersync{}, FirstPartyData: fpd},
+			fpdExpected: false,
+		},
+		{
+			description: "Pass valid FPD data for bidders specified in request",
+			req:         AuctionRequest{BidRequest: newAdapterAliasBidRequest(t), UserSyncs: &emptyUsersync{}, FirstPartyData: fpd},
+			fpdExpected: true,
+		},
+		{
+			description: "Bidders specified in request but there is no fpd data for this bidder",
+			req:         AuctionRequest{BidRequest: newAdapterAliasBidRequest(t), UserSyncs: &emptyUsersync{}, FirstPartyData: make(map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData)},
+			fpdExpected: false,
+		},
+		{
+			description: "No FPD data passed",
+			req:         AuctionRequest{BidRequest: newAdapterAliasBidRequest(t), UserSyncs: &emptyUsersync{}, FirstPartyData: nil},
+			fpdExpected: false,
+		},
+	}
+
+	for _, test := range testCases {
+		metricsMock := metrics.MetricsEngineMock{}
+		bidderToSyncerKey := map[string]string{}
+		permissions := permissionsMock{allowAllBidders: true, passGeo: true, passID: true}
+		bidderRequests, _, err := cleanOpenRTBRequests(context.Background(), test.req, nil, bidderToSyncerKey, &permissions, &metricsMock, gdpr.SignalNo, config.Privacy{}, nil)
+		assert.Empty(t, err, "No errors should be returned")
+		for _, bidderRequest := range bidderRequests {
+			bidderName := bidderRequest.BidderName
+			if test.fpdExpected {
+				assert.Equal(t, fpd[bidderName].Site.Name, bidderRequest.BidRequest.Site.Name, "Incorrect FPD site name")
+				assert.Equal(t, fpd[bidderName].App.Name, bidderRequest.BidRequest.App.Name, "Incorrect FPD app name")
+				assert.Equal(t, fpd[bidderName].User.Keywords, bidderRequest.BidRequest.User.Keywords, "Incorrect FPD user keywords")
+			} else {
+				assert.Equal(t, "", bidderRequest.BidRequest.Site.Name, "Incorrect FPD site name")
+				assert.Equal(t, "", bidderRequest.BidRequest.User.Keywords, "Incorrect FPD user keywords")
+			}
+		}
+	}
+}
+
 func TestCleanOpenRTBRequestsCCPA(t *testing.T) {
 	trueValue, falseValue := true, false
 
@@ -2543,3 +2608,61 @@ func TestBuildXPrebidHeader(t *testing.T) {
 		assert.Equal(t, test.result, result, test.description+":result")
 	}
 }
+
+func TestApplyFPD(t *testing.T) {
+
+	fpdBidderTest := map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData{}
+	bidderTest := openrtb_ext.BidderName("test")
+	fpdBidderTest[bidderTest] = &firstpartydata.ResolvedFirstPartyData{Site: nil, App: nil, User: nil}
+
+	fpdBidderNotNilFPD := map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData{}
+	bidderNotNilFPD := openrtb_ext.BidderName("notNilFPD")
+	fpdBidderNotNilFPD[bidderNotNilFPD] = &firstpartydata.ResolvedFirstPartyData{Site: &openrtb2.Site{ID: "SiteId"}, App: &openrtb2.App{ID: "AppId"}, User: &openrtb2.User{ID: "UserId"}}
+
+	fpdBidderApp := map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData{}
+	bidderApp := openrtb_ext.BidderName("AppFPD")
+	fpdBidderApp[bidderApp] = &firstpartydata.ResolvedFirstPartyData{App: &openrtb2.App{ID: "AppId"}}
+
+	testCases := []struct {
+		description     string
+		inputFpd        map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData
+		bidderName      openrtb_ext.BidderName
+		inputRequest    openrtb2.BidRequest
+		expectedRequest openrtb2.BidRequest
+	}{
+		{
+			description:     "req.Site defined; bidderFPD.Site not defined; expect request.Site remains the same",
+			inputFpd:        fpdBidderTest,
+			bidderName:      bidderTest,
+			inputRequest:    openrtb2.BidRequest{Site: &openrtb2.Site{ID: "SiteId"}},
+			expectedRequest: openrtb2.BidRequest{Site: &openrtb2.Site{ID: "SiteId"}},
+		},
+		{
+			description: "req.Site, req.App, req.User are not defined; bidderFPD.App, bidderFPD.Site and bidderFPD.User defined; " +
+				"expect req.Site, req.App, req.User to be overriden by bidderFPD.App, bidderFPD.Site and bidderFPD.User",
+			inputFpd:        fpdBidderNotNilFPD,
+			bidderName:      bidderNotNilFPD,
+			inputRequest:    openrtb2.BidRequest{},
+			expectedRequest: openrtb2.BidRequest{Site: &openrtb2.Site{ID: "SiteId"}, App: &openrtb2.App{ID: "AppId"}, User: &openrtb2.User{ID: "UserId"}},
+		},
+		{
+			description:     "req.Site, defined; bidderFPD.App defined; expect request.App to be overriden by bidderFPD.App; expect req.Site remains the same",
+			inputFpd:        fpdBidderApp,
+			bidderName:      bidderApp,
+			inputRequest:    openrtb2.BidRequest{Site: &openrtb2.Site{ID: "SiteId"}},
+			expectedRequest: openrtb2.BidRequest{Site: &openrtb2.Site{ID: "SiteId"}, App: &openrtb2.App{ID: "AppId"}},
+		},
+		{
+			description:     "req.Site, req.App defined; bidderFPD.App defined; expect request.App to be overriden by bidderFPD.App",
+			inputFpd:        fpdBidderApp,
+			bidderName:      bidderApp,
+			inputRequest:    openrtb2.BidRequest{Site: &openrtb2.Site{ID: "SiteId"}, App: &openrtb2.App{ID: "TestAppId"}},
+			expectedRequest: openrtb2.BidRequest{Site: &openrtb2.Site{ID: "SiteId"}, App: &openrtb2.App{ID: "AppId"}},
+		},
+	}
+
+	for _, testCase := range testCases {
+		applyFPD(testCase.inputFpd[testCase.bidderName], &testCase.inputRequest)
+		assert.Equal(t, testCase.expectedRequest, testCase.inputRequest, fmt.Sprintf("incorrect request after applying fpd, testcase %s", testCase.description))
+	}
+}
diff --git a/firstpartydata/first_party_data_test.go b/firstpartydata/first_party_data_test.go
new file mode 100644
index 00000000..1e09bab1
--- /dev/null
+++ b/firstpartydata/first_party_data_test.go
@@ -0,0 +1,1436 @@
+package firstpartydata
+
+import (
+	"encoding/json"
+	"github.com/mxmCherry/openrtb/v15/openrtb2"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/stretchr/testify/assert"
+	"io/ioutil"
+	"testing"
+)
+
+func TestExtractGlobalFPD(t *testing.T) {
+
+	testCases := []struct {
+		description string
+		input       openrtb_ext.RequestWrapper
+		expectedReq openrtb_ext.RequestWrapper
+		expectedFpd map[string][]byte
+	}{
+		{
+			description: "Site, app and user data present",
+			input: openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					ID: "bid_id",
+					Site: &openrtb2.Site{
+						ID:   "reqSiteId",
+						Page: "http://www.foobar.com/1234.html",
+						Publisher: &openrtb2.Publisher{
+							ID: "1",
+						},
+						Ext: json.RawMessage(`{"data": {"somesitefpd": "sitefpdDataTest"}}`),
+					},
+					User: &openrtb2.User{
+						ID:     "reqUserID",
+						Yob:    1982,
+						Gender: "M",
+						Ext:    json.RawMessage(`{"data": {"someuserfpd": "userfpdDataTest"}}`),
+					},
+					App: &openrtb2.App{
+						ID:  "appId",
+						Ext: json.RawMessage(`{"data": {"someappfpd": "appfpdDataTest"}}`),
+					},
+				},
+			},
+			expectedReq: openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{
+				ID: "bid_id",
+				Site: &openrtb2.Site{
+					ID:   "reqSiteId",
+					Page: "http://www.foobar.com/1234.html",
+					Publisher: &openrtb2.Publisher{
+						ID: "1",
+					},
+				},
+				User: &openrtb2.User{
+					ID:     "reqUserID",
+					Yob:    1982,
+					Gender: "M",
+				},
+				App: &openrtb2.App{
+					ID: "appId",
+				},
+			}},
+			expectedFpd: map[string][]byte{
+				"site": []byte(`{"somesitefpd": "sitefpdDataTest"}`),
+				"user": []byte(`{"someuserfpd": "userfpdDataTest"}`),
+				"app":  []byte(`{"someappfpd": "appfpdDataTest"}`),
+			},
+		},
+		{
+			description: "App FPD only present",
+			input: openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					ID: "bid_id",
+					Site: &openrtb2.Site{
+						ID:   "reqSiteId",
+						Page: "http://www.foobar.com/1234.html",
+						Publisher: &openrtb2.Publisher{
+							ID: "1",
+						},
+					},
+					App: &openrtb2.App{
+						ID:  "appId",
+						Ext: json.RawMessage(`{"data": {"someappfpd": "appfpdDataTest"}}`),
+					},
+				},
+			},
+			expectedReq: openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					ID: "bid_id",
+					Site: &openrtb2.Site{
+						ID:   "reqSiteId",
+						Page: "http://www.foobar.com/1234.html",
+						Publisher: &openrtb2.Publisher{
+							ID: "1",
+						},
+					},
+					App: &openrtb2.App{
+						ID: "appId",
+					},
+				},
+			},
+			expectedFpd: map[string][]byte{
+				"app":  []byte(`{"someappfpd": "appfpdDataTest"}`),
+				"user": nil,
+				"site": nil,
+			},
+		},
+		{
+			description: "User FPD only present",
+			input: openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					ID: "bid_id",
+					Site: &openrtb2.Site{
+						ID:   "reqSiteId",
+						Page: "http://www.foobar.com/1234.html",
+						Publisher: &openrtb2.Publisher{
+							ID: "1",
+						},
+					},
+					User: &openrtb2.User{
+						ID:     "reqUserID",
+						Yob:    1982,
+						Gender: "M",
+						Ext:    json.RawMessage(`{"data": {"someuserfpd": "userfpdDataTest"}}`),
+					},
+				},
+			},
+			expectedReq: openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					ID: "bid_id",
+					Site: &openrtb2.Site{
+						ID:   "reqSiteId",
+						Page: "http://www.foobar.com/1234.html",
+						Publisher: &openrtb2.Publisher{
+							ID: "1",
+						},
+					},
+					User: &openrtb2.User{
+						ID:     "reqUserID",
+						Yob:    1982,
+						Gender: "M",
+					},
+				},
+			},
+			expectedFpd: map[string][]byte{
+				"app":  nil,
+				"user": []byte(`{"someuserfpd": "userfpdDataTest"}`),
+				"site": nil,
+			},
+		},
+		{
+			description: "No FPD present in req",
+			input: openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					ID: "bid_id",
+					Site: &openrtb2.Site{
+						ID:   "reqSiteId",
+						Page: "http://www.foobar.com/1234.html",
+						Publisher: &openrtb2.Publisher{
+							ID: "1",
+						},
+					},
+					User: &openrtb2.User{
+						ID:     "reqUserID",
+						Yob:    1982,
+						Gender: "M",
+					},
+					App: &openrtb2.App{
+						ID: "appId",
+					},
+				},
+			},
+			expectedReq: openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					ID: "bid_id",
+					Site: &openrtb2.Site{
+						ID:   "reqSiteId",
+						Page: "http://www.foobar.com/1234.html",
+						Publisher: &openrtb2.Publisher{
+							ID: "1",
+						},
+					},
+					User: &openrtb2.User{
+						ID:     "reqUserID",
+						Yob:    1982,
+						Gender: "M",
+					},
+					App: &openrtb2.App{
+						ID: "appId",
+					},
+				},
+			},
+			expectedFpd: map[string][]byte{
+				"app":  nil,
+				"user": nil,
+				"site": nil,
+			},
+		},
+		{
+			description: "Site FPD only present",
+			input: openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					ID: "bid_id",
+					Site: &openrtb2.Site{
+						ID:   "reqSiteId",
+						Page: "http://www.foobar.com/1234.html",
+						Publisher: &openrtb2.Publisher{
+							ID: "1",
+						},
+						Ext: json.RawMessage(`{"data": {"someappfpd": true}}`),
+					},
+					App: &openrtb2.App{
+						ID: "appId",
+					},
+				},
+			},
+			expectedReq: openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					ID: "bid_id",
+					Site: &openrtb2.Site{
+						ID:   "reqSiteId",
+						Page: "http://www.foobar.com/1234.html",
+						Publisher: &openrtb2.Publisher{
+							ID: "1",
+						},
+					},
+					App: &openrtb2.App{
+						ID: "appId",
+					},
+				},
+			},
+			expectedFpd: map[string][]byte{
+				"app":  nil,
+				"user": nil,
+				"site": []byte(`{"someappfpd": true}`),
+			},
+		},
+	}
+	for _, test := range testCases {
+
+		inputReq := &test.input
+		fpd, err := ExtractGlobalFPD(inputReq)
+		assert.NoError(t, err, "Error should be nil")
+		err = inputReq.RebuildRequest()
+		assert.NoError(t, err, "Error should be nil")
+
+		assert.Equal(t, test.expectedReq.BidRequest, inputReq.BidRequest, "Incorrect input request after global fpd extraction")
+
+		assert.Equal(t, test.expectedFpd[userKey], fpd[userKey], "Incorrect User FPD")
+		assert.Equal(t, test.expectedFpd[appKey], fpd[appKey], "Incorrect App FPD")
+		assert.Equal(t, test.expectedFpd[siteKey], fpd[siteKey], "Incorrect Site FPDt")
+	}
+}
+
+func TestExtractOpenRtbGlobalFPD(t *testing.T) {
+
+	testCases := []struct {
+		description     string
+		input           openrtb2.BidRequest
+		output          openrtb2.BidRequest
+		expectedFpdData map[string][]openrtb2.Data
+	}{
+		{
+			description: "Site, app and user data present",
+			input: openrtb2.BidRequest{
+				ID: "bid_id",
+				Imp: []openrtb2.Imp{
+					{ID: "impid"},
+				},
+				Site: &openrtb2.Site{
+					ID: "reqSiteId",
+					Content: &openrtb2.Content{
+						Data: []openrtb2.Data{
+							{ID: "siteDataId1", Name: "siteDataName1"},
+							{ID: "siteDataId2", Name: "siteDataName2"},
+						},
+					},
+				},
+				User: &openrtb2.User{
+					ID:     "reqUserID",
+					Yob:    1982,
+					Gender: "M",
+					Data: []openrtb2.Data{
+						{ID: "userDataId1", Name: "userDataName1"},
+					},
+				},
+				App: &openrtb2.App{
+					ID: "appId",
+					Content: &openrtb2.Content{
+						Data: []openrtb2.Data{
+							{ID: "appDataId1", Name: "appDataName1"},
+						},
+					},
+				},
+			},
+			output: openrtb2.BidRequest{
+				ID: "bid_id",
+				Imp: []openrtb2.Imp{
+					{ID: "impid"},
+				},
+				Site: &openrtb2.Site{
+					ID:      "reqSiteId",
+					Content: &openrtb2.Content{},
+				},
+				User: &openrtb2.User{
+					ID:     "reqUserID",
+					Yob:    1982,
+					Gender: "M",
+				},
+				App: &openrtb2.App{
+					ID:      "appId",
+					Content: &openrtb2.Content{},
+				},
+			},
+			expectedFpdData: map[string][]openrtb2.Data{
+				siteContentDataKey: {{ID: "siteDataId1", Name: "siteDataName1"}, {ID: "siteDataId2", Name: "siteDataName2"}},
+				userDataKey:        {{ID: "userDataId1", Name: "userDataName1"}},
+				appContentDataKey:  {{ID: "appDataId1", Name: "appDataName1"}},
+			},
+		},
+		{
+			description: "No Site, app or user data present",
+			input: openrtb2.BidRequest{
+				ID: "bid_id",
+				Imp: []openrtb2.Imp{
+					{ID: "impid"},
+				},
+			},
+			output: openrtb2.BidRequest{
+				ID: "bid_id",
+				Imp: []openrtb2.Imp{
+					{ID: "impid"},
+				},
+			},
+			expectedFpdData: map[string][]openrtb2.Data{
+				siteContentDataKey: nil,
+				userDataKey:        nil,
+				appContentDataKey:  nil,
+			},
+		},
+		{
+			description: "Site only data present",
+			input: openrtb2.BidRequest{
+				ID: "bid_id",
+				Imp: []openrtb2.Imp{
+					{ID: "impid"},
+				},
+				Site: &openrtb2.Site{
+					ID:   "reqSiteId",
+					Page: "test/page",
+					Content: &openrtb2.Content{
+						Data: []openrtb2.Data{
+							{ID: "siteDataId1", Name: "siteDataName1"},
+						},
+					},
+				},
+			},
+			output: openrtb2.BidRequest{
+				ID: "bid_id",
+				Imp: []openrtb2.Imp{
+					{ID: "impid"},
+				},
+				Site: &openrtb2.Site{
+					ID:      "reqSiteId",
+					Page:    "test/page",
+					Content: &openrtb2.Content{},
+				},
+			},
+			expectedFpdData: map[string][]openrtb2.Data{
+				siteContentDataKey: {{ID: "siteDataId1", Name: "siteDataName1"}},
+				userDataKey:        nil,
+				appContentDataKey:  nil,
+			},
+		},
+		{
+			description: "App only data present",
+			input: openrtb2.BidRequest{
+				ID: "bid_id",
+				Imp: []openrtb2.Imp{
+					{ID: "impid"},
+				},
+				App: &openrtb2.App{
+					ID: "reqAppId",
+					Content: &openrtb2.Content{
+						Data: []openrtb2.Data{
+							{ID: "appDataId1", Name: "appDataName1"},
+						},
+					},
+				},
+			},
+			output: openrtb2.BidRequest{
+				ID: "bid_id",
+				Imp: []openrtb2.Imp{
+					{ID: "impid"},
+				},
+				App: &openrtb2.App{
+					ID:      "reqAppId",
+					Content: &openrtb2.Content{},
+				},
+			},
+			expectedFpdData: map[string][]openrtb2.Data{
+				siteContentDataKey: nil,
+				userDataKey:        nil,
+				appContentDataKey:  {{ID: "appDataId1", Name: "appDataName1"}},
+			},
+		},
+		{
+			description: "User only data present",
+			input: openrtb2.BidRequest{
+				ID: "bid_id",
+				Imp: []openrtb2.Imp{
+					{ID: "impid"},
+				},
+				Site: &openrtb2.Site{
+					ID: "reqSiteId",
+				},
+				App: &openrtb2.App{
+					ID: "reqAppId",
+				},
+				User: &openrtb2.User{
+					ID:     "reqUserId",
+					Yob:    1982,
+					Gender: "M",
+					Data: []openrtb2.Data{
+						{ID: "userDataId1", Name: "userDataName1"},
+					},
+				},
+			},
+			output: openrtb2.BidRequest{
+				ID: "bid_id",
+				Imp: []openrtb2.Imp{
+					{ID: "impid"},
+				},
+				Site: &openrtb2.Site{
+					ID: "reqSiteId",
+				},
+				App: &openrtb2.App{
+					ID: "reqAppId",
+				},
+				User: &openrtb2.User{
+					ID:     "reqUserId",
+					Yob:    1982,
+					Gender: "M",
+				},
+			},
+			expectedFpdData: map[string][]openrtb2.Data{
+				siteContentDataKey: nil,
+				userDataKey:        {{ID: "userDataId1", Name: "userDataName1"}},
+				appContentDataKey:  nil,
+			},
+		},
+	}
+	for _, test := range testCases {
+
+		inputReq := &test.input
+
+		res := ExtractOpenRtbGlobalFPD(inputReq)
+
+		assert.Equal(t, &test.output, inputReq, "Result request is incorrect")
+		assert.Equal(t, test.expectedFpdData[siteContentDataKey], res[siteContentDataKey], "siteContentData data is incorrect")
+		assert.Equal(t, test.expectedFpdData[userDataKey], res[userDataKey], "userData is incorrect")
+		assert.Equal(t, test.expectedFpdData[appContentDataKey], res[appContentDataKey], "appContentData is incorrect")
+
+	}
+}
+
+func TestExtractBidderConfigFPD(t *testing.T) {
+
+	if specFiles, err := ioutil.ReadDir("./tests/extractbidderconfigfpd"); err == nil {
+		for _, specFile := range specFiles {
+			fileName := "./tests/extractbidderconfigfpd/" + specFile.Name()
+
+			fpdFile, err := loadFpdFile(fileName)
+			if err != nil {
+				t.Errorf("Unable to load file: %s", fileName)
+			}
+			var extReq openrtb_ext.ExtRequestPrebid
+			err = json.Unmarshal(fpdFile.InputRequestData, &extReq)
+			if err != nil {
+				t.Errorf("Unable to unmarshal input request: %s", fileName)
+			}
+			reqExt := openrtb_ext.RequestExt{}
+			reqExt.SetPrebid(&extReq)
+			fpdData, err := ExtractBidderConfigFPD(&reqExt)
+
+			if len(fpdFile.ValidationErrors) > 0 {
+				assert.Equal(t, err.Error(), fpdFile.ValidationErrors[0].Message, "Incorrect first party data error message")
+				continue
+			}
+
+			assert.Nil(t, reqExt.GetPrebid().BidderConfigs, "Bidder specific FPD config should be removed from request")
+
+			assert.Nil(t, err, "No error should be returned")
+			assert.Equal(t, len(fpdFile.BidderConfigFPD), len(fpdData), "Incorrect fpd data")
+
+			for bidderName, bidderFPD := range fpdFile.BidderConfigFPD {
+
+				if bidderFPD.Site != nil {
+					resSite := fpdData[bidderName].Site
+					for k, v := range bidderFPD.Site {
+						assert.NotNil(t, resSite[k], "Property is not found in result site")
+						assert.JSONEq(t, string(v), string(resSite[k]), "site is incorrect")
+					}
+				} else {
+					assert.Nil(t, fpdData[bidderName].Site, "Result site should be also nil")
+				}
+
+				if bidderFPD.App != nil {
+					resApp := fpdData[bidderName].App
+					for k, v := range bidderFPD.App {
+						assert.NotNil(t, resApp[k], "Property is not found in result app")
+						assert.JSONEq(t, string(v), string(resApp[k]), "app is incorrect")
+					}
+				} else {
+					assert.Nil(t, fpdData[bidderName].App, "Result app should be also nil")
+				}
+
+				if bidderFPD.User != nil {
+					resUser := fpdData[bidderName].User
+					for k, v := range bidderFPD.User {
+						assert.NotNil(t, resUser[k], "Property is not found in result user")
+						assert.JSONEq(t, string(v), string(resUser[k]), "site is incorrect")
+					}
+				} else {
+					assert.Nil(t, fpdData[bidderName].User, "Result user should be also nil")
+				}
+			}
+		}
+	}
+}
+
+func TestResolveFPD(t *testing.T) {
+
+	if specFiles, err := ioutil.ReadDir("./tests/resolvefpd"); err == nil {
+		for _, specFile := range specFiles {
+			fileName := "./tests/resolvefpd/" + specFile.Name()
+
+			fpdFile, err := loadFpdFile(fileName)
+			if err != nil {
+				t.Errorf("Unable to load file: %s", fileName)
+			}
+
+			var inputReq openrtb2.BidRequest
+			err = json.Unmarshal(fpdFile.InputRequestData, &inputReq)
+			if err != nil {
+				t.Errorf("Unable to unmarshal input request: %s", fileName)
+			}
+
+			var inputReqCopy openrtb2.BidRequest
+			err = json.Unmarshal(fpdFile.InputRequestData, &inputReqCopy)
+			if err != nil {
+				t.Errorf("Unable to unmarshal input request: %s", fileName)
+			}
+
+			var outputReq openrtb2.BidRequest
+			err = json.Unmarshal(fpdFile.OutputRequestData, &outputReq)
+			if err != nil {
+				t.Errorf("Unable to unmarshal output request: %s", fileName)
+			}
+
+			reqExtFPD := make(map[string][]byte, 3)
+			reqExtFPD["site"] = fpdFile.GlobalFPD["site"]
+			reqExtFPD["app"] = fpdFile.GlobalFPD["app"]
+			reqExtFPD["user"] = fpdFile.GlobalFPD["user"]
+
+			reqFPD := make(map[string][]openrtb2.Data, 3)
+
+			reqFPDSiteContentData := fpdFile.GlobalFPD[siteContentDataKey]
+			if len(reqFPDSiteContentData) > 0 {
+				var siteConData []openrtb2.Data
+				err = json.Unmarshal(reqFPDSiteContentData, &siteConData)
+				if err != nil {
+					t.Errorf("Unable to unmarshal site.content.data: %s", fileName)
+				}
+				reqFPD[siteContentDataKey] = siteConData
+			}
+
+			reqFPDAppContentData := fpdFile.GlobalFPD[appContentDataKey]
+			if len(reqFPDAppContentData) > 0 {
+				var appConData []openrtb2.Data
+				err = json.Unmarshal(reqFPDAppContentData, &appConData)
+				if err != nil {
+					t.Errorf("Unable to unmarshal app.content.data: %s", fileName)
+				}
+				reqFPD[appContentDataKey] = appConData
+			}
+
+			reqFPDUserData := fpdFile.GlobalFPD[userDataKey]
+			if len(reqFPDUserData) > 0 {
+				var userData []openrtb2.Data
+				err = json.Unmarshal(reqFPDUserData, &userData)
+				if err != nil {
+					t.Errorf("Unable to unmarshal app.content.data: %s", fileName)
+				}
+				reqFPD[userDataKey] = userData
+			}
+			if fpdFile.BidderConfigFPD == nil {
+				fpdFile.BidderConfigFPD = make(map[openrtb_ext.BidderName]*openrtb_ext.ORTB2)
+				fpdFile.BidderConfigFPD["appnexus"] = &openrtb_ext.ORTB2{}
+			}
+
+			resultFPD, errL := ResolveFPD(&inputReq, fpdFile.BidderConfigFPD, reqExtFPD, reqFPD, []string{"appnexus"})
+
+			if len(errL) == 0 {
+				assert.Equal(t, inputReq, inputReqCopy, "Original request should not be modified")
+
+				bidderFPD := resultFPD["appnexus"]
+
+				if outputReq.Site != nil && len(outputReq.Site.Ext) > 0 {
+					resSiteExt := bidderFPD.Site.Ext
+					expectedSiteExt := outputReq.Site.Ext
+					bidderFPD.Site.Ext = nil
+					outputReq.Site.Ext = nil
+					assert.JSONEq(t, string(expectedSiteExt), string(resSiteExt), "site.ext is incorrect")
+
+					assert.Equal(t, outputReq.Site, bidderFPD.Site, "Site is incorrect")
+				}
+				if outputReq.App != nil && len(outputReq.App.Ext) > 0 {
+					resAppExt := bidderFPD.App.Ext
+					expectedAppExt := outputReq.App.Ext
+					bidderFPD.App.Ext = nil
+					outputReq.App.Ext = nil
+					assert.JSONEq(t, string(expectedAppExt), string(resAppExt), "app.ext is incorrect")
+
+					assert.Equal(t, outputReq.App, bidderFPD.App, "App is incorrect")
+				}
+				if outputReq.User != nil && len(outputReq.User.Ext) > 0 {
+					resUserExt := bidderFPD.User.Ext
+					expectedUserExt := outputReq.User.Ext
+					bidderFPD.User.Ext = nil
+					outputReq.User.Ext = nil
+					assert.JSONEq(t, string(expectedUserExt), string(resUserExt), "user.ext is incorrect")
+
+					assert.Equal(t, outputReq.User, bidderFPD.User, "User is incorrect")
+				}
+			} else {
+				assert.ElementsMatch(t, errL, fpdFile.ValidationErrors, "Incorrect first party data warning message")
+			}
+
+		}
+	}
+}
+
+func TestExtractFPDForBidders(t *testing.T) {
+
+	if specFiles, err := ioutil.ReadDir("./tests/extractfpdforbidders"); err == nil {
+		for _, specFile := range specFiles {
+			fileName := "./tests/extractfpdforbidders/" + specFile.Name()
+			fpdFile, err := loadFpdFile(fileName)
+
+			if err != nil {
+				t.Errorf("Unable to load file: %s", fileName)
+			}
+
+			var expectedRequest openrtb2.BidRequest
+			err = json.Unmarshal(fpdFile.OutputRequestData, &expectedRequest)
+			if err != nil {
+				t.Errorf("Unable to unmarshal input request: %s", fileName)
+			}
+
+			resultRequest := &openrtb_ext.RequestWrapper{}
+			resultRequest.BidRequest = &openrtb2.BidRequest{}
+			err = json.Unmarshal(fpdFile.InputRequestData, resultRequest.BidRequest)
+			assert.NoError(t, err, "Error should be nil")
+
+			resultFPD, errL := ExtractFPDForBidders(resultRequest)
+
+			if len(fpdFile.ValidationErrors) > 0 {
+				assert.Equal(t, len(fpdFile.ValidationErrors), len(errL), "Incorrect number of errors was returned")
+				assert.ElementsMatch(t, errL, fpdFile.ValidationErrors, "Incorrect errors were returned")
+				//in case or error no further assertions needed
+				continue
+			}
+			assert.Empty(t, errL, "Error should be empty")
+			assert.Equal(t, len(resultFPD), len(fpdFile.BiddersFPDResolved))
+
+			for bidderName, expectedValue := range fpdFile.BiddersFPDResolved {
+				actualValue := resultFPD[bidderName]
+				if expectedValue.Site != nil {
+					if len(expectedValue.Site.Ext) > 0 {
+						assert.JSONEq(t, string(expectedValue.Site.Ext), string(actualValue.Site.Ext), "Incorrect first party data")
+						expectedValue.Site.Ext = nil
+						actualValue.Site.Ext = nil
+					}
+					assert.Equal(t, expectedValue.Site, actualValue.Site, "Incorrect first party data")
+				}
+				if expectedValue.App != nil {
+					if len(expectedValue.App.Ext) > 0 {
+						assert.JSONEq(t, string(expectedValue.App.Ext), string(actualValue.App.Ext), "Incorrect first party data")
+						expectedValue.App.Ext = nil
+						actualValue.App.Ext = nil
+					}
+					assert.Equal(t, expectedValue.App, actualValue.App, "Incorrect first party data")
+				}
+				if expectedValue.User != nil {
+					if len(expectedValue.User.Ext) > 0 {
+						assert.JSONEq(t, string(expectedValue.User.Ext), string(actualValue.User.Ext), "Incorrect first party data")
+						expectedValue.User.Ext = nil
+						actualValue.User.Ext = nil
+					}
+					assert.Equal(t, expectedValue.User, actualValue.User, "Incorrect first party data")
+				}
+			}
+
+			if expectedRequest.Site != nil {
+				if len(expectedRequest.Site.Ext) > 0 {
+					assert.JSONEq(t, string(expectedRequest.Site.Ext), string(resultRequest.BidRequest.Site.Ext), "Incorrect site in request")
+					expectedRequest.Site.Ext = nil
+					resultRequest.BidRequest.Site.Ext = nil
+				}
+				assert.Equal(t, expectedRequest.Site, resultRequest.BidRequest.Site, "Incorrect site in request")
+			}
+			if expectedRequest.App != nil {
+				if len(expectedRequest.App.Ext) > 0 {
+					assert.JSONEq(t, string(expectedRequest.App.Ext), string(resultRequest.BidRequest.App.Ext), "Incorrect app in request")
+					expectedRequest.App.Ext = nil
+					resultRequest.BidRequest.App.Ext = nil
+				}
+				assert.Equal(t, expectedRequest.App, resultRequest.BidRequest.App, "Incorrect app in request")
+			}
+			if expectedRequest.User != nil {
+				if len(expectedRequest.User.Ext) > 0 {
+					assert.JSONEq(t, string(expectedRequest.User.Ext), string(resultRequest.BidRequest.User.Ext), "Incorrect user in request")
+					expectedRequest.User.Ext = nil
+					resultRequest.BidRequest.User.Ext = nil
+				}
+				assert.Equal(t, expectedRequest.User, resultRequest.BidRequest.User, "Incorrect user in request")
+			}
+
+		}
+	}
+}
+
+func loadFpdFile(filename string) (fpdFile, error) {
+	var fileData fpdFile
+	fileContents, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return fileData, err
+	}
+	err = json.Unmarshal(fileContents, &fileData)
+	if err != nil {
+		return fileData, err
+	}
+
+	return fileData, nil
+}
+
+type fpdFile struct {
+	InputRequestData   json.RawMessage                                    `json:"inputRequestData,omitempty"`
+	OutputRequestData  json.RawMessage                                    `json:"outputRequestData,omitempty"`
+	BidderConfigFPD    map[openrtb_ext.BidderName]*openrtb_ext.ORTB2      `json:"bidderConfigFPD,omitempty"`
+	BiddersFPDResolved map[openrtb_ext.BidderName]*ResolvedFirstPartyData `json:"biddersFPDResolved,omitempty"`
+	GlobalFPD          map[string]json.RawMessage                         `json:"globalFPD,omitempty"`
+	ValidationErrors   []*errortypes.BadInput                             `json:"validationErrors,omitempty"`
+}
+
+func TestResolveUser(t *testing.T) {
+
+	fpdConfigUser := make(map[string]json.RawMessage, 0)
+	fpdConfigUser["id"] = []byte(`"fpdConfigUserId"`)
+	fpdConfigUser[yobKey] = []byte(`1980`)
+	fpdConfigUser[genderKey] = []byte(`"M"`)
+	fpdConfigUser[keywordsKey] = []byte(`"fpdConfigUserKeywords"`)
+	fpdConfigUser["data"] = []byte(`[{"id":"UserDataId1", "name":"UserDataName1"}, {"id":"UserDataId2", "name":"UserDataName2"}]`)
+	fpdConfigUser["ext"] = []byte(`{"data":{"fpdConfigUserExt": 123}}`)
+
+	bidRequestUser := &openrtb2.User{
+		ID:       "bidRequestUserId",
+		Yob:      1990,
+		Gender:   "F",
+		Keywords: "bidRequestUserKeywords",
+	}
+
+	globalFPD := make(map[string][]byte, 0)
+	globalFPD[userKey] = []byte(`{"globalFPDUserData": "globalFPDUserDataValue"}`)
+
+	openRtbGlobalFPD := make(map[string][]openrtb2.Data, 0)
+	openRtbGlobalFPD[userDataKey] = []openrtb2.Data{
+		{ID: "openRtbGlobalFPDUserDataId1", Name: "openRtbGlobalFPDUserDataName1"},
+		{ID: "openRtbGlobalFPDUserDataId2", Name: "openRtbGlobalFPDUserDataName2"},
+	}
+
+	expectedUser := &openrtb2.User{
+		ID:       "bidRequestUserId",
+		Yob:      1980,
+		Gender:   "M",
+		Keywords: "fpdConfigUserKeywords",
+		Data: []openrtb2.Data{
+			{ID: "openRtbGlobalFPDUserDataId1", Name: "openRtbGlobalFPDUserDataName1"},
+			{ID: "openRtbGlobalFPDUserDataId2", Name: "openRtbGlobalFPDUserDataName2"},
+		},
+	}
+
+	testCases := []struct {
+		description       string
+		bidRequestUserExt []byte
+		expectedUserExt   string
+	}{
+		{
+			description:       "bid request user.ext is nil",
+			bidRequestUserExt: nil,
+			expectedUserExt: `{"data":{
+									"data":[
+										{"id":"UserDataId1","name":"UserDataName1"},
+										{"id":"UserDataId2","name":"UserDataName2"}
+									],
+									"fpdConfigUserExt":123,
+									"globalFPDUserData":"globalFPDUserDataValue",
+									"id":"fpdConfigUserId"
+									}
+							}`,
+		},
+		{
+			description:       "bid request user.ext is not nil",
+			bidRequestUserExt: []byte(`{"bidRequestUserExt": 1234}`),
+			expectedUserExt: `{"data":{
+									"data":[
+										{"id":"UserDataId1","name":"UserDataName1"},
+										{"id":"UserDataId2","name":"UserDataName2"}
+									],
+									"fpdConfigUserExt":123,
+									"globalFPDUserData":"globalFPDUserDataValue",
+									"id":"fpdConfigUserId"
+									},
+								"bidRequestUserExt":1234
+							}`,
+		},
+	}
+
+	for _, test := range testCases {
+		bidRequestUser.Ext = test.bidRequestUserExt
+
+		fpdConfigUser := make(map[string]json.RawMessage, 0)
+		fpdConfigUser["id"] = []byte(`"fpdConfigUserId"`)
+		fpdConfigUser[yobKey] = []byte(`1980`)
+		fpdConfigUser[genderKey] = []byte(`"M"`)
+		fpdConfigUser[keywordsKey] = []byte(`"fpdConfigUserKeywords"`)
+		fpdConfigUser["data"] = []byte(`[{"id":"UserDataId1", "name":"UserDataName1"}, {"id":"UserDataId2", "name":"UserDataName2"}]`)
+		fpdConfigUser["ext"] = []byte(`{"data":{"fpdConfigUserExt": 123}}`)
+		fpdConfig := &openrtb_ext.ORTB2{User: fpdConfigUser}
+
+		resultUser, err := resolveUser(fpdConfig, bidRequestUser, globalFPD, openRtbGlobalFPD, "appnexus")
+		assert.NoError(t, err, "No error should be returned")
+
+		assert.JSONEq(t, test.expectedUserExt, string(resultUser.Ext), "Result user.Ext is incorrect")
+		resultUser.Ext = nil
+		assert.Equal(t, expectedUser, resultUser, "Result user is incorrect")
+	}
+
+}
+
+func TestResolveUserNilValues(t *testing.T) {
+	resultUser, err := resolveUser(nil, nil, nil, nil, "appnexus")
+	assert.NoError(t, err, "No error should be returned")
+	assert.Nil(t, resultUser, "Result user should be nil")
+}
+
+func TestResolveUserBadInput(t *testing.T) {
+	fpdConfigUser := make(map[string]json.RawMessage, 0)
+	fpdConfigUser["id"] = []byte(`"fpdConfigUserId"`)
+	fpdConfig := &openrtb_ext.ORTB2{User: fpdConfigUser}
+
+	resultUser, err := resolveUser(fpdConfig, nil, nil, nil, "appnexus")
+	assert.Error(t, err, "Error should be returned")
+	assert.Equal(t, "incorrect First Party Data for bidder appnexus: User object is not defined in request, but defined in FPD config", err.Error(), "Incorrect error message")
+	assert.Nil(t, resultUser, "Result user should be nil")
+}
+
+func TestMergeUsers(t *testing.T) {
+
+	originalUser := &openrtb2.User{
+		ID:       "bidRequestUserId",
+		Yob:      1980,
+		Gender:   "M",
+		Keywords: "fpdConfigUserKeywords",
+		Data: []openrtb2.Data{
+			{ID: "openRtbGlobalFPDUserDataId1", Name: "openRtbGlobalFPDUserDataName1"},
+			{ID: "openRtbGlobalFPDUserDataId2", Name: "openRtbGlobalFPDUserDataName2"},
+		},
+		Ext: []byte(`{"bidRequestUserExt": 1234}`),
+	}
+	fpdConfigUser := make(map[string]json.RawMessage, 0)
+	fpdConfigUser["id"] = []byte(`"fpdConfigUserId"`)
+	fpdConfigUser[yobKey] = []byte(`1980`)
+	fpdConfigUser[genderKey] = []byte(`"M"`)
+	fpdConfigUser[keywordsKey] = []byte(`"fpdConfigUserKeywords"`)
+	fpdConfigUser["data"] = []byte(`[{"id":"UserDataId1", "name":"UserDataName1"}, {"id":"UserDataId2", "name":"UserDataName2"}]`)
+	fpdConfigUser["ext"] = []byte(`{"data":{"fpdConfigUserExt": 123}}`)
+
+	resultUser, err := mergeUsers(originalUser, fpdConfigUser)
+	assert.NoError(t, err, "No error should be returned")
+
+	expectedUserExt := `{"bidRequestUserExt":1234,
+						 "data":{
+							"data":[
+								{"id":"UserDataId1","name":"UserDataName1"},
+								{"id":"UserDataId2","name":"UserDataName2"}],
+							"fpdConfigUserExt":123,
+							"id":"fpdConfigUserId"}
+						 }`
+	assert.JSONEq(t, expectedUserExt, string(resultUser.Ext), "Result user.Ext is incorrect")
+	resultUser.Ext = nil
+
+	expectedUser := openrtb2.User{
+		ID:       "bidRequestUserId",
+		Yob:      1980,
+		Gender:   "M",
+		Keywords: "fpdConfigUserKeywords",
+		Data: []openrtb2.Data{
+			{ID: "openRtbGlobalFPDUserDataId1", Name: "openRtbGlobalFPDUserDataName1"},
+			{ID: "openRtbGlobalFPDUserDataId2", Name: "openRtbGlobalFPDUserDataName2"},
+		},
+	}
+	assert.Equal(t, expectedUser, resultUser, "Result user is incorrect")
+}
+
+func TestResolveExtension(t *testing.T) {
+
+	testCases := []struct {
+		description string
+		fpdConfig   map[string]json.RawMessage
+		originalExt json.RawMessage
+		expectedExt string
+	}{
+		{description: "Fpd config with ext only",
+			fpdConfig:   map[string]json.RawMessage{"ext": json.RawMessage(`{"data":{"fpdConfigUserExt": 123}}`)},
+			originalExt: json.RawMessage(`{"bidRequestUserExt": 1234}`),
+			expectedExt: `{"bidRequestUserExt":1234, "data":{"fpdConfigUserExt":123}}`,
+		},
+		{description: "Fpd config with ext and another property",
+			fpdConfig:   map[string]json.RawMessage{"ext": json.RawMessage(`{"data":{"fpdConfigUserExt": 123}}`), "prebid": json.RawMessage(`{"prebidData":{"isPrebid": true}}`)},
+			originalExt: json.RawMessage(`{"bidRequestUserExt": 1234}`),
+			expectedExt: `{"bidRequestUserExt":1234, "data":{"fpdConfigUserExt":123, "prebid":{"prebidData":{"isPrebid": true}}}}`,
+		},
+		{description: "Fpd config empty",
+			fpdConfig:   nil,
+			originalExt: json.RawMessage(`{"bidRequestUserExt": 1234}`),
+			expectedExt: `{"bidRequestUserExt":1234}`,
+		},
+		{description: "Original ext empty",
+			fpdConfig:   map[string]json.RawMessage{"ext": json.RawMessage(`{"data":{"fpdConfigUserExt": 123}}`)},
+			originalExt: nil,
+			expectedExt: `{"data":{"ext":{"data":{"fpdConfigUserExt":123}}}}`,
+		},
+	}
+
+	for _, test := range testCases {
+		resExt, err := resolveExtension(test.fpdConfig, test.originalExt)
+		assert.NoError(t, err, "No error should be returned")
+		assert.JSONEq(t, test.expectedExt, string(resExt), "result ext is incorrect")
+	}
+}
+
+func TestResolveSite(t *testing.T) {
+
+	fpdConfigSite := make(map[string]json.RawMessage, 0)
+	fpdConfigSite["id"] = []byte(`"fpdConfigSiteId"`)
+	fpdConfigSite[keywordsKey] = []byte(`"fpdConfigSiteKeywords"`)
+	fpdConfigSite[nameKey] = []byte(`"fpdConfigSiteName"`)
+	fpdConfigSite[pageKey] = []byte(`"fpdConfigSitePage"`)
+	fpdConfigSite["data"] = []byte(`[{"id":"SiteDataId1", "name":"SiteDataName1"}, {"id":"SiteDataId2", "name":"SiteDataName2"}]`)
+	fpdConfigSite["ext"] = []byte(`{"data":{"fpdConfigSiteExt": 123}}`)
+
+	bidRequestSite := &openrtb2.Site{
+		ID:       "bidRequestSiteId",
+		Keywords: "bidRequestSiteKeywords",
+		Name:     "bidRequestSiteName",
+		Page:     "bidRequestSitePage",
+		Content: &openrtb2.Content{
+			ID:      "bidRequestSiteContentId",
+			Episode: 4,
+			Data: []openrtb2.Data{
+				{ID: "bidRequestSiteContentDataId1", Name: "bidRequestSiteContentDataName1"},
+				{ID: "bidRequestSiteContentDataId2", Name: "bidRequestSiteContentDataName2"},
+			},
+		},
+	}
+
+	globalFPD := make(map[string][]byte, 0)
+	globalFPD[siteKey] = []byte(`{"globalFPDSiteData": "globalFPDSiteDataValue"}`)
+
+	openRtbGlobalFPD := make(map[string][]openrtb2.Data, 0)
+	openRtbGlobalFPD[siteContentDataKey] = []openrtb2.Data{
+		{ID: "openRtbGlobalFPDSiteContentDataId1", Name: "openRtbGlobalFPDSiteContentDataName1"},
+		{ID: "openRtbGlobalFPDSiteContentDataId2", Name: "openRtbGlobalFPDSiteContentDataName2"},
+	}
+
+	expectedSite := &openrtb2.Site{
+		ID:       "bidRequestSiteId",
+		Keywords: "fpdConfigSiteKeywords",
+		Name:     "bidRequestSiteName",
+		Page:     "bidRequestSitePage",
+		Content: &openrtb2.Content{
+			ID:      "bidRequestSiteContentId",
+			Episode: 4,
+			Data: []openrtb2.Data{
+				{ID: "openRtbGlobalFPDSiteContentDataId1", Name: "openRtbGlobalFPDSiteContentDataName1"},
+				{ID: "openRtbGlobalFPDSiteContentDataId2", Name: "openRtbGlobalFPDSiteContentDataName2"},
+			},
+		},
+	}
+
+	testCases := []struct {
+		description       string
+		bidRequestSiteExt []byte
+		expectedSiteExt   string
+		siteContentNil    bool
+	}{
+		{
+			description:       "bid request site.ext is nil",
+			bidRequestSiteExt: nil,
+			expectedSiteExt: `{"data":{
+									"data":[
+										{"id":"SiteDataId1","name":"SiteDataName1"},
+										{"id":"SiteDataId2","name":"SiteDataName2"}
+									],
+									"fpdConfigSiteExt":123,
+									"globalFPDSiteData":"globalFPDSiteDataValue",
+									"id":"fpdConfigSiteId"
+									}
+							}`,
+			siteContentNil: false,
+		},
+		{
+			description:       "bid request site.ext is not nil",
+			bidRequestSiteExt: []byte(`{"bidRequestSiteExt": 1234}`),
+			expectedSiteExt: `{"data":{
+									"data":[
+										{"id":"SiteDataId1","name":"SiteDataName1"},
+										{"id":"SiteDataId2","name":"SiteDataName2"}
+									],
+									"fpdConfigSiteExt":123,
+									"globalFPDSiteData":"globalFPDSiteDataValue",
+									"id":"fpdConfigSiteId"
+									},
+								"bidRequestSiteExt":1234
+							}`,
+			siteContentNil: false,
+		},
+		{
+			description:       "bid request site.content.data is nil ",
+			bidRequestSiteExt: []byte(`{"bidRequestSiteExt": 1234}`),
+			expectedSiteExt: `{"data":{
+									"data":[
+										{"id":"SiteDataId1","name":"SiteDataName1"},
+										{"id":"SiteDataId2","name":"SiteDataName2"}
+									],
+									"fpdConfigSiteExt":123,
+									"globalFPDSiteData":"globalFPDSiteDataValue",
+									"id":"fpdConfigSiteId"
+									},
+								"bidRequestSiteExt":1234
+							}`,
+			siteContentNil: true,
+		},
+	}
+
+	for _, test := range testCases {
+		if test.siteContentNil {
+			bidRequestSite.Content = nil
+			expectedSite.Content = &openrtb2.Content{Data: []openrtb2.Data{
+				{ID: "openRtbGlobalFPDSiteContentDataId1", Name: "openRtbGlobalFPDSiteContentDataName1"},
+				{ID: "openRtbGlobalFPDSiteContentDataId2", Name: "openRtbGlobalFPDSiteContentDataName2"},
+			}}
+		}
+
+		bidRequestSite.Ext = test.bidRequestSiteExt
+
+		fpdConfigSite := make(map[string]json.RawMessage, 0)
+		fpdConfigSite["id"] = []byte(`"fpdConfigSiteId"`)
+		fpdConfigSite[keywordsKey] = []byte(`"fpdConfigSiteKeywords"`)
+		fpdConfigSite["data"] = []byte(`[{"id":"SiteDataId1", "name":"SiteDataName1"}, {"id":"SiteDataId2", "name":"SiteDataName2"}]`)
+		fpdConfigSite["ext"] = []byte(`{"data":{"fpdConfigSiteExt": 123}}`)
+		fpdConfig := &openrtb_ext.ORTB2{Site: fpdConfigSite}
+
+		resultSite, err := resolveSite(fpdConfig, bidRequestSite, globalFPD, openRtbGlobalFPD, "appnexus")
+		assert.NoError(t, err, "No error should be returned")
+
+		assert.JSONEq(t, test.expectedSiteExt, string(resultSite.Ext), "Result site.Ext is incorrect")
+		resultSite.Ext = nil
+		assert.Equal(t, expectedSite, resultSite, "Result site is incorrect")
+	}
+
+}
+
+func TestResolveSiteNilValues(t *testing.T) {
+	resultSite, err := resolveSite(nil, nil, nil, nil, "appnexus")
+	assert.NoError(t, err, "No error should be returned")
+	assert.Nil(t, resultSite, "Result site should be nil")
+}
+
+func TestResolveSiteBadInput(t *testing.T) {
+	fpdConfigSite := make(map[string]json.RawMessage, 0)
+	fpdConfigSite["id"] = []byte(`"fpdConfigSiteId"`)
+	fpdConfig := &openrtb_ext.ORTB2{Site: fpdConfigSite}
+
+	resultSite, err := resolveSite(fpdConfig, nil, nil, nil, "appnexus")
+	assert.Error(t, err, "Error should be returned")
+	assert.Equal(t, "incorrect First Party Data for bidder appnexus: Site object is not defined in request, but defined in FPD config", err.Error(), "Incorrect error message")
+	assert.Nil(t, resultSite, "Result site should be nil")
+}
+
+func TestMergeSites(t *testing.T) {
+
+	originalSite := &openrtb2.Site{
+		ID:         "bidRequestSiteId",
+		Keywords:   "bidRequestSiteKeywords",
+		Page:       "bidRequestSitePage",
+		Name:       "bidRequestSiteName",
+		Domain:     "bidRequestSiteDomain",
+		Cat:        []string{"books1", "magazines1"},
+		SectionCat: []string{"books2", "magazines2"},
+		PageCat:    []string{"books3", "magazines3"},
+		Search:     "bidRequestSiteSearch",
+		Ref:        "bidRequestSiteRef",
+		Content: &openrtb2.Content{
+			Title: "bidRequestSiteContentTitle",
+			Data: []openrtb2.Data{
+				{ID: "openRtbGlobalFPDSiteDataId1", Name: "openRtbGlobalFPDSiteDataName1"},
+				{ID: "openRtbGlobalFPDSiteDataId2", Name: "openRtbGlobalFPDSiteDataName2"},
+			},
+		},
+		Ext: []byte(`{"bidRequestSiteExt": 1234}`),
+	}
+	fpdConfigSite := make(map[string]json.RawMessage, 0)
+	fpdConfigSite["id"] = []byte(`"fpdConfigSiteId"`)
+	fpdConfigSite[keywordsKey] = []byte(`"fpdConfigSiteKeywords"`)
+	fpdConfigSite[pageKey] = []byte(`"fpdConfigSitePage"`)
+	fpdConfigSite[nameKey] = []byte(`"fpdConfigSiteName"`)
+	fpdConfigSite[domainKey] = []byte(`"fpdConfigSiteDomain"`)
+	fpdConfigSite[catKey] = []byte(`["cars1", "auto1"]`)
+	fpdConfigSite[sectionCatKey] = []byte(`["cars2", "auto2"]`)
+	fpdConfigSite[pageCatKey] = []byte(`["cars3", "auto3"]`)
+	fpdConfigSite[searchKey] = []byte(`"fpdConfigSiteSearch"`)
+	fpdConfigSite[refKey] = []byte(`"fpdConfigSiteRef"`)
+	fpdConfigSite["data"] = []byte(`[{"id":"SiteDataId1", "name":"SiteDataName1"}, {"id":"SiteDataId2", "name":"SiteDataName2"}]`)
+	fpdConfigSite["ext"] = []byte(`{"data":{"fpdConfigSiteExt": 123}}`)
+
+	resultSite, err := mergeSites(originalSite, fpdConfigSite, "appnexus")
+	assert.NoError(t, err, "No error should be returned")
+
+	expectedSiteExt := `{"bidRequestSiteExt":1234,
+						 "data":{
+							"data":[
+								{"id":"SiteDataId1","name":"SiteDataName1"},
+								{"id":"SiteDataId2","name":"SiteDataName2"}],
+							"fpdConfigSiteExt":123,
+							"id":"fpdConfigSiteId"}
+						 }`
+	assert.JSONEq(t, expectedSiteExt, string(resultSite.Ext), "Result user.Ext is incorrect")
+	resultSite.Ext = nil
+
+	expectedSite := openrtb2.Site{
+		ID:         "bidRequestSiteId",
+		Keywords:   "fpdConfigSiteKeywords",
+		Page:       "fpdConfigSitePage",
+		Name:       "fpdConfigSiteName",
+		Domain:     "fpdConfigSiteDomain",
+		Cat:        []string{"cars1", "auto1"},
+		SectionCat: []string{"cars2", "auto2"},
+		PageCat:    []string{"cars3", "auto3"},
+		Search:     "fpdConfigSiteSearch",
+		Ref:        "fpdConfigSiteRef",
+		Content: &openrtb2.Content{
+			Title: "bidRequestSiteContentTitle",
+			Data: []openrtb2.Data{
+				{ID: "openRtbGlobalFPDSiteDataId1", Name: "openRtbGlobalFPDSiteDataName1"},
+				{ID: "openRtbGlobalFPDSiteDataId2", Name: "openRtbGlobalFPDSiteDataName2"},
+			},
+		},
+		Ext: nil,
+	}
+	assert.Equal(t, expectedSite, resultSite, "Result user is incorrect")
+}
+
+func TestResolveApp(t *testing.T) {
+
+	fpdConfigApp := make(map[string]json.RawMessage, 0)
+	fpdConfigApp["id"] = []byte(`"fpdConfigAppId"`)
+	fpdConfigApp[keywordsKey] = []byte(`"fpdConfigAppKeywords"`)
+	fpdConfigApp[nameKey] = []byte(`"fpdConfigAppName"`)
+	fpdConfigApp[bundleKey] = []byte(`"fpdConfigAppBundle"`)
+	fpdConfigApp["data"] = []byte(`[{"id":"AppDataId1", "name":"AppDataName1"}, {"id":"AppDataId2", "name":"AppDataName2"}]`)
+	fpdConfigApp["ext"] = []byte(`{"data":{"fpdConfigAppExt": 123}}`)
+
+	bidRequestApp := &openrtb2.App{
+		ID:       "bidRequestAppId",
+		Keywords: "bidRequestAppKeywords",
+		Name:     "bidRequestAppName",
+		Bundle:   "bidRequestAppBundle",
+		Content: &openrtb2.Content{
+			ID:      "bidRequestAppContentId",
+			Episode: 4,
+			Data: []openrtb2.Data{
+				{ID: "bidRequestAppContentDataId1", Name: "bidRequestAppContentDataName1"},
+				{ID: "bidRequestAppContentDataId2", Name: "bidRequestAppContentDataName2"},
+			},
+		},
+	}
+
+	globalFPD := make(map[string][]byte, 0)
+	globalFPD[appKey] = []byte(`{"globalFPDAppData": "globalFPDAppDataValue"}`)
+
+	openRtbGlobalFPD := make(map[string][]openrtb2.Data, 0)
+	openRtbGlobalFPD[appContentDataKey] = []openrtb2.Data{
+		{ID: "openRtbGlobalFPDAppContentDataId1", Name: "openRtbGlobalFPDAppContentDataName1"},
+		{ID: "openRtbGlobalFPDAppContentDataId2", Name: "openRtbGlobalFPDAppContentDataName2"},
+	}
+
+	expectedApp := &openrtb2.App{
+		ID:       "bidRequestAppId",
+		Keywords: "fpdConfigAppKeywords",
+		Name:     "bidRequestAppName",
+		Bundle:   "bidRequestAppBundle",
+		Content: &openrtb2.Content{
+			ID:      "bidRequestAppContentId",
+			Episode: 4,
+			Data: []openrtb2.Data{
+				{ID: "openRtbGlobalFPDAppContentDataId1", Name: "openRtbGlobalFPDAppContentDataName1"},
+				{ID: "openRtbGlobalFPDAppContentDataId2", Name: "openRtbGlobalFPDAppContentDataName2"},
+			},
+		},
+	}
+
+	testCases := []struct {
+		description      string
+		bidRequestAppExt []byte
+		expectedAppExt   string
+		appContentNil    bool
+	}{
+		{
+			description:      "bid request app.ext is nil",
+			bidRequestAppExt: nil,
+			expectedAppExt: `{"data":{
+									"data":[
+										{"id":"AppDataId1","name":"AppDataName1"},
+										{"id":"AppDataId2","name":"AppDataName2"}
+									],
+									"fpdConfigAppExt":123,
+									"globalFPDAppData":"globalFPDAppDataValue",
+									"id":"fpdConfigAppId"
+									}
+							}`,
+			appContentNil: false,
+		},
+		{
+			description:      "bid request app.ext is not nil",
+			bidRequestAppExt: []byte(`{"bidRequestAppExt": 1234}`),
+			expectedAppExt: `{"data":{
+									"data":[
+										{"id":"AppDataId1","name":"AppDataName1"},
+										{"id":"AppDataId2","name":"AppDataName2"}
+									],
+									"fpdConfigAppExt":123,
+									"globalFPDAppData":"globalFPDAppDataValue",
+									"id":"fpdConfigAppId"
+									},
+								"bidRequestAppExt":1234
+							}`,
+			appContentNil: false,
+		},
+		{
+			description:      "bid request app.content.data is nil ",
+			bidRequestAppExt: []byte(`{"bidRequestAppExt": 1234}`),
+			expectedAppExt: `{"data":{
+									"data":[
+										{"id":"AppDataId1","name":"AppDataName1"},
+										{"id":"AppDataId2","name":"AppDataName2"}
+									],
+									"fpdConfigAppExt":123,
+									"globalFPDAppData":"globalFPDAppDataValue",
+									"id":"fpdConfigAppId"
+									},
+								"bidRequestAppExt":1234
+							}`,
+			appContentNil: true,
+		},
+	}
+
+	for _, test := range testCases {
+		if test.appContentNil {
+			bidRequestApp.Content = nil
+			expectedApp.Content = &openrtb2.Content{Data: []openrtb2.Data{
+				{ID: "openRtbGlobalFPDAppContentDataId1", Name: "openRtbGlobalFPDAppContentDataName1"},
+				{ID: "openRtbGlobalFPDAppContentDataId2", Name: "openRtbGlobalFPDAppContentDataName2"},
+			}}
+		}
+
+		bidRequestApp.Ext = test.bidRequestAppExt
+
+		fpdConfigApp := make(map[string]json.RawMessage, 0)
+		fpdConfigApp["id"] = []byte(`"fpdConfigAppId"`)
+		fpdConfigApp[keywordsKey] = []byte(`"fpdConfigAppKeywords"`)
+		fpdConfigApp["data"] = []byte(`[{"id":"AppDataId1", "name":"AppDataName1"}, {"id":"AppDataId2", "name":"AppDataName2"}]`)
+		fpdConfigApp["ext"] = []byte(`{"data":{"fpdConfigAppExt": 123}}`)
+		fpdConfig := &openrtb_ext.ORTB2{App: fpdConfigApp}
+
+		resultApp, err := resolveApp(fpdConfig, bidRequestApp, globalFPD, openRtbGlobalFPD, "appnexus")
+		assert.NoError(t, err, "No error should be returned")
+
+		assert.JSONEq(t, test.expectedAppExt, string(resultApp.Ext), "Result app.Ext is incorrect")
+		resultApp.Ext = nil
+		assert.Equal(t, expectedApp, resultApp, "Result app is incorrect")
+	}
+
+}
+
+func TestResolveAppNilValues(t *testing.T) {
+	resultApp, err := resolveApp(nil, nil, nil, nil, "appnexus")
+	assert.NoError(t, err, "No error should be returned")
+	assert.Nil(t, resultApp, "Result app should be nil")
+}
+
+func TestResolveAppBadInput(t *testing.T) {
+	fpdConfigApp := make(map[string]json.RawMessage, 0)
+	fpdConfigApp["id"] = []byte(`"fpdConfigAppId"`)
+	fpdConfig := &openrtb_ext.ORTB2{App: fpdConfigApp}
+
+	resultApp, err := resolveApp(fpdConfig, nil, nil, nil, "appnexus")
+	assert.Error(t, err, "Error should be returned")
+	assert.Equal(t, "incorrect First Party Data for bidder appnexus: App object is not defined in request, but defined in FPD config", err.Error(), "Incorrect error message")
+	assert.Nil(t, resultApp, "Result app should be nil")
+}
+
+func TestMergeApps(t *testing.T) {
+
+	originalApp := &openrtb2.App{
+		ID:         "bidRequestAppId",
+		Keywords:   "bidRequestAppKeywords",
+		Name:       "bidRequestAppName",
+		Domain:     "bidRequestAppDomain",
+		Bundle:     "bidRequestAppBundle",
+		StoreURL:   "bidRequestAppStoreUrl",
+		Ver:        "bidRequestAppVer",
+		Cat:        []string{"books1", "magazines1"},
+		SectionCat: []string{"books2", "magazines2"},
+		PageCat:    []string{"books3", "magazines3"},
+		Content: &openrtb2.Content{
+			Title: "bidRequestAppContentTitle",
+			Data: []openrtb2.Data{
+				{ID: "openRtbGlobalFPDAppDataId1", Name: "openRtbGlobalFPDAppDataName1"},
+				{ID: "openRtbGlobalFPDAppDataId2", Name: "openRtbGlobalFPDAppDataName2"},
+			},
+		},
+		Ext: []byte(`{"bidRequestAppExt": 1234}`),
+	}
+	fpdConfigApp := make(map[string]json.RawMessage, 0)
+	fpdConfigApp["id"] = []byte(`"fpdConfigAppId"`)
+	fpdConfigApp[keywordsKey] = []byte(`"fpdConfigAppKeywords"`)
+	fpdConfigApp[nameKey] = []byte(`"fpdConfigAppName"`)
+	fpdConfigApp[domainKey] = []byte(`"fpdConfigAppDomain"`)
+	fpdConfigApp[bundleKey] = []byte(`"fpdConfigAppBundle"`)
+	fpdConfigApp[storeUrlKey] = []byte(`"fpdConfigAppStoreUrl"`)
+	fpdConfigApp[verKey] = []byte(`"fpdConfigAppVer"`)
+	fpdConfigApp[catKey] = []byte(`["cars1", "auto1"]`)
+	fpdConfigApp[sectionCatKey] = []byte(`["cars2", "auto2"]`)
+	fpdConfigApp[pageCatKey] = []byte(`["cars3", "auto3"]`)
+	fpdConfigApp["data"] = []byte(`[{"id":"AppDataId1", "name":"AppDataName1"}, {"id":"AppDataId2", "name":"AppDataName2"}]`)
+	fpdConfigApp["ext"] = []byte(`{"data":{"fpdConfigAppExt": 123}}`)
+
+	resultApp, err := mergeApps(originalApp, fpdConfigApp)
+	assert.NoError(t, err, "No error should be returned")
+
+	expectedAppExt := `{"bidRequestAppExt":1234,
+						 "data":{
+							"data":[
+								{"id":"AppDataId1","name":"AppDataName1"},
+								{"id":"AppDataId2","name":"AppDataName2"}],
+							"fpdConfigAppExt":123,
+							"id":"fpdConfigAppId"}
+						 }`
+	assert.JSONEq(t, expectedAppExt, string(resultApp.Ext), "Result user.Ext is incorrect")
+	resultApp.Ext = nil
+
+	expectedApp := openrtb2.App{
+		ID:         "bidRequestAppId",
+		Keywords:   "fpdConfigAppKeywords",
+		Name:       "fpdConfigAppName",
+		Domain:     "fpdConfigAppDomain",
+		Bundle:     "fpdConfigAppBundle",
+		Ver:        "fpdConfigAppVer",
+		StoreURL:   "fpdConfigAppStoreUrl",
+		Cat:        []string{"cars1", "auto1"},
+		SectionCat: []string{"cars2", "auto2"},
+		PageCat:    []string{"cars3", "auto3"},
+		Content: &openrtb2.Content{
+			Title: "bidRequestAppContentTitle",
+			Data: []openrtb2.Data{
+				{ID: "openRtbGlobalFPDAppDataId1", Name: "openRtbGlobalFPDAppDataName1"},
+				{ID: "openRtbGlobalFPDAppDataId2", Name: "openRtbGlobalFPDAppDataName2"},
+			},
+		},
+		Ext: nil,
+	}
+	assert.Equal(t, expectedApp, resultApp, "Result user is incorrect")
+}
+
+func TestBuildExtData(t *testing.T) {
+	testCases := []struct {
+		description string
+		input       []byte
+		expectedRes string
+	}{
+		{
+			description: "Input object with int value",
+			input:       []byte(`{"someData": 123}`),
+			expectedRes: `{"data": {"someData": 123}}`,
+		},
+		{
+			description: "Input object with bool value",
+			input:       []byte(`{"someData": true}`),
+			expectedRes: `{"data": {"someData": true}}`,
+		},
+		{
+			description: "Input object with string value",
+			input:       []byte(`{"someData": "true"}`),
+			expectedRes: `{"data": {"someData": "true"}}`,
+		},
+		{
+			description: "No input object",
+			input:       []byte(`{}`),
+			expectedRes: `{"data": {}}`,
+		},
+		{
+			description: "Input object with object value",
+			input:       []byte(`{"someData": {"moreFpdData": "fpddata"}}`),
+			expectedRes: `{"data": {"someData": {"moreFpdData": "fpddata"}}}`,
+		},
+	}
+
+	for _, test := range testCases {
+		actualRes := buildExtData(test.input)
+		assert.JSONEq(t, test.expectedRes, string(actualRes), "Incorrect result data")
+	}
+}
diff --git a/firstpartydata/tests/extractbidderconfigfpd/bidder-config-config-null.json b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-config-null.json
new file mode 100644
index 00000000..6d2d54f6
--- /dev/null
+++ b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-config-null.json
@@ -0,0 +1,20 @@
+{
+  "description": "Bidder config for bidder is null - expected bidder fpd with no data",
+  "inputRequestData": {
+    "data": {},
+    "bidderconfig": [
+      {
+        "bidders": [
+          "appnexus"
+        ],
+        "config": null
+      }
+    ]
+  },
+  "outputRequestData": {
+  },
+  "bidderConfigFPD": {
+    "appnexus": {}
+  }
+}
+
diff --git a/firstpartydata/tests/extractbidderconfigfpd/bidder-config-config-ortb-null.json b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-config-ortb-null.json
new file mode 100644
index 00000000..3bafae6d
--- /dev/null
+++ b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-config-ortb-null.json
@@ -0,0 +1,22 @@
+{
+  "description": "Bidder config.ortb2 for bidder is null - expected bidder fpd with no data",
+  "inputRequestData": {
+    "data": {},
+    "bidderconfig": [
+      {
+        "bidders": [
+          "appnexus"
+        ],
+        "config": {
+          "ortb2": null
+        }
+      }
+    ]
+  },
+  "outputRequestData": {
+  },
+  "bidderConfigFPD": {
+    "appnexus": {
+    }
+  }
+}
diff --git a/firstpartydata/tests/extractbidderconfigfpd/bidder-config-duplicated.json b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-duplicated.json
new file mode 100644
index 00000000..356b84aa
--- /dev/null
+++ b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-duplicated.json
@@ -0,0 +1,63 @@
+{
+  "description": "Verifies error presence in case more than one bidder config specified for the same bidder",
+  "inputRequestData": {
+    "data": {},
+    "bidderconfig": [
+      {
+        "bidders": [
+          "appnexus"
+        ],
+        "config": {
+          "ortb2": {
+            "site": {
+              "id": "apnSiteId",
+              "ext": {
+                "data": {
+                  "sitefpddata": "sitefpddata",
+                  "moreFpd": {
+                    "fpd": 123
+                  },
+                  "morefpdData": "morefpddata"
+                }
+              }
+            }
+          }
+        }
+      },
+      {
+        "bidders": [
+          "appnexus",
+          "telaria",
+          "testBidder2"
+        ],
+        "config": {
+          "ortb2": {
+            "user": {
+              "id": "telariaUserData",
+              "ext": {
+                "data": {
+                  "userdata": "fpduserdata"
+                }
+              }
+            },
+            "app": {
+              "id": "telariaAppData",
+              "ext": {
+                "data": {
+                  "appdata": "fpdappdata"
+                }
+              }
+            }
+          }
+        }
+      }
+    ]
+  },
+  "outputRequestData": {},
+  "bidderConfigFPD": {},
+  "validationErrors": [
+    {"Message": "multiple First Party Data bidder configs provided for bidder: appnexus"}
+  ]
+}
+
+
diff --git a/firstpartydata/tests/extractbidderconfigfpd/bidder-config-empty.json b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-empty.json
new file mode 100644
index 00000000..d127e5df
--- /dev/null
+++ b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-empty.json
@@ -0,0 +1,11 @@
+{
+  "description": "Bidder config is empty - no bidder fpd expected for any bidders",
+  "inputRequestData": {
+    "data": {},
+    "bidderconfig": []
+  },
+  "outputRequestData": {},
+  "bidderConfigFPD": {}
+}
+
+
diff --git a/firstpartydata/tests/extractbidderconfigfpd/bidder-config-multiple-bidder.json b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-multiple-bidder.json
new file mode 100644
index 00000000..48ea27a7
--- /dev/null
+++ b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-multiple-bidder.json
@@ -0,0 +1,109 @@
+{
+  "description": "Extracts bidder configs for multiple bidders, one config has two bidders; expect to have bidder FPD configs for all bidders",
+  "inputRequestData": {
+    "data": {},
+    "bidderconfig": [
+      {
+        "bidders": [
+          "appnexus"
+        ],
+        "config": {
+          "ortb2": {
+            "site": {
+              "id": "apnSiteId",
+              "ext": {
+                "data": {
+                  "sitefpddata": "sitefpddata",
+                  "moreFpd": {
+                    "fpd": 123
+                  },
+                  "morefpdData": "morefpddata"
+                }
+              }
+            }
+          }
+        }
+      },
+      {
+        "bidders": [
+          "telaria",
+          "testBidder2"
+        ],
+        "config": {
+          "ortb2": {
+            "user": {
+              "id": "telariaUserData",
+              "ext": {
+                "data": {
+                  "userdata": "fpduserdata"
+                }
+              }
+            },
+            "app": {
+              "id": "telariaAppData",
+              "ext": {
+                "data": {
+                  "appdata": "fpdappdata"
+                }
+              }
+            }
+          }
+        }
+      }
+    ]
+  },
+  "outputRequestData": {
+  },
+  "bidderConfigFPD": {
+    "appnexus": {
+      "site": {
+        "id": "apnSiteId",
+        "ext": {
+          "data": {
+            "sitefpddata": "sitefpddata",
+            "moreFpd": {
+              "fpd": 123
+            },
+            "morefpdData": "morefpddata"
+          }
+        }
+      }
+    },
+    "telaria": {
+      "user": {
+        "id": "telariaUserData",
+        "ext": {
+          "data": {
+            "userdata": "fpduserdata"
+          }
+        }
+      },
+      "app": {
+        "id": "telariaAppData",
+        "ext": {
+          "data": {
+            "appdata": "fpdappdata"
+          }
+        }
+      }
+    },
+    "testBidder2": {
+      "user": {
+        "id": "telariaUserData",
+        "ext": {
+          "data": {
+            "userdata": "fpduserdata"
+          }
+        }
+      },
+      "app": {
+        "id": "telariaAppData",
+        "ext": {
+          "data": {
+            "appdata": "fpdappdata"
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/firstpartydata/tests/extractbidderconfigfpd/bidder-config-not-specified.json b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-not-specified.json
new file mode 100644
index 00000000..d3dfc5b0
--- /dev/null
+++ b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-not-specified.json
@@ -0,0 +1,7 @@
+{
+  "description": "Bidder config not specified",
+  "inputRequestData": {},
+  "outputRequestData": {},
+  "bidderConfigFPD": {}
+}
+
diff --git a/firstpartydata/tests/extractbidderconfigfpd/bidder-config-null.json b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-null.json
new file mode 100644
index 00000000..1063f7ec
--- /dev/null
+++ b/firstpartydata/tests/extractbidderconfigfpd/bidder-config-null.json
@@ -0,0 +1,11 @@
+{
+  "description": "Bidder config is null - no bidder fpd expected",
+  "inputRequestData": {
+    "data": {},
+    "bidderconfig": null
+  },
+  "outputRequestData": {},
+  "bidderConfigFPD": {}
+}
+
+
diff --git a/firstpartydata/tests/extractbidderconfigfpd/bidder-list-specified-only.json b/firstpartydata/tests/extractbidderconfigfpd/bidder-list-specified-only.json
new file mode 100644
index 00000000..6c1938bc
--- /dev/null
+++ b/firstpartydata/tests/extractbidderconfigfpd/bidder-list-specified-only.json
@@ -0,0 +1,15 @@
+{
+  "description": "Only global bidder list specified - no bidder fpds expected",
+  "inputRequestData": {
+    "data": {
+      "bidders": [
+        "appnexus",
+        "telaria",
+        "testBidder"
+      ]
+    }
+  },
+  "outputRequestData": {},
+  "bidderConfigFPD": {}
+}
+
diff --git a/firstpartydata/tests/extractfpdforbidders/global-fpd-defined-app-content-data.json b/firstpartydata/tests/extractfpdforbidders/global-fpd-defined-app-content-data.json
new file mode 100644
index 00000000..797846ee
--- /dev/null
+++ b/firstpartydata/tests/extractfpdforbidders/global-fpd-defined-app-content-data.json
@@ -0,0 +1,87 @@
+{
+  "description": "req.app.content.data defined; req.ext.prebid.data.bidders contains appnexus; expect req.app.content to be in the resolved FPD for appnexus but extracted from the original request",
+  "inputRequestData": {
+    "id": "bid_id",
+    "app": {
+      "id": "reqAppId",
+      "domain": "http://www.foobar.com/1234.html",
+      "name": "reqAppName",
+      "cat": [
+        "electronics",
+        "phone"
+      ],
+      "content": {
+        "len": 900,
+        "title": "reqContentTitle",
+        "season": "reqContentSeason",
+        "data": [
+          {
+            "id": "reqContentDataId1",
+            "name": "reqContentDataName1"
+          },
+          {
+            "id": "reqContentDataId2",
+            "name": "reqContentDataName2"
+          }
+        ]
+      }
+    },
+    "test": 1,
+    "ext": {
+      "prebid": {
+        "data": {
+          "bidders": [
+            "appnexus"
+          ]
+        }
+      }
+    }
+  },
+  "outputRequestData": {
+    "id": "bid_id",
+    "app": {
+      "id": "reqAppId",
+      "domain": "http://www.foobar.com/1234.html",
+      "name": "reqAppName",
+      "cat": [
+        "electronics",
+        "phone"
+      ],
+      "content": {
+        "len": 900,
+        "title": "reqContentTitle",
+        "season": "reqContentSeason"
+      }
+    },
+    "test": 1
+  },
+  "biddersFPDResolved": {
+    "appnexus": {
+      "app": {
+        "id": "reqAppId",
+        "domain": "http://www.foobar.com/1234.html",
+        "name": "reqAppName",
+        "cat": [
+          "electronics",
+          "phone"
+        ],
+        "content": {
+          "len": 900,
+          "title": "reqContentTitle",
+          "season": "reqContentSeason",
+          "data": [
+            {
+              "id": "reqContentDataId1",
+              "name": "reqContentDataName1"
+            },
+            {
+              "id": "reqContentDataId2",
+              "name": "reqContentDataName2"
+            }
+          ]
+        }
+      }
+    }
+  },
+  "validationErrors": []
+}
diff --git a/firstpartydata/tests/extractfpdforbidders/global-fpd-defined-app.json b/firstpartydata/tests/extractfpdforbidders/global-fpd-defined-app.json
new file mode 100644
index 00000000..4f312b3d
--- /dev/null
+++ b/firstpartydata/tests/extractfpdforbidders/global-fpd-defined-app.json
@@ -0,0 +1,80 @@
+{
+  "description": "req.app and req.ext.prebid.data.bidders contains appnexus; expect bidder config with app data to be included in the resolved FPD for appnexus",
+  "inputRequestData": {
+    "id": "bid_id",
+    "app": {
+      "id": "reqAppId",
+      "publisher": {
+        "id": "1"
+      }
+    },
+    "test": 1,
+    "ext": {
+      "prebid": {
+        "data": {
+          "bidders": [
+            "appnexus"
+          ]
+        },
+        "bidderconfig": [
+          {
+            "bidders": [
+              "appnexus"
+            ],
+            "config": {
+              "ortb2": {
+                "app": {
+                  "id": "apnAppId",
+                  "name": "fpdAppName",
+                  "bundle": "fpdAppBundle",
+                  "domain": "fpdAppDomain",
+                  "storeurl": "fpdAppstoreUrl",
+                  "cat": ["books"],
+                  "sectioncat": ["books"],
+                  "pagecat": ["magazines"],
+                  "keywords": "apnKeywords",
+                  "ver": "1.2"
+                }
+              }
+            }
+          }
+        ]
+      }
+    }
+  },
+  "outputRequestData": {
+    "id": "bid_id",
+    "app": {
+      "id": "reqAppId",
+      "publisher": {
+        "id": "1"
+      }
+    },
+    "test": 1
+  },
+  "biddersFPDResolved": {
+    "appnexus": {
+      "app": {
+        "id": "reqAppId",
+        "name": "fpdAppName",
+        "bundle": "fpdAppBundle",
+        "domain": "fpdAppDomain",
+        "storeurl": "fpdAppstoreUrl",
+        "cat": ["books"],
+        "sectioncat": ["books"],
+        "pagecat": ["magazines"],
+        "keywords": "apnKeywords",
+        "ver": "1.2",
+        "publisher": {
+          "id": "1"
+        },
+        "ext": {
+          "data": {
+            "id": "apnAppId"
+          }
+        }
+      }
+    }
+  },
+  "validationErrors": []
+}
diff --git a/firstpartydata/tests/extractfpdforbidders/global-fpd-defined-data-user.json b/firstpartydata/tests/extractfpdforbidders/global-fpd-defined-data-user.json
new file mode 100644
index 00000000..368be1e5
--- /dev/null
+++ b/firstpartydata/tests/extractfpdforbidders/global-fpd-defined-data-user.json
@@ -0,0 +1,63 @@
+{
+  "description": "req.user.data defined; req.ext.prebid.data.bidders contains appnexus; expect req.user.data to be included in the appnexus resolved FPD but extracted from the original request",
+  "inputRequestData": {
+    "id": "bid_id",
+    "user": {
+      "id": "reqUserID",
+      "keywords": "userKeyword",
+      "yob": 1982,
+      "gender": "M",
+      "data": [
+        {
+          "id": "userDataId1",
+          "name": "userDataName1"
+        },
+        {
+          "id": "userDataId2",
+          "name": "userDataName2"
+        }
+      ]
+    },
+    "test": 1,
+    "ext": {
+      "prebid": {
+        "data": {
+          "bidders": [
+            "appnexus"
+          ]
+        }
+      }
+    }
+  },
+  "outputRequestData": {
+    "id": "bid_id",
+    "user": {
+      "id": "reqUserID",
+      "keywords": "userKeyword",
+      "yob": 1982,
+      "gender": "M"
+    },
+    "test": 1
+  },
+  "biddersFPDResolved": {
+    "appnexus": {
+      "user": {
+        "id": "reqUserID",
+        "keywords": "userKeyword",
+        "yob": 1982,
+        "gender": "M",
+        "data": [
+          {
+            "id": "userDataId1",
+            "name": "userDataName1"
+          },
+          {
+            "id": "userDataId2",
+            "name": "userDataName2"
+          }
+        ]
+      }
+    }
+  },
+  "validationErrors": []
+}
diff --git a/firstpartydata/tests/extractfpdforbidders/global-fpd-defined-site-content-data.json b/firstpartydata/tests/extractfpdforbidders/global-fpd-defined-site-content-data.json
new file mode 100644
index 00000000..ea20afb5
--- /dev/null
+++ b/firstpartydata/tests/extractfpdforbidders/global-fpd-defined-site-content-data.json
@@ -0,0 +1,96 @@
+{
+  "description": "req.site.content.data defined; req.ext.prebid.data.bidders contains appnexus; expect req.site.content.data to be included in the appnexus resolved FPD but extracted from the original request",
+  "inputRequestData": {
+    "id": "bid_id",
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "name": "reqSiteName",
+      "cat": [
+        "electronics",
+        "phone"
+      ],
+      "publisher": {
+        "id": "1"
+      },
+      "content": {
+        "len": 900,
+        "title": "reqContentTitle",
+        "season": "reqContentSeason",
+        "data": [
+          {
+            "id": "reqContentDataId1",
+            "name": "reqContentDataName1"
+          },
+          {
+            "id": "reqContentDataId2",
+            "name": "reqContentDataName2"
+          }
+        ]
+      }
+    },
+    "test": 1,
+    "ext": {
+      "prebid": {
+        "data": {
+          "bidders": [
+            "appnexus"
+          ]
+        }
+      }
+    }
+  },
+  "outputRequestData": {
+    "id": "bid_id",
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "name": "reqSiteName",
+      "cat": [
+        "electronics",
+        "phone"
+      ],
+      "publisher": {
+        "id": "1"
+      },
+      "content": {
+        "len": 900,
+        "title": "reqContentTitle",
+        "season": "reqContentSeason"
+      }
+    },
+    "test": 1
+  },
+  "biddersFPDResolved": {
+    "appnexus": {
+      "site": {
+        "id": "reqSiteId",
+        "page": "http://www.foobar.com/1234.html",
+        "name": "reqSiteName",
+        "cat": [
+          "electronics",
+          "phone"
+        ],
+        "publisher": {
+          "id": "1"
+        },
+        "content": {
+          "len": 900,
+          "title": "reqContentTitle",
+          "season": "reqContentSeason",
+          "data": [
+            {
+              "id": "reqContentDataId1",
+              "name": "reqContentDataName1"
+            },
+            {
+              "id": "reqContentDataId2",
+              "name": "reqContentDataName2"
+            }
+          ]
+        }
+      }
+    }
+  },
+  "validationErrors": []
+}
diff --git a/firstpartydata/tests/extractfpdforbidders/global-fpd-defined-site.json b/firstpartydata/tests/extractfpdforbidders/global-fpd-defined-site.json
new file mode 100644
index 00000000..5fc7c0d7
--- /dev/null
+++ b/firstpartydata/tests/extractfpdforbidders/global-fpd-defined-site.json
@@ -0,0 +1,74 @@
+{
+  "description": "req.site defined; req.ext.prebid.data.bidders contains appnexus and contains site configuration; expect bidder config with site data to be included in the resolved FPD for appnexus and to have been extracted from the original request",
+  "inputRequestData": {
+    "id": "bid_id",
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "publisher": {
+        "id": "1"
+      }
+    },
+    "test": 1,
+    "ext": {
+      "prebid": {
+        "data": {
+          "bidders": [
+            "appnexus"
+          ]
+        },
+        "bidderconfig": [
+          {
+            "bidders": [
+              "appnexus"
+            ],
+            "config": {
+              "ortb2": {
+                "site": {
+                  "id": "apnSiteId",
+                  "keywords": "apnKeywords",
+                  "sectioncat": ["books"],
+                  "search": "books",
+                  "pagecat": ["magazines"],
+                  "page": "http://www.foobar.com/testurl.html"
+                }
+              }
+            }
+          }
+        ]
+      }
+    }
+  },
+  "outputRequestData": {
+    "id": "bid_id",
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "publisher": {
+        "id": "1"
+      }
+    },
+    "test": 1
+  },
+  "biddersFPDResolved": {
+    "appnexus": {
+      "site": {
+        "id": "reqSiteId",
+        "keywords": "apnKeywords",
+        "sectioncat": ["books"],
+        "pagecat": ["magazines"],
+        "search": "books",
+        "page": "http://www.foobar.com/testurl.html",
+        "publisher": {
+          "id": "1"
+        },
+        "ext": {
+          "data": {
+            "id": "apnSiteId"
+          }
+        }
+      }
+    }
+  },
+  "validationErrors": []
+}
diff --git a/firstpartydata/tests/extractfpdforbidders/global-fpd-empty-app-content-data.json b/firstpartydata/tests/extractfpdforbidders/global-fpd-empty-app-content-data.json
new file mode 100644
index 00000000..57d79210
--- /dev/null
+++ b/firstpartydata/tests/extractfpdforbidders/global-fpd-empty-app-content-data.json
@@ -0,0 +1,58 @@
+{
+  "description": "req.app.content.data defined; req.ext.prebid.data.bidders is empty; expect req.app.content.data to not be included in any of the resolved bidder FPD and extracted from the original request",
+  "inputRequestData": {
+    "id": "bid_id",
+    "app": {
+      "id": "reqAppId",
+      "domain": "http://www.foobar.com/1234.html",
+      "name": "reqAppName",
+      "cat": [
+        "electronics",
+        "phone"
+      ],
+      "content": {
+        "len": 900,
+        "title": "reqContentTitle",
+        "season": "reqContentSeason",
+        "data": [
+          {
+            "id": "reqContentDataId1",
+            "name": "reqContentDataName1"
+          },
+          {
+            "id": "reqContentDataId2",
+            "name": "reqContentDataName2"
+          }
+        ]
+      }
+    },
+    "test": 1,
+    "ext": {
+      "prebid": {
+        "data": {
+          "bidders": []
+        }
+      }
+    }
+  },
+  "outputRequestData": {
+    "id": "bid_id",
+    "app": {
+      "id": "reqAppId",
+      "domain": "http://www.foobar.com/1234.html",
+      "name": "reqAppName",
+      "cat": [
+        "electronics",
+        "phone"
+      ],
+      "content": {
+        "len": 900,
+        "title": "reqContentTitle",
+        "season": "reqContentSeason"
+      }
+    },
+    "test": 1
+  },
+  "biddersFPDResolved": {},
+  "validationErrors": []
+}
diff --git a/firstpartydata/tests/extractfpdforbidders/global-fpd-empty-app.json b/firstpartydata/tests/extractfpdforbidders/global-fpd-empty-app.json
new file mode 100644
index 00000000..e37edf12
--- /dev/null
+++ b/firstpartydata/tests/extractfpdforbidders/global-fpd-empty-app.json
@@ -0,0 +1,43 @@
+{
+  "description": "req.app.ext.data is present but req.ext.prebid.data.bidders is empty; expect req.app.ext.data to not be included in any of the resolved bidder FPD and to have been extracted from the original request",
+    "inputRequestData": {
+    "id": "bid_id",
+    "app": {
+      "id": "reqAppId",
+      "keywords": "appKeywords",
+      "publisher": {
+        "id": "1"
+      },
+      "ext": {
+        "amp": 1,
+        "data": {
+          "someappfpd": "appfpdDataTest"
+        }
+      }
+    },
+    "test": 1,
+    "ext": {
+      "prebid": {
+        "data": {
+          "bidders": []
+        }
+      }
+    }
+  },
+  "outputRequestData":  {
+    "id": "bid_id",
+    "app": {
+      "id": "reqAppId",
+      "keywords": "appKeywords",
+      "publisher": {
+        "id": "1"
+      },
+      "ext": {
+        "amp": 1
+      }
+    },
+    "test": 1
+  },
+  "biddersFPDResolved": {},
+  "validationErrors": []
+}
diff --git a/firstpartydata/tests/extractfpdforbidders/global-fpd-empty-site-content-data.json b/firstpartydata/tests/extractfpdforbidders/global-fpd-empty-site-content-data.json
new file mode 100644
index 00000000..5ff8fb6b
--- /dev/null
+++ b/firstpartydata/tests/extractfpdforbidders/global-fpd-empty-site-content-data.json
@@ -0,0 +1,64 @@
+{
+  "description": "req.site.content.data defined; req.ext.prebid.data.bidders is empty; expect req.site.content.data not to be included in the resolved FPD for any bidders and to have been extracted from the original request",
+  "inputRequestData": {
+    "id": "bid_id",
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "name": "reqSiteName",
+      "cat": [
+        "electronics",
+        "phone"
+      ],
+      "publisher": {
+        "id": "1"
+      },
+      "content": {
+        "len": 900,
+        "title": "reqContentTitle",
+        "season": "reqContentSeason",
+        "data": [
+          {
+            "id": "reqContentDataId1",
+            "name": "reqContentDataName1"
+          },
+          {
+            "id": "reqContentDataId2",
+            "name": "reqContentDataName2"
+          }
+        ]
+      }
+    },
+    "test": 1,
+    "ext": {
+      "prebid": {
+        "data": {
+          "bidders": []
+        }
+      }
+    }
+  },
+  "outputRequestData": {
+    "id": "bid_id",
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "name": "reqSiteName",
+      "cat": [
+        "electronics",
+        "phone"
+      ],
+      "publisher": {
+        "id": "1"
+      },
+      "content": {
+        "len": 900,
+        "title": "reqContentTitle",
+        "season": "reqContentSeason"
+      }
+    },
+    "test": 1
+  },
+  "biddersFPDResolved": {},
+  "validationErrors": []
+}
diff --git a/firstpartydata/tests/extractfpdforbidders/global-fpd-empty-site.json b/firstpartydata/tests/extractfpdforbidders/global-fpd-empty-site.json
new file mode 100644
index 00000000..985edd34
--- /dev/null
+++ b/firstpartydata/tests/extractfpdforbidders/global-fpd-empty-site.json
@@ -0,0 +1,43 @@
+{
+  "description": "req.site.ext.data is present but req.ext.prebid.data.bidders is empty; expect req.site.ext.data to not be included in any of the resolved bidder FPD and to have been extracted from the original request",
+  "inputRequestData": {
+    "id": "bid_id",
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "publisher": {
+        "id": "1"
+      },
+      "ext": {
+        "amp": 1,
+        "data": {
+          "somesitefpd": "sitefpdDataTest"
+        }
+      }
+    },
+    "test": 1,
+    "ext": {
+      "prebid": {
+        "data": {
+          "bidders": []
+        }
+      }
+    }
+  },
+  "outputRequestData":  {
+    "id": "bid_id",
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "publisher": {
+        "id": "1"
+      },
+      "ext": {
+        "amp": 1
+      }
+    },
+    "test": 1
+  },
+  "biddersFPDResolved": {},
+  "validationErrors": []
+}
diff --git a/firstpartydata/tests/extractfpdforbidders/global-fpd-empty-user-data.json b/firstpartydata/tests/extractfpdforbidders/global-fpd-empty-user-data.json
new file mode 100644
index 00000000..f7bc1d81
--- /dev/null
+++ b/firstpartydata/tests/extractfpdforbidders/global-fpd-empty-user-data.json
@@ -0,0 +1,42 @@
+{
+  "description": "req.user.data defined; req.ext.prebid.data.bidders is empty; expect req.site.content.data do not contain req.app.content.data from the original request",
+  "inputRequestData": {
+    "id": "bid_id",
+    "user": {
+      "id": "reqUserID",
+      "keywords": "userKeyword",
+      "yob": 1982,
+      "gender": "M",
+      "data": [
+        {
+          "id": "userDataId1",
+          "name": "userDataName1"
+        },
+        {
+          "id": "userDataId2",
+          "name": "userDataName2"
+        }
+      ]
+    },
+    "test": 1,
+    "ext": {
+      "prebid": {
+        "data": {
+          "bidders": []
+        }
+      }
+    }
+  },
+  "outputRequestData": {
+    "id": "bid_id",
+    "user": {
+      "id": "reqUserID",
+      "keywords": "userKeyword",
+      "yob": 1982,
+      "gender": "M"
+    },
+    "test": 1
+  },
+  "biddersFPDResolved": {},
+  "validationErrors": []
+}
diff --git a/firstpartydata/tests/extractfpdforbidders/global-fpd-empty-user.json b/firstpartydata/tests/extractfpdforbidders/global-fpd-empty-user.json
new file mode 100644
index 00000000..cd40d84d
--- /dev/null
+++ b/firstpartydata/tests/extractfpdforbidders/global-fpd-empty-user.json
@@ -0,0 +1,43 @@
+{
+  "description": "req.user.ext.data is present but req.ext.prebid.data.bidders is empty; expect req.user.ext.data to not be included in any of the resolved bidders FPD and to have been extracted from the original request",
+  "inputRequestData": {
+    "id": "bid_id",
+    "user": {
+      "id": "reqUserId",
+      "keywords": "userKeywords",
+      "publisher": {
+        "id": "1"
+      },
+      "ext": {
+        "amp": 1,
+        "data": {
+          "someuserfpd": "userfpdDataTest"
+        }
+      }
+    },
+    "test": 1,
+    "ext": {
+      "prebid": {
+        "data": {
+          "bidders": []
+        }
+      }
+    }
+  },
+  "outputRequestData":  {
+    "id": "bid_id",
+    "user": {
+      "id": "reqUserId",
+      "keywords": "userKeywords",
+      "publisher": {
+        "id": "1"
+      },
+      "ext": {
+        "amp": 1
+      }
+    },
+    "test": 1
+  },
+  "biddersFPDResolved": {},
+  "validationErrors": []
+}
diff --git a/firstpartydata/tests/extractfpdforbidders/global-fpd-not-defined-app-content-data.json b/firstpartydata/tests/extractfpdforbidders/global-fpd-not-defined-app-content-data.json
new file mode 100644
index 00000000..2f753ad5
--- /dev/null
+++ b/firstpartydata/tests/extractfpdforbidders/global-fpd-not-defined-app-content-data.json
@@ -0,0 +1,61 @@
+{
+  "description": "req.app.content.data is present but req.ext.prebid is not defined; expect req.app.content.data remains the same and no FPD data for bidders",
+  "inputRequestData": {
+    "id": "bid_id",
+    "app": {
+      "id": "reqAppId",
+      "domain": "http://www.foobar.com/1234.html",
+      "name": "reqAppName",
+      "cat": [
+        "electronics",
+        "phone"
+      ],
+      "content": {
+        "len": 900,
+        "title": "reqContentTitle",
+        "season": "reqContentSeason",
+        "data": [
+          {
+            "id": "reqContentDataId1",
+            "name": "reqContentDataName1"
+          },
+          {
+            "id": "reqContentDataId2",
+            "name": "reqContentDataName2"
+          }
+        ]
+      }
+    },
+    "test": 1
+  },
+  "outputRequestData": {
+    "id": "bid_id",
+    "app": {
+      "id": "reqAppId",
+      "domain": "http://www.foobar.com/1234.html",
+      "name": "reqAppName",
+      "cat": [
+        "electronics",
+        "phone"
+      ],
+      "content": {
+        "len": 900,
+        "title": "reqContentTitle",
+        "season": "reqContentSeason",
+        "data": [
+          {
+            "id": "reqContentDataId1",
+            "name": "reqContentDataName1"
+          },
+          {
+            "id": "reqContentDataId2",
+            "name": "reqContentDataName2"
+          }
+        ]
+      }
+    },
+    "test": 1
+  },
+  "biddersFPDResolved": {},
+  "validationErrors": []
+}
diff --git a/firstpartydata/tests/extractfpdforbidders/global-fpd-not-defined-app.json b/firstpartydata/tests/extractfpdforbidders/global-fpd-not-defined-app.json
new file mode 100644
index 00000000..c60c9a86
--- /dev/null
+++ b/firstpartydata/tests/extractfpdforbidders/global-fpd-not-defined-app.json
@@ -0,0 +1,39 @@
+{
+  "description": "req.app.ext.data is present but req.ext.prebid is not defined; expect req.app.ext.data remains the same and no FPD data for bidders",
+  "inputRequestData": {
+    "id": "bid_id",
+    "app": {
+      "id": "reqAppId",
+      "keywords": "appKeywords",
+      "publisher": {
+        "id": "1"
+      },
+      "ext": {
+        "amp": 1,
+        "data": {
+          "someappfpd": "appfpdDataTest"
+        }
+      }
+    },
+    "test": 1
+  },
+  "outputRequestData":  {
+    "id": "bid_id",
+    "app": {
+      "id": "reqAppId",
+      "keywords": "appKeywords",
+      "publisher": {
+        "id": "1"
+      },
+      "ext": {
+        "amp":1,
+        "data": {
+          "someappfpd": "appfpdDataTest"
+        }
+      }
+    },
+    "test": 1
+  },
+  "biddersFPDResolved": {},
+  "validationErrors": []
+}
diff --git a/firstpartydata/tests/extractfpdforbidders/global-fpd-not-defined-site-content-data.json b/firstpartydata/tests/extractfpdforbidders/global-fpd-not-defined-site-content-data.json
new file mode 100644
index 00000000..594c8e77
--- /dev/null
+++ b/firstpartydata/tests/extractfpdforbidders/global-fpd-not-defined-site-content-data.json
@@ -0,0 +1,67 @@
+{
+  "description": "req.site.content.data is present but req.ext.prebid is not defined; expect req.site.content.data remains the same and no FPD data for bidders",
+  "inputRequestData": {
+    "id": "bid_id",
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "name": "reqSiteName",
+      "cat": [
+        "electronics",
+        "phone"
+      ],
+      "publisher": {
+        "id": "1"
+      },
+      "content": {
+        "len": 900,
+        "title": "reqContentTitle",
+        "season": "reqContentSeason",
+        "data": [
+          {
+            "id": "reqContentDataId1",
+            "name": "reqContentDataName1"
+          },
+          {
+            "id": "reqContentDataId2",
+            "name": "reqContentDataName2"
+          }
+        ]
+      }
+    },
+    "test": 1
+  },
+  "outputRequestData": {
+    "id": "bid_id",
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "name": "reqSiteName",
+      "cat": [
+        "electronics",
+        "phone"
+      ],
+      "publisher": {
+        "id": "1"
+      },
+      "content": {
+        "len": 900,
+        "title": "reqContentTitle",
+        "season": "reqContentSeason",
+        "data": [
+          {
+            "id": "reqContentDataId1",
+            "name": "reqContentDataName1"
+          },
+          {
+            "id": "reqContentDataId2",
+            "name": "reqContentDataName2"
+          }
+        ]
+      }
+    },
+    "test": 1
+  },
+  "biddersFPDResolved": {},
+  "validationErrors": []
+}
diff --git a/firstpartydata/tests/extractfpdforbidders/global-fpd-not-defined-site.json b/firstpartydata/tests/extractfpdforbidders/global-fpd-not-defined-site.json
new file mode 100644
index 00000000..1eb04ff2
--- /dev/null
+++ b/firstpartydata/tests/extractfpdforbidders/global-fpd-not-defined-site.json
@@ -0,0 +1,39 @@
+{
+  "description": "req.site.ext.data is present but req.ext.prebid is not defined; expect req.site.ext.data remains the same and no FPD data for bidders",
+  "inputRequestData": {
+    "id": "bid_id",
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "publisher": {
+        "id": "1"
+      },
+      "ext": {
+        "amp": 1,
+        "data": {
+          "somesitefpd": "sitefpdDataTest"
+        }
+      }
+    },
+    "test": 1
+  },
+  "outputRequestData":  {
+    "id": "bid_id",
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "publisher": {
+        "id": "1"
+      },
+      "ext": {
+        "amp":1,
+        "data": {
+          "somesitefpd": "sitefpdDataTest"
+        }
+      }
+    },
+    "test": 1
+  },
+  "biddersFPDResolved": {},
+  "validationErrors": []
+}
diff --git a/firstpartydata/tests/extractfpdforbidders/global-fpd-not-defined-user-data.json b/firstpartydata/tests/extractfpdforbidders/global-fpd-not-defined-user-data.json
new file mode 100644
index 00000000..4bb013b1
--- /dev/null
+++ b/firstpartydata/tests/extractfpdforbidders/global-fpd-not-defined-user-data.json
@@ -0,0 +1,45 @@
+{
+  "description": "req.user.data is present but req.ext.prebid is not defined; expect req.user.data remains the same and no FPD data for bidders",
+  "inputRequestData": {
+    "id": "bid_id",
+    "user": {
+      "id": "reqUserID",
+      "keywords": "userKeyword",
+      "yob": 1982,
+      "gender": "M",
+      "data": [
+        {
+          "id": "userDataId1",
+          "name": "userDataName1"
+        },
+        {
+          "id": "userDataId2",
+          "name": "userDataName2"
+        }
+      ]
+    },
+    "test": 1
+  },
+  "outputRequestData": {
+    "id": "bid_id",
+    "user": {
+      "id": "reqUserID",
+      "keywords": "userKeyword",
+      "yob": 1982,
+      "gender": "M",
+      "data": [
+        {
+          "id": "userDataId1",
+          "name": "userDataName1"
+        },
+        {
+          "id": "userDataId2",
+          "name": "userDataName2"
+        }
+      ]
+    },
+    "test": 1
+  },
+  "biddersFPDResolved": {},
+  "validationErrors": []
+}
diff --git a/firstpartydata/tests/extractfpdforbidders/global-fpd-not-defined-user.json b/firstpartydata/tests/extractfpdforbidders/global-fpd-not-defined-user.json
new file mode 100644
index 00000000..91ebcab4
--- /dev/null
+++ b/firstpartydata/tests/extractfpdforbidders/global-fpd-not-defined-user.json
@@ -0,0 +1,39 @@
+{
+  "description": "req.user.ext.data is present but req.ext.prebid is not defined; expect req.user.ext.data remains the same and no FPD data for bidders",
+  "inputRequestData": {
+    "id": "bid_id",
+    "user": {
+      "id": "reqUserId",
+      "keywords": "userKeywords",
+      "publisher": {
+        "id": "1"
+      },
+      "ext": {
+        "amp": 1,
+        "data": {
+          "someuserfpd": "userfpdDataTest"
+        }
+      }
+    },
+    "test": 1
+  },
+  "outputRequestData":  {
+    "id": "bid_id",
+    "user": {
+      "id": "reqUserId",
+      "keywords": "userKeywords",
+      "publisher": {
+        "id": "1"
+      },
+      "ext": {
+        "amp":1,
+        "data": {
+          "someuserfpd": "userfpdDataTest"
+        }
+      }
+    },
+    "test": 1
+  },
+  "biddersFPDResolved": {},
+  "validationErrors": []
+}
diff --git a/firstpartydata/tests/extractfpdforbidders/two-bidders-correct-fpd.json b/firstpartydata/tests/extractfpdforbidders/two-bidders-correct-fpd.json
new file mode 100644
index 00000000..62b2a44e
--- /dev/null
+++ b/firstpartydata/tests/extractfpdforbidders/two-bidders-correct-fpd.json
@@ -0,0 +1,98 @@
+{
+  "description": "req.site is present; req.ext.prebid.data.bidders has two bidders; req.ext.prebid.bidderconfig is defined for two bidders from req.ext.prebid.data.bidders; expect to see both bidders in resolved bidder FPD",
+  "inputRequestData": {
+    "id": "bid_id",
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "publisher": {
+        "id": "1"
+      }
+    },
+    "user": {
+      "id": "reqUserId"
+    },
+    "test": 1,
+    "ext": {
+      "prebid": {
+        "data": {
+          "bidders": [
+            "appnexus",
+            "telaria"
+          ]
+        },
+        "bidderconfig": [
+          {
+            "bidders": [
+              "appnexus"
+            ],
+            "config": {
+              "ortb2": {
+                "site": {
+                  "id": "apnSiteId"
+                },
+                "user": {
+                  "id": "apnUserId"
+                }
+              }
+            }
+          },
+          {
+            "bidders": [
+              "telaria"
+            ],
+            "config": {
+              "ortb2": {
+                "user": {
+                  "keywords": "telariaUserKeywords"
+                }
+              }
+            }
+          }
+        ]
+      }
+    }
+  },
+  "outputRequestData": {
+    "id": "bid_id",
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "publisher": {
+        "id": "1"
+      }
+    },
+    "test": 1
+  },
+  "biddersFPDResolved": {
+    "appnexus": {
+      "site": {
+        "id": "reqSiteId",
+        "page": "http://www.foobar.com/1234.html",
+        "publisher": {
+          "id": "1"
+        },
+        "ext": {
+          "data": {
+            "id": "apnSiteId"
+          }
+        }
+      },
+      "user": {
+        "id": "reqUserId",
+        "ext": {
+          "data": {
+            "id": "apnUserId"
+          }
+        }
+      }
+    },
+    "telaria": {
+      "user": {
+        "id": "reqUserId",
+        "keywords": "telariaUserKeywords"
+      }
+    }
+  },
+  "validationErrors": []
+}
diff --git a/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-app-user.json b/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-app-user.json
new file mode 100644
index 00000000..6fed14e9
--- /dev/null
+++ b/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-app-user.json
@@ -0,0 +1,184 @@
+{
+  "description": "req.app.content.data defined; bidder config defined for appnexus and telaria but only appnexus listed in req.ext.prebid.data.bidders; expect all FPD data to be included only in appnexus resolved FPD and extracted from the original request",
+  "inputRequestData": {
+    "id": "bid_id",
+    "app": {
+      "id": "reqAppId",
+      "page": "http://www.foobar.com/1234.html",
+      "name": "reqAppName",
+      "cat": [
+        "electronics",
+        "phone"
+      ],
+      "publisher": {
+        "id": "1"
+      },
+      "content": {
+        "len": 900,
+        "title": "reqContentTitle",
+        "season": "reqContentSeason",
+        "data": [
+          {
+            "id": "reqContentDataId1",
+            "name": "reqContentDataName1"
+          },
+          {
+            "id": "reqContentDataId2",
+            "name": "reqContentDataName2"
+          }
+        ]
+      }
+    },
+    "user": {
+      "id": "reqUserId",
+      "keywords": "reqUserKeyword"
+    },
+    "test": 1,
+    "ext": {
+      "prebid": {
+        "data": {
+          "bidders": [
+            "appnexus"
+          ]
+        },
+        "bidderconfig": [
+          {
+            "bidders": [
+              "appnexus"
+            ],
+            "config": {
+              "ortb2": {
+                "app": {
+                  "id": "apnFpdAppId",
+                  "name": "apnFpdAppIdAppName",
+                  "cat": [
+                    "books",
+                    "news"
+                  ],
+                  "publisher": {
+                    "id": "1"
+                  },
+                  "content": {
+                    "len": 900,
+                    "title": "apnFpdAppIdContentTitle",
+                    "season": "apnFpdAppIdContentSeason",
+                    "data": [
+                      {
+                        "id": "apnFpdAppIdContentDataId1",
+                        "name": "apnFpdAppIdContentDataName1"
+                      },
+                      {
+                        "id": "apnFpdAppIdContentDataId2",
+                        "name": "apnFpdAppIdContentDataName2"
+                      }
+                    ]
+                  }
+                },
+                "user": {
+                  "id": "apnUserId",
+                  "keywords": "apnFpdUserKeyword"
+                }
+              }
+            }
+          },
+          {
+            "bidders": [
+              "telaria"
+            ],
+            "config": {
+              "ortb2": {
+                "user": {
+                  "keywords": "telariaFpdUserKeywords"
+                }
+              }
+            }
+          }
+        ]
+      }
+    }
+  },
+  "outputRequestData": {
+    "id": "bid_id",
+    "app": {
+      "id": "reqAppId",
+      "page": "http://www.foobar.com/1234.html",
+      "name": "reqAppName",
+      "cat": [
+        "electronics",
+        "phone"
+      ],
+      "publisher": {
+        "id": "1"
+      },
+      "content": {
+        "len": 900,
+        "title": "reqContentTitle",
+        "season": "reqContentSeason"
+      }
+    },
+    "test": 1
+  },
+  "biddersFPDResolved": {
+    "appnexus": {
+      "app": {
+        "id": "reqAppId",
+        "name": "apnFpdAppIdAppName",
+        "cat": [
+          "books",
+          "news"
+        ],
+        "publisher": {
+          "id": "1"
+        },
+        "content": {
+          "len": 900,
+          "title": "reqContentTitle",
+          "season": "reqContentSeason",
+          "data": [
+            {
+              "id": "reqContentDataId1",
+              "name": "reqContentDataName1"
+            },
+            {
+              "id": "reqContentDataId2",
+              "name": "reqContentDataName2"
+            }
+          ]
+        },
+        "ext": {
+          "data": {
+            "content": {
+              "len": 900,
+              "title": "apnFpdAppIdContentTitle",
+              "season": "apnFpdAppIdContentSeason",
+              "data": [
+                {
+                  "id": "apnFpdAppIdContentDataId1",
+                  "name": "apnFpdAppIdContentDataName1"
+                },
+                {
+                  "id": "apnFpdAppIdContentDataId2",
+                  "name": "apnFpdAppIdContentDataName2"
+                }
+              ]
+            },
+            "id": "apnFpdAppId",
+            "publisher": {
+              "id": "1"
+            }
+          }
+        }
+      },
+      "user": {
+        "id": "reqUserId",
+        "keywords": "apnFpdUserKeyword",
+        "ext": {
+          "data": {
+            "id":"apnUserId"
+          }
+        }
+      }
+    }
+  },
+  "validationErrors": []
+}
diff --git a/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-site-user.json b/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-site-user.json
new file mode 100644
index 00000000..12908744
--- /dev/null
+++ b/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-site-user.json
@@ -0,0 +1,186 @@
+{
+  "description": "req.site is present; req.ext.prebid.data.bidders has appnexus bidder only; req.ext.prebid.bidderconfig is defined for two bidders appnexus and telaria; expect to see only appnexus in resolved bidder FPD and have extra data in site.ext.data",
+  "inputRequestData": {
+    "id": "bid_id",
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "name": "reqSiteName",
+      "cat": [
+        "electronics",
+        "phone"
+      ],
+      "publisher": {
+        "id": "1"
+      },
+      "content": {
+        "len": 900,
+        "title": "reqContentTitle",
+        "season": "reqContentSeason",
+        "data": [
+          {
+            "id": "reqContentDataId1",
+            "name": "reqContentDataName1"
+          },
+          {
+            "id": "reqContentDataId2",
+            "name": "reqContentDataName2"
+          }
+        ]
+      }
+    },
+    "user": {
+      "id": "reqUserId",
+      "keywords": "reqUserKeyword"
+    },
+    "test": 1,
+    "ext": {
+      "prebid": {
+        "data": {
+          "bidders": [
+            "appnexus"
+          ]
+        },
+        "bidderconfig": [
+          {
+            "bidders": [
+              "appnexus"
+            ],
+            "config": {
+              "ortb2": {
+                "site": {
+                  "id": "apnFpdSiteId",
+                  "page": "http://www.foobar.com/4321.html",
+                  "name": "apnFpdSiteIdSiteName",
+                  "cat": [
+                    "books",
+                    "news"
+                  ],
+                  "publisher": {
+                    "id": "1"
+                  },
+                  "content": {
+                    "len": 900,
+                    "title": "apnFpdSiteIdContentTitle",
+                    "season": "apnFpdSiteIdContentSeason",
+                    "data": [
+                      {
+                        "id": "apnFpdSiteIdContentDataId1",
+                        "name": "apnFpdSiteIdContentDataName1"
+                      },
+                      {
+                        "id": "apnFpdSiteIdContentDataId2",
+                        "name": "apnFpdSiteIdContentDataName2"
+                      }
+                    ]
+                  }
+                },
+                "user": {
+                  "id": "apnUserId",
+                  "keywords": "apnFpdUserKeyword"
+                }
+              }
+            }
+          },
+          {
+            "bidders": [
+              "telaria"
+            ],
+            "config": {
+              "ortb2": {
+                "user": {
+                  "keywords": "telariaFpdUserKeywords"
+                }
+              }
+            }
+          }
+        ]
+      }
+    }
+  },
+  "outputRequestData": {
+    "id": "bid_id",
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "name": "reqSiteName",
+      "cat": [
+        "electronics",
+        "phone"
+      ],
+      "publisher": {
+        "id": "1"
+      },
+      "content": {
+        "len": 900,
+        "title": "reqContentTitle",
+        "season": "reqContentSeason"
+      }
+    },
+    "test": 1
+  },
+  "biddersFPDResolved": {
+    "appnexus": {
+      "site": {
+        "id": "reqSiteId",
+        "page": "http://www.foobar.com/4321.html",
+        "name": "apnFpdSiteIdSiteName",
+        "cat": [
+          "books",
+          "news"
+        ],
+        "publisher": {
+          "id": "1"
+        },
+        "content": {
+          "len": 900,
+          "title": "reqContentTitle",
+          "season": "reqContentSeason",
+          "data": [
+            {
+              "id": "reqContentDataId1",
+              "name": "reqContentDataName1"
+            },
+            {
+              "id": "reqContentDataId2",
+              "name": "reqContentDataName2"
+            }
+          ]
+        },
+        "ext": {
+          "data": {
+            "content": {
+              "len": 900,
+              "title": "apnFpdSiteIdContentTitle",
+              "season": "apnFpdSiteIdContentSeason",
+              "data": [
+                {
+                  "id": "apnFpdSiteIdContentDataId1",
+                  "name": "apnFpdSiteIdContentDataName1"
+                },
+                {
+                  "id": "apnFpdSiteIdContentDataId2",
+                  "name": "apnFpdSiteIdContentDataName2"
+                }
+              ]
+            },
+            "id": "apnFpdSiteId",
+            "publisher": {
+              "id": "1"
+            }
+          }
+        }
+      },
+      "user": {
+        "id": "reqUserId",
+        "keywords": "apnFpdUserKeyword",
+        "ext": {
+          "data": {
+            "id":"apnUserId"
+          }
+        }
+      }
+    }
+  },
+  "validationErrors": []
+}
diff --git a/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-user.json b/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-user.json
new file mode 100644
index 00000000..c04838c7
--- /dev/null
+++ b/firstpartydata/tests/extractfpdforbidders/two-bidders-global-and-non-global-user.json
@@ -0,0 +1,222 @@
+{
+  "description": "req.site and req.user is present; req.ext.prebid.data.bidders has appnexus bidder only; req.ext.prebid.bidderconfig is defined for two bidders; expect to see only appnexus in bidders FPD and contain site and user data",
+  "inputRequestData": {
+    "id": "bid_id",
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "name": "reqSiteName",
+      "cat": [
+        "electronics",
+        "phone"
+      ],
+      "publisher": {
+        "id": "1"
+      },
+      "content": {
+        "len": 900,
+        "title": "reqContentTitle",
+        "season": "reqContentSeason",
+        "data": [
+          {
+            "id": "reqContentDataId1",
+            "name": "reqContentDataName1"
+          },
+          {
+            "id": "reqContentDataId2",
+            "name": "reqContentDataName2"
+          }
+        ]
+      }
+    },
+    "user": {
+      "id": "reqUserId"
+    },
+    "test": 1,
+    "ext": {
+      "prebid": {
+        "data": {
+          "bidders": [
+            "appnexus"
+          ]
+        },
+        "bidderconfig": [
+          {
+            "bidders": [
+              "appnexus"
+            ],
+            "config": {
+              "ortb2": {
+                "site": {
+                  "id": "apnFpdSiteId",
+                  "page": "http://www.foobar.com/4321.html",
+                  "name": "apnFpdSiteIdSiteName",
+                  "cat": [
+                    "books",
+                    "news"
+                  ],
+                  "publisher": {
+                    "id": "1"
+                  },
+                  "content": {
+                    "len": 900,
+                    "title": "apnFpdSiteIdContentTitle",
+                    "season": "apnFpdSiteIdContentSeason",
+                    "data": [
+                      {
+                        "id": "apnFpdSiteIdContentDataId1",
+                        "name": "apnFpdSiteIdContentDataName1"
+                      },
+                      {
+                        "id": "apnFpdSiteIdContentDataId2",
+                        "name": "apnFpdSiteIdContentDataName2"
+                      }
+                    ]
+                  }
+                },
+                "user": {
+                  "id": "apnUserId",
+                  "gender": "F",
+                  "yob": 2000,
+                  "keywords": "apnFpdUserKeyword",
+                  "data": [
+                    {
+                      "id": "apnFpdUserContentDataId1",
+                      "name": "apnFpdUserContentDataName1"
+                    },
+                    {
+                      "id": "apnFpdUserContentDataId2",
+                      "name": "apnFpdUserContentDataName2"
+                    }
+                  ]
+                }
+              }
+            }
+          },
+          {
+            "bidders": [
+              "telaria"
+            ],
+            "config": {
+              "ortb2": {
+                "user": {
+                  "id": "telariaFpdUserId",
+                  "gender": "M",
+                  "yob": 2001,
+                  "keywords": "telariaFpdUserKeyword",
+                  "data": [
+                    {
+                      "id": "telariaFpdUserContentDataId1",
+                      "name": "telariaFpdUserContentDataName1"
+                    },
+                    {
+                      "id": "telariaFpdUserContentDataId2",
+                      "name": "telariaFpdUserContentDataName2"
+                    }
+                  ]
+                }
+              }
+            }
+          }
+        ]
+      }
+    }
+  },
+  "outputRequestData": {
+    "id": "bid_id",
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "name": "reqSiteName",
+      "cat": [
+        "electronics",
+        "phone"
+      ],
+      "publisher": {
+        "id": "1"
+      },
+      "content": {
+        "len": 900,
+        "title": "reqContentTitle",
+        "season": "reqContentSeason"
+      }
+    },
+    "test": 1
+  },
+  "biddersFPDResolved": {
+    "appnexus": {
+      "site": {
+        "id": "reqSiteId",
+        "page": "http://www.foobar.com/4321.html",
+        "name": "apnFpdSiteIdSiteName",
+        "cat": [
+          "books",
+          "news"
+        ],
+        "publisher": {
+          "id": "1"
+        },
+        "content": {
+          "len": 900,
+          "title": "reqContentTitle",
+          "season": "reqContentSeason",
+          "data": [
+            {
+              "id": "reqContentDataId1",
+              "name": "reqContentDataName1"
+            },
+            {
+              "id": "reqContentDataId2",
+              "name": "reqContentDataName2"
+            }
+          ]
+        },
+        "ext": {
+          "data": {
+            "id": "apnFpdSiteId",
+            "publisher": {
+              "id": "1"
+            },
+            "content": {
+              "len": 900,
+              "title": "apnFpdSiteIdContentTitle",
+              "season": "apnFpdSiteIdContentSeason",
+              "data": [
+                {
+                  "id": "apnFpdSiteIdContentDataId1",
+                  "name": "apnFpdSiteIdContentDataName1"
+                },
+                {
+                  "id": "apnFpdSiteIdContentDataId2",
+                  "name": "apnFpdSiteIdContentDataName2"
+                }
+              ]
+            }
+          }
+        }
+      },
+      "user": {
+        "id": "reqUserId",
+        "gender": "F",
+        "yob": 2000,
+        "keywords": "apnFpdUserKeyword",
+        "ext": {
+          "data": {
+            "id": "apnUserId",
+            "data": [
+              {
+                "id": "apnFpdUserContentDataId1",
+                "name": "apnFpdUserContentDataName1"
+              },
+              {
+                "id": "apnFpdUserContentDataId2",
+                "name": "apnFpdUserContentDataName2"
+              }
+            ]
+          }
+        }
+      }
+    }
+  },
+  "validationErrors": []
+}
diff --git a/firstpartydata/tests/extractfpdforbidders/two-bidders-no-global-bidder-list.json b/firstpartydata/tests/extractfpdforbidders/two-bidders-no-global-bidder-list.json
new file mode 100644
index 00000000..d86db6dc
--- /dev/null
+++ b/firstpartydata/tests/extractfpdforbidders/two-bidders-no-global-bidder-list.json
@@ -0,0 +1,98 @@
+{
+  "description": "req.site is present; req.ext.prebid.data.bidders is not defined; req.ext.prebid.bidderconfig is defined for two bidders. Expect to have both bidders in resoved FPD",
+  "inputRequestData": {
+    "id": "bid_id",
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "publisher": {
+        "id": "1"
+      },
+      "ext": {
+        "data": "fpdGlobalSiteData"
+      }
+    },
+    "user": {
+      "id": "reqUserId"
+    },
+    "test": 1,
+    "ext": {
+      "prebid": {
+        "bidderconfig": [
+          {
+            "bidders": [
+              "appnexus"
+            ],
+            "config": {
+              "ortb2": {
+                "site": {
+                  "id": "apnSiteId"
+                },
+                "user": {
+                  "id": "apnUserId"
+                }
+              }
+            }
+          },
+          {
+            "bidders": [
+              "telaria"
+            ],
+            "config": {
+              "ortb2": {
+                "user": {
+                  "keywords": "telariaUserKeywords"
+                }
+              }
+            }
+          }
+        ]
+      }
+    }
+  },
+  "outputRequestData": {
+    "id": "bid_id",
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "publisher": {
+        "id": "1"
+      },
+      "ext": {
+        "data": "fpdGlobalSiteData"
+      }
+    },
+    "test": 1
+  },
+  "biddersFPDResolved": {
+    "appnexus": {
+      "site": {
+        "id": "reqSiteId",
+        "page": "http://www.foobar.com/1234.html",
+        "publisher": {
+          "id": "1"
+        },
+        "ext": {
+          "data":{
+            "id":"apnSiteId"
+          }
+        }
+      },
+      "user": {
+        "id": "reqUserId",
+        "ext": {
+          "data":{
+            "id":"apnUserId"
+          }
+        }
+      }
+    },
+    "telaria": {
+      "user": {
+        "id": "reqUserId",
+        "keywords": "telariaUserKeywords"
+      }
+    }
+  },
+  "validationErrors": []
+}
diff --git a/firstpartydata/tests/extractfpdforbidders/two-bidders-one-with-incorrect-fpd.json b/firstpartydata/tests/extractfpdforbidders/two-bidders-one-with-incorrect-fpd.json
new file mode 100644
index 00000000..a473fb9c
--- /dev/null
+++ b/firstpartydata/tests/extractfpdforbidders/two-bidders-one-with-incorrect-fpd.json
@@ -0,0 +1,54 @@
+{
+  "description": "req.site is present; req.ext.prebid.data.bidders has appnexus and telaria bidders; req.ext.prebid.bidderconfig is defined for both bidders from req.ext.prebid.data.bidders; req.ext.prebid.bidderconfig for telaria has app config in it; expect to have validation error about app object is not defined in request",
+  "inputRequestData": {
+    "id": "bid_id",
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "publisher": {
+        "id": "1"
+      }
+    },
+    "test": 1,
+    "ext": {
+      "prebid": {
+        "data": {
+          "bidders": [
+            "appnexus", "telaria"
+          ]
+        },
+        "bidderconfig": [
+          {
+            "bidders": [
+              "appnexus"
+            ],
+            "config": {
+              "ortb2": {
+                "site": {
+                  "id": "apnSiteId"
+                }
+              }
+            }
+          },
+          {
+            "bidders": [
+              "telaria"
+            ],
+            "config": {
+              "ortb2": {
+                "app": {
+                  "id": "telariaAppId"
+                }
+              }
+            }
+          }
+        ]
+      }
+    }
+  },
+  "outputRequestData":  {},
+  "biddersFPDResolved": {},
+  "validationErrors": [
+    {"Message": "incorrect First Party Data for bidder telaria: App object is not defined in request, but defined in FPD config"}
+  ]
+}
diff --git a/firstpartydata/tests/extractfpdforbidders/two-bidders-with-incorrect-fpd.json b/firstpartydata/tests/extractfpdforbidders/two-bidders-with-incorrect-fpd.json
new file mode 100644
index 00000000..a23b4539
--- /dev/null
+++ b/firstpartydata/tests/extractfpdforbidders/two-bidders-with-incorrect-fpd.json
@@ -0,0 +1,59 @@
+{
+  "description": "req.site is present; req.ext.prebid.data.bidders has appnexus and telaria bidders; req.ext.prebid.bidderconfig is defined for both of them; req.ext.prebid.bidderconfig for appnexus contains app and user; req.ext.prebid.bidderconfig for telaria contains app; expected to have error for appnexus: app is not present in request, user is not present in request; expect to have error for telaria: app is not present in request",
+  "inputRequestData": {
+    "id": "bid_id",
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "publisher": {
+        "id": "1"
+      }
+    },
+    "test": 1,
+    "ext": {
+      "prebid": {
+        "data": {
+          "bidders": [
+            "appnexus", "telaria"
+          ]
+        },
+        "bidderconfig": [
+          {
+            "bidders": [
+              "appnexus"
+            ],
+            "config": {
+              "ortb2": {
+                "app": {
+                  "id": "apnAppId"
+                },
+                "user": {
+                  "id": "apnUserId"
+                }
+              }
+            }
+          },
+          {
+            "bidders": [
+              "telaria"
+            ],
+            "config": {
+              "ortb2": {
+                "app": {
+                  "id": "telariaAppId"
+                }
+              }
+            }
+          }
+        ]
+      }
+    }
+  },
+  "outputRequestData":{},
+  "biddersFPDResolved": {},
+  "validationErrors": [
+    {"Message": "incorrect First Party Data for bidder appnexus: User object is not defined in request, but defined in FPD config"},
+    {"Message": "incorrect First Party Data for bidder appnexus: App object is not defined in request, but defined in FPD config"},
+    {"Message": "incorrect First Party Data for bidder telaria: App object is not defined in request, but defined in FPD config"}
+  ]
+}
diff --git a/firstpartydata/tests/resolvefpd/bidder-fpd-only-app.json b/firstpartydata/tests/resolvefpd/bidder-fpd-only-app.json
new file mode 100644
index 00000000..db579fb2
--- /dev/null
+++ b/firstpartydata/tests/resolvefpd/bidder-fpd-only-app.json
@@ -0,0 +1,44 @@
+{
+  "description": "Bidder FPD defined only for app",
+  "inputRequestData": {
+    "app": {
+      "id": "reqUserID"
+    }
+  },
+  "bidderConfigFPD": {
+    "appnexus": {
+      "app": {
+        "id": "apnAppId",
+        "ext": {
+          "data": {
+            "morefpdData": "morefpddata",
+            "appFpddata": "appFpddata",
+            "moreFpd": {
+              "fpd": 123
+            }
+          }
+        }
+      }
+    }
+  },
+  "outputRequestData": {
+    "app": {
+      "id": "reqUserID",
+      "ext": {
+        "data": {
+          "ext": {
+            "data": {
+              "morefpdData": "morefpddata",
+              "appFpddata": "appFpddata",
+              "moreFpd": {
+                "fpd": 123
+              }
+            }
+          },
+          "id": "apnAppId"
+        }
+      }
+    }
+  }
+}
+
diff --git a/firstpartydata/tests/resolvefpd/bidder-fpd-only-site.json b/firstpartydata/tests/resolvefpd/bidder-fpd-only-site.json
new file mode 100644
index 00000000..86a40f29
--- /dev/null
+++ b/firstpartydata/tests/resolvefpd/bidder-fpd-only-site.json
@@ -0,0 +1,44 @@
+{
+  "description": "Bidder FPD defined only for site",
+  "inputRequestData": {
+    "site": {
+      "id": "reqUserID"
+    }
+  },
+  "bidderConfigFPD": {
+    "appnexus": {
+      "site": {
+        "id": "apnSiteId",
+        "ext": {
+          "data": {
+            "morefpdData": "morefpddata",
+            "siteFpddata": "siteFpddata",
+            "moreFpd": {
+              "fpd": 123
+            }
+          }
+        }
+      }
+    }
+  },
+  "outputRequestData": {
+    "site": {
+      "id": "reqUserID",
+      "ext": {
+        "data": {
+          "ext": {
+            "data": {
+              "morefpdData": "morefpddata",
+              "siteFpddata": "siteFpddata",
+              "moreFpd": {
+                "fpd": 123
+              }
+            }
+          },
+          "id": "apnSiteId"
+        }
+      }
+    }
+  }
+}
+
diff --git a/firstpartydata/tests/resolvefpd/bidder-fpd-only-user.json b/firstpartydata/tests/resolvefpd/bidder-fpd-only-user.json
new file mode 100644
index 00000000..a1fb4b41
--- /dev/null
+++ b/firstpartydata/tests/resolvefpd/bidder-fpd-only-user.json
@@ -0,0 +1,49 @@
+{
+  "description": "Bidder FPD defined only for user",
+  "inputRequestData": {
+    "user": {
+      "id": "reqUserID",
+      "yob": 1982,
+      "gender": "M"
+    }
+  },
+  "bidderConfigFPD": {
+    "appnexus": {
+      "user": {
+        "id": "apnUserId",
+        "yob": 1982,
+        "ext": {
+          "data": {
+            "morefpdData": "morefpddata",
+            "userFpddata": "userFpddata",
+            "moreFpd": {
+              "fpd": 123
+            }
+          }
+        }
+      }
+    }
+  },
+  "outputRequestData": {
+    "user": {
+      "id": "reqUserID",
+      "yob": 1982,
+      "gender": "M",
+      "ext": {
+        "data": {
+          "ext": {
+            "data": {
+              "morefpdData": "morefpddata",
+              "userFpddata": "userFpddata",
+              "moreFpd": {
+                "fpd": 123
+              }
+            }
+          },
+          "id": "apnUserId"
+        }
+      }
+    }
+  }
+}
+
diff --git a/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-app-content-data-user-data.json b/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-app-content-data-user-data.json
new file mode 100644
index 00000000..ebba9041
--- /dev/null
+++ b/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-app-content-data-user-data.json
@@ -0,0 +1,114 @@
+{
+  "description": "Global FPD defined for app and user. Bidder FPD defined for app. Global FPD has app.content.data and user.data",
+  "inputRequestData": {
+    "app": {
+      "id": "reqAppId",
+      "page": "http://www.foobar.com/1234.html",
+      "publisher": {
+        "id": "1"
+      }
+    },
+    "user": {
+      "id": "reqUserID",
+      "yob": 1982,
+      "gender": "M"
+    }
+  },
+  "bidderConfigFPD": {
+    "appnexus": {
+      "app": {
+        "id": "apnAppId",
+        "ext": {
+          "data": {
+            "morefpdData": "morefpddata",
+            "appFpddata": "appFpddata",
+            "moreFpd": {
+              "fpd": 123
+            }
+          }
+        }
+      }
+    }
+  },
+  "globalFPD": {
+    "app": {
+      "appFpd": 123
+    },
+    "user": {
+      "testUserFpd": "testuser"
+    },
+    "appContentData": [
+      {
+        "id": "appData1",
+        "name": "appName1"
+      },
+      {
+        "id": "appData2",
+        "name": "appName2"
+      }
+    ],
+    "userData": [
+      {
+        "id": "userData1",
+        "name": "userName1"
+      },
+      {
+        "id": "userData2",
+        "name": "userName2"
+      }
+    ]
+  },
+  "outputRequestData": {
+    "app": {
+      "id": "reqAppId",
+      "page": "http://www.foobar.com/1234.html",
+      "publisher": {
+        "id": "1"
+      },
+      "content": {
+        "data": [
+          {
+            "id": "appData1",
+            "name": "appName1"
+          },
+          {
+            "id": "appData2",
+            "name": "appName2"
+          }
+        ]
+      },
+      "ext": {
+        "data": {
+          "moreFpd": {
+            "fpd": 123
+          },
+          "id": "apnAppId",
+          "morefpdData": "morefpddata",
+          "appFpd": 123,
+          "appFpddata": "appFpddata"
+        }
+      }
+    },
+    "user": {
+      "id": "reqUserID",
+      "yob": 1982,
+      "gender": "M",
+      "data": [
+        {
+          "id": "userData1",
+          "name": "userName1"
+        },
+        {
+          "id": "userData2",
+          "name": "userName2"
+        }
+      ],
+      "ext": {
+        "data": {
+          "testUserFpd": "testuser"
+        }
+      }
+    }
+  }
+}
+
diff --git a/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-app.json b/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-app.json
new file mode 100644
index 00000000..dea87414
--- /dev/null
+++ b/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-app.json
@@ -0,0 +1,80 @@
+{
+  "description": "Global and bidder FPD defined for app",
+  "inputRequestData": {
+    "app": {
+      "id": "reqAppId",
+      "page": "http://www.foobar.com/1234.html",
+      "publisher": {
+        "id": "1"
+      },
+      "content": {
+        "episode": 6,
+        "title": "episodeName",
+        "series": "TvName",
+        "season": "season3",
+        "len": 900
+      }
+    },
+    "device": {
+      "ua": "testDevice",
+      "ip": "123.145.167.10",
+      "devicetype": 1,
+      "ifa": "123"
+    }
+  },
+  "bidderConfigFPD": {
+    "appnexus": {
+      "app": {
+        "id": "apnAppId",
+        "ext": {
+          "data": {
+            "morefpdData": "morefpddata",
+            "appFpddata": "appFpddata",
+            "moreFpd": {
+              "fpd": 123
+            }
+          }
+        }
+      }
+    }
+  },
+  "globalFPD": {
+    "app": {
+      "appFpd": 123
+    }
+  },
+  "outputRequestData": {
+    "app": {
+      "id": "reqAppId",
+      "page": "http://www.foobar.com/1234.html",
+      "publisher": {
+        "id": "1"
+      },
+      "content": {
+        "episode": 6,
+        "title": "episodeName",
+        "series": "TvName",
+        "season": "season3",
+        "len": 900
+      },
+      "ext": {
+        "data": {
+          "id": "apnAppId",
+          "moreFpd": {
+            "fpd": 123
+          },
+          "morefpdData": "morefpddata",
+          "appFpd": 123,
+          "appFpddata": "appFpddata"
+        }
+      }
+    },
+    "device": {
+      "ua": "testDevice",
+      "ip": "123.145.167.10",
+      "devicetype": 1,
+      "ifa": "123"
+    }
+  }
+}
+
diff --git a/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-site-app-user.json b/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-site-app-user.json
new file mode 100644
index 00000000..73f82788
--- /dev/null
+++ b/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-site-app-user.json
@@ -0,0 +1,130 @@
+{
+  "description": "Global and bidder FPD defined for site and user. Global FPD has site.content.data",
+  "inputRequestData": {
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "publisher": {
+        "id": "1"
+      },
+      "content": {
+        "episode": 6,
+        "title": "episodeName",
+        "series": "TvName",
+        "season": "season3",
+        "len": 900
+      },
+      "ext": {
+        "testSiteExt": 123
+      }
+    },
+    "device": {
+      "ua": "testDevice",
+      "ip": "123.145.167.10",
+      "devicetype": 1,
+      "ifa": "123"
+    },
+    "user": {
+      "id": "reqUserID",
+      "yob": 1982,
+      "gender": "M"
+    }
+  },
+  "bidderConfigFPD": {
+    "appnexus": {
+      "site": {
+        "id": "apnSiteId",
+        "page": "http://www.foobar.com/1234.html",
+        "ref": "fpdRef",
+        "ext": {
+          "data": {
+            "morefpdData": "morefpddata",
+            "siteFpddata": "siteFpddata",
+            "moreFpd": {
+              "fpd": 123
+            }
+          }
+        }
+      }
+    }
+  },
+  "globalFPD": {
+    "site": {
+      "siteFpd": 123
+    },
+    "app": {
+      "appFpd": {
+        "testValue": true
+      }
+    },
+    "user": {
+      "testUserFpd": "testuser"
+    },
+    "siteContentData": [
+      {
+        "id": "siteData1",
+        "name": "siteName1"
+      },
+      {
+        "id": "siteData2",
+        "name": "siteName2"
+      }
+    ]
+  },
+  "outputRequestData": {
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "ref": "fpdRef",
+      "publisher": {
+        "id": "1"
+      },
+      "content": {
+        "episode": 6,
+        "title": "episodeName",
+        "series": "TvName",
+        "season": "season3",
+        "len": 900,
+        "data": [
+          {
+            "id": "siteData1",
+            "name": "siteName1"
+          },
+          {
+            "id": "siteData2",
+            "name": "siteName2"
+          }
+        ]
+      },
+      "ext": {
+        "testSiteExt": 123,
+        "data": {
+          "id": "apnSiteId",
+          "moreFpd": {
+            "fpd": 123
+          },
+          "morefpdData": "morefpddata",
+          "siteFpd": 123,
+          "siteFpddata": "siteFpddata"
+        }
+      }
+    },
+    "device": {
+      "ua": "testDevice",
+      "ip": "123.145.167.10",
+      "devicetype": 1,
+      "ifa": "123"
+    },
+    "user": {
+      "id": "reqUserID",
+      "yob": 1982,
+      "gender": "M",
+      "ext": {
+        "data": {
+          "testUserFpd": "testuser"
+        }
+      }
+    }
+  }
+}
+
diff --git a/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-site-content-data.json b/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-site-content-data.json
new file mode 100644
index 00000000..28438349
--- /dev/null
+++ b/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-site-content-data.json
@@ -0,0 +1,144 @@
+{
+  "description": "Global and bidder FPD defined for site. Global FPD has site.content.data",
+  "inputRequestData": {
+    "site": {
+      "id": "reqSiteId",
+      "name": "reqSiteName",
+      "domain": "reqSiteDomain",
+      "cat": [
+        "books",
+        "novels"
+      ],
+      "search": "book search",
+      "publisher": {
+        "id": "1"
+      },
+      "content": {
+        "episode": 6,
+        "title": "episodeName",
+        "series": "TvName",
+        "season": "season3",
+        "len": 900,
+        "data": [
+          {
+            "id": "reqSiteData",
+            "name": "reqSiteName"
+          }
+        ]
+      }
+    },
+    "device": {
+      "ua": "testDevice",
+      "ip": "123.145.167.10",
+      "devicetype": 1,
+      "ifa": "123"
+    }
+  },
+  "bidderConfigFPD": {
+    "appnexus": {
+      "site": {
+        "id": "apnSiteId",
+        "name": "apnSiteName",
+        "domain": "apnSiteDomain",
+        "page": "http://www.foobar.com/1234.html",
+        "content": {
+          "episode": 7,
+          "title": "apnEpisodeName",
+          "len": 600,
+          "data": [
+            {
+              "id": "siteData3",
+              "name": "siteName3"
+            }
+          ]
+        },
+        "ext": {
+          "data": {
+            "morefpdData": "morefpddata",
+            "siteFpddata": "siteFpddata",
+            "moreFpd": {
+              "fpd": 123
+            }
+          }
+        }
+      }
+    }
+  },
+  "globalFPD": {
+    "site": {
+      "testSiteFpd": "testSite"
+    },
+    "siteContentData": [
+      {
+        "id": "siteData1",
+        "name": "siteName1"
+      },
+      {
+        "id": "siteData2",
+        "name": "siteName2"
+      }
+    ]
+  },
+  "outputRequestData": {
+    "site": {
+      "id": "reqSiteId",
+      "name": "apnSiteName",
+      "page": "http://www.foobar.com/1234.html",
+      "domain": "apnSiteDomain",
+      "cat": [
+        "books",
+        "novels"
+      ],
+      "search": "book search",
+      "publisher": {
+        "id": "1"
+      },
+      "content": {
+        "episode": 6,
+        "title": "episodeName",
+        "series": "TvName",
+        "season": "season3",
+        "len": 900,
+        "data": [
+          {
+            "id": "siteData1",
+            "name": "siteName1"
+          },
+          {
+            "id": "siteData2",
+            "name": "siteName2"
+          }
+        ]
+      },
+      "ext": {
+        "data": {
+          "id": "apnSiteId",
+          "content": {
+            "episode": 7,
+            "title": "apnEpisodeName",
+            "len": 600,
+            "data": [
+              {
+                "id": "siteData3",
+                "name": "siteName3"
+              }
+            ]
+          },
+          "morefpdData": "morefpddata",
+          "siteFpddata": "siteFpddata",
+          "testSiteFpd": "testSite",
+          "moreFpd": {
+            "fpd": 123
+          }
+        }
+      }
+    },
+    "device": {
+      "ua": "testDevice",
+      "ip": "123.145.167.10",
+      "devicetype": 1,
+      "ifa": "123"
+    }
+  }
+}
+
diff --git a/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-site-user.json b/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-site-user.json
new file mode 100644
index 00000000..fc65518d
--- /dev/null
+++ b/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-site-user.json
@@ -0,0 +1,96 @@
+{
+  "description": "Global FPD defined for user and bidder FPD defined for site and user",
+  "inputRequestData": {
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "publisher": {
+        "id": "1"
+      },
+      "content": {
+        "episode": 6,
+        "title": "episodeName",
+        "series": "TvName",
+        "season": "season3",
+        "len": 900
+      }
+    },
+    "user": {
+      "id": "apnUserId"
+    }
+  },
+  "bidderConfigFPD": {
+    "appnexus": {
+      "site": {
+        "id": "apnSiteId",
+        "ext": {
+          "data": {
+            "morefpdData": "morefpddata",
+            "siteFpddata": "siteFpddata",
+            "moreFpd": {
+              "fpd": 123
+            }
+          }
+        }
+      },
+      "user": {
+        "id": "apnUserId",
+        "ext": {
+          "data": {
+            "moreFpd": {
+              "fpd": 567
+            }
+          }
+        }
+      }
+    }
+  },
+  "globalFPD": {
+    "user": {
+      "testUserFpd": "testuser"
+    }
+  },
+  "outputRequestData": {
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "publisher": {
+        "id": "1"
+      },
+      "content": {
+        "episode": 6,
+        "title": "episodeName",
+        "series": "TvName",
+        "season": "season3",
+        "len": 900
+      },
+      "ext": {
+        "data": {
+          "ext": {
+            "data": {
+              "morefpdData": "morefpddata",
+              "siteFpddata": "siteFpddata",
+              "moreFpd": {
+                "fpd": 123
+              }
+            }
+          },
+          "id": "apnSiteId"
+        }
+      }
+    },
+    "user": {
+      "id": "apnUserId",
+      "ext": {
+        "data": {
+          "id": "apnUserId",
+          "moreFpd": {
+            "fpd": 567
+          },
+          "testUserFpd": "testuser"
+        }
+      }
+    }
+  }
+}
+
diff --git a/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-site.json b/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-site.json
new file mode 100644
index 00000000..f3d3fb74
--- /dev/null
+++ b/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-site.json
@@ -0,0 +1,71 @@
+{
+  "description": "Global and bidder FPD defined for site",
+  "inputRequestData": {
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "publisher": {
+        "id": "1"
+      },
+      "content": {
+        "episode": 6,
+        "title": "episodeName",
+        "series": "TvName",
+        "season": "season3",
+        "len": 900
+      }
+    }
+  },
+  "bidderConfigFPD": {
+    "appnexus": {
+      "site": {
+        "id": "apnSiteId",
+        "page": "http://www.foobar.com/1234.html",
+        "ref": "fpdRef",
+        "ext": {
+          "data": {
+            "morefpdData": "morefpddata",
+            "siteFpddata": "siteFpddata",
+            "moreFpd": {
+              "fpd": 123
+            }
+          }
+        }
+      }
+    }
+  },
+  "globalFPD": {
+    "site": {
+      "siteFpd": 123
+    }
+  },
+  "outputRequestData": {
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "ref": "fpdRef",
+      "publisher": {
+        "id": "1"
+      },
+      "content": {
+        "episode": 6,
+        "title": "episodeName",
+        "series": "TvName",
+        "season": "season3",
+        "len": 900
+      },
+      "ext": {
+        "data": {
+          "id": "apnSiteId",
+          "moreFpd": {
+            "fpd": 123
+          },
+          "morefpdData": "morefpddata",
+          "siteFpd": 123,
+          "siteFpddata": "siteFpddata"
+        }
+      }
+    }
+  }
+}
+
diff --git a/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-user.json b/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-user.json
new file mode 100644
index 00000000..61251865
--- /dev/null
+++ b/firstpartydata/tests/resolvefpd/global-and-bidder-fpd-user.json
@@ -0,0 +1,51 @@
+{
+  "description": "Global and bidder FPD defined for user",
+  "inputRequestData": {
+    "user": {
+      "id": "reqUserID",
+      "yob": 1982,
+      "gender": "M"
+    }
+  },
+  "bidderConfigFPD": {
+    "appnexus": {
+      "user": {
+        "id": "apnUserId",
+        "yob": 1982,
+        "ext": {
+          "data": {
+            "morefpdData": "morefpddata",
+            "userFpddata": "siteFpddata",
+            "moreFpd": {
+              "fpd": 123
+            }
+          }
+        }
+      }
+    }
+  },
+  "globalFPD": {
+    "user": {
+      "testUserFpd": "testuser"
+    }
+  },
+  "outputRequestData": {
+    "user": {
+      "id": "reqUserID",
+      "yob": 1982,
+      "gender": "M",
+      "ext": {
+        "data": {
+          "id": "apnUserId",
+          "testUserFpd": "testuser",
+          "morefpdData": "morefpddata",
+          "userFpddata": "siteFpddata",
+          "moreFpd": {
+            "fpd": 123
+          }
+        }
+      }
+    }
+  }
+}
+
diff --git a/firstpartydata/tests/resolvefpd/global-and-bidder-site-content-data.json b/firstpartydata/tests/resolvefpd/global-and-bidder-site-content-data.json
new file mode 100644
index 00000000..d0154d1b
--- /dev/null
+++ b/firstpartydata/tests/resolvefpd/global-and-bidder-site-content-data.json
@@ -0,0 +1,114 @@
+{
+  "description": "Global FPD defined for site, app, and user and bidder FPD defined for site. Global FPD has site.content.data",
+  "inputRequestData": {
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "publisher": {
+        "id": "1"
+      }
+    },
+    "device": {
+      "ua": "testDevice",
+      "ip": "123.145.167.10",
+      "devicetype": 1,
+      "ifa": "123"
+    },
+    "user": {
+      "id": "reqUserID",
+      "yob": 1982,
+      "gender": "M"
+    }
+  },
+  "bidderConfigFPD": {
+    "appnexus": {
+      "site": {
+        "id": "apnSiteId",
+        "page": "http://www.foobar.com/1234.html",
+        "ref": "fpdRef",
+        "ext": {
+          "data": {
+            "morefpdData": "morefpddata",
+            "siteFpddata": "siteFpddata",
+            "moreFpd": {
+              "fpd": 123
+            }
+          }
+        }
+      }
+    }
+  },
+  "globalFPD": {
+    "site": {
+      "siteFpd": 123
+    },
+    "app": {
+      "appFpd": {
+        "testValue": true
+      }
+    },
+    "user": {
+      "testUserFpd": "testuser"
+    },
+    "siteContentData": [
+      {
+        "id": "siteData1",
+        "name": "siteName1"
+      },
+      {
+        "id": "siteData2",
+        "name": "siteName2"
+      }
+    ]
+  },
+  "outputRequestData": {
+    "site": {
+      "id": "reqSiteId",
+      "page": "http://www.foobar.com/1234.html",
+      "ref": "fpdRef",
+      "publisher": {
+        "id": "1"
+      },
+      "content": {
+        "data": [
+          {
+            "id": "siteData1",
+            "name": "siteName1"
+          },
+          {
+            "id": "siteData2",
+            "name": "siteName2"
+          }
+        ]
+      },
+      "ext": {
+        "data": {
+          "id": "apnSiteId",
+          "moreFpd": {
+            "fpd": 123
+          },
+          "morefpdData": "morefpddata",
+          "siteFpd": 123,
+          "siteFpddata": "siteFpddata"
+        }
+      }
+    },
+    "device": {
+      "ua": "testDevice",
+      "ip": "123.145.167.10",
+      "devicetype": 1,
+      "ifa": "123"
+    },
+    "user": {
+      "id": "reqUserID",
+      "yob": 1982,
+      "gender": "M",
+      "ext": {
+        "data": {
+          "testUserFpd": "testuser"
+        }
+      }
+    }
+  }
+}
+
diff --git a/firstpartydata/tests/resolvefpd/req-app-not-defined.json b/firstpartydata/tests/resolvefpd/req-app-not-defined.json
new file mode 100644
index 00000000..f24a4d27
--- /dev/null
+++ b/firstpartydata/tests/resolvefpd/req-app-not-defined.json
@@ -0,0 +1,46 @@
+{
+  "description": "Incorrect bidder specific FPD: app not defined in request",
+  "inputRequestData": {
+    "site": {
+      "page": "http://www.foobar.com/1234.html",
+      "publisher": {
+        "id": "1"
+      },
+      "content": {
+        "episode": 6,
+        "title": "episodeName",
+        "series": "TvName",
+        "season": "season3",
+        "len": 900
+      }
+    },
+    "user": {
+      "id": "reqUserID",
+      "yob": 1982,
+      "gender": "M"
+    }
+  },
+  "bidderConfigFPD": {
+    "appnexus": {
+      "app": {
+        "page": "",
+        "ext": {
+          "data": {
+            "morefpdData": "morefpddata",
+            "siteFpddata": "siteFpddata",
+            "moreFpd": {
+              "fpd": 123
+            }
+          }
+        }
+      }
+    }
+  },
+  "outputRequestData": {},
+  "validationErrors": [
+    {
+      "Message": "incorrect First Party Data for bidder appnexus: App object is not defined in request, but defined in FPD config"
+    }
+  ]
+}
+
diff --git a/firstpartydata/tests/resolvefpd/req-site-not-defined.json b/firstpartydata/tests/resolvefpd/req-site-not-defined.json
new file mode 100644
index 00000000..d078ad78
--- /dev/null
+++ b/firstpartydata/tests/resolvefpd/req-site-not-defined.json
@@ -0,0 +1,33 @@
+{
+  "description": "Incorrect bidder specific FPD: site not defined in request",
+  "inputRequestData": {
+    "user": {
+      "id": "reqUserID",
+      "yob": 1982,
+      "gender": "M"
+    }
+  },
+  "bidderConfigFPD": {
+    "appnexus": {
+      "site": {
+        "page": "",
+        "ext": {
+          "data": {
+            "morefpdData": "morefpddata",
+            "siteFpddata": "siteFpddata",
+            "moreFpd": {
+              "fpd": 123
+            }
+          }
+        }
+      }
+    }
+  },
+  "outputRequestData": {},
+  "validationErrors": [
+    {
+      "Message": "incorrect First Party Data for bidder appnexus: Site object is not defined in request, but defined in FPD config"
+    }
+  ]
+}
+
diff --git a/firstpartydata/tests/resolvefpd/req-user-not-defined.json b/firstpartydata/tests/resolvefpd/req-user-not-defined.json
new file mode 100644
index 00000000..76ae3f82
--- /dev/null
+++ b/firstpartydata/tests/resolvefpd/req-user-not-defined.json
@@ -0,0 +1,31 @@
+{
+  "description": "Incorrect bidder specific FPD: user not defined in request",
+  "inputRequestData": {
+    "test": 1,
+    "at": 1,
+    "tmax": 5000
+  },
+  "bidderConfigFPD": {
+    "appnexus": {
+      "user": {
+        "keywords": "test user keywords",
+        "ext": {
+          "data": {
+            "morefpdData": "morefpddata",
+            "siteFpddata": "siteFpddata",
+            "moreFpd": {
+              "fpd": 123
+            }
+          }
+        }
+      }
+    }
+  },
+  "outputRequestData": {},
+  "validationErrors": [
+    {
+      "Message": "incorrect First Party Data for bidder appnexus: User object is not defined in request, but defined in FPD config"
+    }
+  ]
+}
+
diff --git a/firstpartydata/tests/resolvefpd/site-page-empty-conflict.json b/firstpartydata/tests/resolvefpd/site-page-empty-conflict.json
new file mode 100644
index 00000000..8da3dc69
--- /dev/null
+++ b/firstpartydata/tests/resolvefpd/site-page-empty-conflict.json
@@ -0,0 +1,45 @@
+{
+  "description": "Incorrect bidder specific FPD: site has no id and attempts to overwrite page to empty",
+  "inputRequestData": {
+    "site": {
+      "page": "http://www.foobar.com/1234.html",
+      "publisher": {
+        "id": "1"
+      }
+    },
+    "device": {
+      "ua": "testDevice",
+      "ip": "123.145.167.10",
+      "devicetype": 1,
+      "ifa": "123"
+    },
+    "user": {
+      "id": "reqUserID",
+      "yob": 1982,
+      "gender": "M"
+    }
+  },
+  "bidderConfigFPD": {
+    "appnexus": {
+      "site": {
+        "page": "",
+        "ext": {
+          "data": {
+            "morefpdData": "morefpddata",
+            "siteFpddata": "siteFpddata",
+            "moreFpd": {
+              "fpd": 123
+            }
+          }
+        }
+      }
+    }
+  },
+  "outputRequestData": {},
+  "validationErrors": [
+    {
+      "Message": "incorrect First Party Data for bidder appnexus: Site object cannot set empty page if req.site.id is empty"
+    }
+  ]
+}
+
diff --git a/util/jsonutil/jsonutil_test.go b/util/jsonutil/jsonutil_test.go
index d8738e17..12b1fd5e 100644
--- a/util/jsonutil/jsonutil_test.go
+++ b/util/jsonutil/jsonutil_test.go
@@ -7,300 +7,179 @@ import (
 )
 
 func TestDropElement(t *testing.T) {
+
 	tests := []struct {
 		description     string
 		input           []byte
-		elementToRemove string
+		elementToRemove []string
 		output          []byte
 		errorExpected   bool
 		errorContains   string
 	}{
 		{
-			description:     "Drop single element after another element",
+			description:     "Drop Single Element After Another Element",
 			input:           []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"test": 1,"consented_providers": [1608,765,492]}}`),
-			elementToRemove: "consented_providers",
+			elementToRemove: []string{"consented_providers"},
 			output:          []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"test": 1}}`),
 			errorExpected:   false,
 			errorContains:   "",
 		},
 		{
-			description:     "Drop single element before another element",
+			description:     "Drop Single Element Before Another Element",
 			input:           []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"consented_providers": [1608,765,492],"test": 1}}`),
-			elementToRemove: "consented_providers",
+			elementToRemove: []string{"consented_providers"},
 			output:          []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"test": 1}}`),
 			errorExpected:   false,
 			errorContains:   "",
 		},
 		{
-			description:     "Drop single element",
+			description:     "Drop Single Element",
 			input:           []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"consented_providers": [1545,2563,1411]}}`),
-			elementToRemove: "consented_providers",
+			elementToRemove: []string{"consented_providers"},
 			output:          []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {}}`),
 			errorExpected:   false,
 			errorContains:   "",
 		},
 		{
-			description:     "Drop single element string",
+			description:     "Drop Single Element string",
 			input:           []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"consented_providers": "test"}}`),
-			elementToRemove: "consented_providers",
+			elementToRemove: []string{"consented_providers"},
 			output:          []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {}}`),
 			errorExpected:   false,
 			errorContains:   "",
 		},
 		{
-			description:     "Drop parent element between two elements",
+			description:     "Drop Parent Element Between Two Elements",
 			input:           []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"consented_providers": [1608,765,492], "test": 1},"test": 123}`),
-			elementToRemove: "consented_providers_settings",
+			elementToRemove: []string{"consented_providers_settings"},
 			output:          []byte(`{"consent": "TESTCONSENT","test": 123}`),
 			errorExpected:   false,
 			errorContains:   "",
 		},
 		{
-			description:     "Drop parent element before element",
+			description:     "Drop Parent Element Before Element",
 			input:           []byte(`{"consented_providers_settings": {"consented_providers": [1608,765,492], "test": 1},"test": 123}`),
-			elementToRemove: "consented_providers_settings",
+			elementToRemove: []string{"consented_providers_settings"},
 			output:          []byte(`{"test": 123}`),
 			errorExpected:   false,
 			errorContains:   "",
 		},
 		{
-			description:     "Drop parent element after element",
+			description:     "Drop Parent Element After Element",
 			input:           []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"consented_providers": [1608,765,492], "test": 1}}`),
-			elementToRemove: "consented_providers_settings",
+			elementToRemove: []string{"consented_providers_settings"},
 			output:          []byte(`{"consent": "TESTCONSENT"}`),
 			errorExpected:   false,
 			errorContains:   "",
 		},
 		{
-			description:     "Drop parent element only",
+			description:     "Drop Parent Element Only",
 			input:           []byte(`{"consented_providers_settings": {"consented_providers": [1608,765,492], "test": 1}}`),
-			elementToRemove: "consented_providers_settings",
+			elementToRemove: []string{"consented_providers_settings"},
 			output:          []byte(`{}`),
 			errorExpected:   false,
 			errorContains:   "",
 		},
 		{
-			description:     "Drop element that doesn't exist",
-			input:           []byte(`{"consented_providers_settings": {"consented_providers": [1608,765,492], "test": 1}}`),
-			elementToRemove: "test2",
-			output:          []byte(`{"consented_providers_settings": {"consented_providers": [1608,765,492], "test": 1}}`),
+			description:     "Drop Parent Element List",
+			input:           []byte(`{"consented_providers_settings":{"consented_providers":[1608,765,492],"test":1},"data": [{"test1":5},{"test2": [1,2,3]}]}`),
+			elementToRemove: []string{"data"},
+			output:          []byte(`{"consented_providers_settings":{"consented_providers":[1608,765,492],"test":1}}`),
 			errorExpected:   false,
 			errorContains:   "",
 		},
-		//Errors
 		{
-			description:     "Error decode",
-			input:           []byte(`{"consented_providers_settings": {"consented_providers": ["123",1,,1365,5678,1545,2563,1411], "test": 1}}`),
-			elementToRemove: "consented_providers",
-			output:          []byte(``),
-			errorExpected:   true,
-			errorContains:   "looking for beginning of value",
-		},
-		{
-			description:     "Error malformed",
-			input:           []byte(`{consented_providers_settings: {"consented_providers": [1365,5678,1545,2563,1411], "test": 1}}`),
-			elementToRemove: "consented_providers",
-			output:          []byte(``),
-			errorExpected:   true,
-			errorContains:   "invalid character",
-		},
-	}
-
-	for _, tt := range tests {
-		res, err := DropElement(tt.input, tt.elementToRemove)
-
-		if tt.errorExpected {
-			assert.Error(t, err, "Error should not be nil")
-			assert.True(t, strings.Contains(err.Error(), tt.errorContains))
-		} else {
-			assert.NoError(t, err, "Error should be nil")
-			assert.Equal(t, tt.output, res, "Result is incorrect")
-		}
-
-	}
-}
-
-func TestFindElement(t *testing.T) {
-	tests := []struct {
-		description   string
-		input         []byte
-		elementToFind string
-		output        []byte
-		elementFound  bool
-	}{
-		{
-			description:   "Find array element",
-			input:         []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"test": 1,"consented_providers":[1608,765,492]}}`),
-			elementToFind: "consented_providers",
-			output:        []byte(`[1608,765,492]`),
-			elementFound:  true,
-		},
-		{
-			description:   "Find object element",
-			input:         []byte(`{"consent": "TESTCONSENT","consented_providers_settings":{"test": 1,"consented_providers": [1608,765,492]}}`),
-			elementToFind: "consented_providers_settings",
-			output:        []byte(`{"test": 1,"consented_providers": [1608,765,492]}`),
-			elementFound:  true,
-		},
-		{
-			description:   "Find element that doesn't exist",
-			input:         []byte(`{"consent": "TESTCONSENT","consented_providers_settings":{"test": 1,"consented_providers": [1608,765,492]}}`),
-			elementToFind: "test_element",
-			output:        []byte(nil),
-			elementFound:  false,
-		},
-	}
-
-	for _, tt := range tests {
-		exists, res, err := FindElement(tt.input, tt.elementToFind)
-		assert.NoError(t, err, "Error should be nil")
-		assert.Equal(t, tt.output, res, "Result is incorrect")
-
-		if tt.elementFound {
-			assert.True(t, exists, "Element must be found")
-		} else {
-			assert.False(t, exists, "Element must not be found")
-		}
-	}
-}
-
-func TestFindElementIndexes(t *testing.T) {
-	tests := []struct {
-		description   string
-		input         []byte
-		elementToFind string
-		startIndex    int64
-		endIndex      int64
-		found         bool
-		errorExpected bool
-		errorContains string
-	}{
-		{
-			description:   "Find single element after another element",
-			input:         []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"test": 1,"consented_providers": [1608,765,492]}}`),
-			elementToFind: "consented_providers",
-			startIndex:    68,
-			endIndex:      106,
-			found:         true,
-			errorExpected: false,
-			errorContains: "",
-		},
-		{
-			description:   "Find single element before another element",
-			input:         []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"consented_providers": [1608,765,492],"test": 1}}`),
-			elementToFind: "consented_providers",
-			startIndex:    59,
-			endIndex:      97,
-			found:         true,
-			errorExpected: false,
-			errorContains: "",
+			description:     "Drop Element That Doesn't Exist",
+			input:           []byte(`{"consented_providers_settings": {"consented_providers": [1608,765,492], "test": 1}}`),
+			elementToRemove: []string{"test2"},
+			output:          []byte(`{"consented_providers_settings": {"consented_providers": [1608,765,492], "test": 1}}`),
+			errorExpected:   false,
+			errorContains:   "",
 		},
 		{
-			description:   "Find single element",
-			input:         []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"consented_providers": [1545,2563,1411]}}`),
-			elementToFind: "consented_providers",
-			startIndex:    59,
-			endIndex:      98,
-			found:         true,
-			errorExpected: false,
-			errorContains: "",
+			description:     "Drop Nested Element Single Occurrence",
+			input:           []byte(`{"consented_providers_settings":{"consented_providers":[1608,765,492],"test":1},"data": [{"test1":5},{"test2": [1,2,3]}]}`),
+			elementToRemove: []string{"consented_providers_settings", "test"},
+			output:          []byte(`{"consented_providers_settings":{"consented_providers":[1608,765,492]},"data": [{"test1":5},{"test2": [1,2,3]}]}`),
+			errorExpected:   false,
+			errorContains:   "",
 		},
 		{
-			description:   "Find single element string",
-			input:         []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"consented_providers": "test"}}`),
-			elementToFind: "consented_providers",
-			startIndex:    59,
-			endIndex:      88,
-			found:         true,
-			errorExpected: false,
-			errorContains: "",
+			description:     "Drop Nested Element Multiple Occurrence",
+			input:           []byte(`{"consented_providers_settings":{"consented_providers":[1608,765,492],"test":1},"data": [{"test":5},{"test": [1,2,3]}]}`),
+			elementToRemove: []string{"consented_providers_settings", "test"},
+			output:          []byte(`{"consented_providers_settings":{"consented_providers":[1608,765,492]},"data": [{"test":5},{"test": [1,2,3]}]}`),
+			errorExpected:   false,
+			errorContains:   "",
 		},
 		{
-			description:   "Find parent element between two elements",
-			input:         []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"consented_providers": [1608,765,492], "test": 1},"test": 123}`),
-			elementToFind: "consented_providers_settings",
-			startIndex:    26,
-			endIndex:      109,
-			found:         true,
-			errorExpected: false,
-			errorContains: "",
+			description:     "Drop Nested Element Multiple Occurrence Skip Path",
+			input:           []byte(`{"consented_providers_settings":{"consented_providers":[1608,765,492],"data": {"amp":1, "test": 25}}}`),
+			elementToRemove: []string{"consented_providers_settings", "test"},
+			output:          []byte(`{"consented_providers_settings":{"consented_providers":[1608,765,492],"data": {"amp":1}}}`),
+			errorExpected:   false,
+			errorContains:   "",
 		},
 		{
-			description:   "Find parent element before element",
-			input:         []byte(`{"consented_providers_settings": {"consented_providers": [1608,765,492], "test": 1},"test": 123}`),
-			elementToFind: "consented_providers_settings",
-			startIndex:    1,
-			endIndex:      84,
-			found:         true,
-			errorExpected: false,
-			errorContains: "",
+			description:     "Drop Nested Structure Single Occurrence",
+			input:           []byte(`{"consented_providers":{"providers":[1608,765,492],"test":{"nested":true}},"data": [{"test":5},{"test": [1,2,3]}]}`),
+			elementToRemove: []string{"consented_providers", "test"},
+			output:          []byte(`{"consented_providers":{"providers":[1608,765,492]},"data": [{"test":5},{"test": [1,2,3]}]}`),
+			errorExpected:   false,
+			errorContains:   "",
 		},
 		{
-			description:   "Find parent element after element",
-			input:         []byte(`{"consent": "TESTCONSENT","consented_providers_settings": {"consented_providers": [1608,765,492], "test": 1}}`),
-			elementToFind: "consented_providers_settings",
-			startIndex:    25,
-			endIndex:      108,
-			found:         true,
-			errorExpected: false,
-			errorContains: "",
+			description:     "Drop Nested Structure Single Occurrence Deep Nested",
+			input:           []byte(`{"consented_providers":{"providers":[1608,765,492],"test":{"nested":true, "nested2": {"test6": 123}}},"data": [{"test":5},{"test": [1,2,3]}]}`),
+			elementToRemove: []string{"consented_providers", "test6"},
+			output:          []byte(`{"consented_providers":{"providers":[1608,765,492],"test":{"nested":true, "nested2": {}}},"data": [{"test":5},{"test": [1,2,3]}]}`),
+			errorExpected:   false,
+			errorContains:   "",
 		},
 		{
-			description:   "Find parent element only",
-			input:         []byte(`{"consented_providers_settings": {"consented_providers": [1608,765,492], "test": 1}}`),
-			elementToFind: "consented_providers_settings",
-			startIndex:    1,
-			endIndex:      83,
-			found:         true,
-			errorExpected: false,
-			errorContains: "",
+			description:     "Drop Nested Structure Single Occurrence Deep Nested Full Path",
+			input:           []byte(`{"consented_providers":{"providers":[1608,765,492],"test":{"nested":true,"nested2": {"test6": 123}}},"data": [{"test":5},{"test": [1,2,3]}]}`),
+			elementToRemove: []string{"consented_providers", "test", "nested"},
+			output:          []byte(`{"consented_providers":{"providers":[1608,765,492],"test":{"nested2": {"test6": 123}}},"data": [{"test":5},{"test": [1,2,3]}]}`),
+			errorExpected:   false,
+			errorContains:   "",
 		},
 		{
-			description:   "Find element that doesn't exist",
-			input:         []byte(`{"consented_providers_settings": {"consented_providers": [1608,765,492], "test": 1}}`),
-			elementToFind: "test2",
-			startIndex:    -1,
-			endIndex:      -1,
-			found:         false,
-			errorExpected: false,
-			errorContains: "",
+			description:     "Drop Nested Structure Doesn't Exist",
+			input:           []byte(`{"consented_providers":{"providers":[1608,765,492]},"test":{"nested":true}},"data": [{"test":5},{"test": [1,2,3]}]}`),
+			elementToRemove: []string{"consented_providers", "test2"},
+			output:          []byte(`{"consented_providers":{"providers":[1608,765,492]},"test":{"nested":true}},"data": [{"test":5},{"test": [1,2,3]}]}`),
+			errorExpected:   false,
+			errorContains:   "",
 		},
 		//Errors
 		{
-			description:   "Error decode",
-			input:         []byte(`{"consented_providers_settings": {"consented_providers": ["123",1,,1365,5678,1545,2563,1411], "test": 1}}`),
-			elementToFind: "consented_providers",
-			startIndex:    -1,
-			endIndex:      -1,
-			found:         false,
-			errorExpected: true,
-			errorContains: "looking for beginning of value",
+			description:     "Error Decode",
+			input:           []byte(`{"consented_providers_settings": {"consented_providers": ["123",1,,1365,5678,1545,2563,1411], "test": 1}}`),
+			elementToRemove: []string{"consented_providers"},
+			output:          []byte(``),
+			errorExpected:   true,
+			errorContains:   "looking for beginning of value",
 		},
 		{
-			description:   "Error malformed",
-			input:         []byte(`{consented_providers_settings: {"consented_providers": [1365,5678,1545,2563,1411], "test": 1}}`),
-			elementToFind: "consented_providers",
-			startIndex:    -1,
-			endIndex:      -1,
-			found:         false,
-			errorExpected: true,
-			errorContains: "invalid character",
+			description:     "Error Malformed",
+			input:           []byte(`{consented_providers_settings: {"consented_providers": [1365,5678,1545,2563,1411], "test": 1}}`),
+			elementToRemove: []string{"consented_providers"},
+			output:          []byte(``),
+			errorExpected:   true,
+			errorContains:   "invalid character",
 		},
 	}
 
 	for _, tt := range tests {
-		found, start, end, err := findElementIndexes(tt.input, tt.elementToFind)
-
-		assert.Equal(t, tt.found, found, "Incorrect value of element existence")
+		res, err := DropElement(tt.input, tt.elementToRemove...)
 
 		if tt.errorExpected {
 			assert.Error(t, err, "Error should not be nil")
 			assert.True(t, strings.Contains(err.Error(), tt.errorContains))
 		} else {
 			assert.NoError(t, err, "Error should be nil")
-			assert.Equal(t, tt.startIndex, start, "Result is incorrect")
-			assert.Equal(t, tt.endIndex, end, "Result is incorrect")
+			assert.Equal(t, tt.output, res, "Result is incorrect")
 		}
-
 	}
 }