Repository: prebid/prebid-server
Pull request #2067 (2021-11-01 16:38:47)

New Adapter: Streamkey (Alias to Adtelligent)
Ads streamkey bidder as alias of adtelligent
Doc update: https://github.com/prebid/prebid.github.io/pull/3383


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-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")
 		}
-
 	}
 }