Repository: prebid/prebid-server
Pull request #2918 (2023-07-11 19:37:19)

Mobilefuse: Added support for skadn. Forwarding imp.ext.skadn
This is a change to forward imp.ext.skadn whereas previously anything under imp.ext was not forwarded.


Patch:
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index f4ea099d..ebf6f614 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -6,7 +6,7 @@
 		"dockerfile": "Dockerfile",
 		"args": {
 			// Update the VARIANT arg to pick a version of Go
-			"VARIANT": "1.19",
+			"VARIANT": "1.20",
 			// Options
 			"INSTALL_NODE": "false",
 			"NODE_VERSION": "lts/*"
diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml
index 87c5573e..565bb9b8 100644
--- a/.github/workflows/semgrep.yml
+++ b/.github/workflows/semgrep.yml
@@ -1,8 +1,9 @@
 name: Adapter semgrep checks
 on:
-  pull_request:
+  pull_request_target:
     paths: ["adapters/*/*.go"]
-permissions: write-all
+permissions: 
+    pull-requests: write
 jobs:
   semgrep-check:
     runs-on: ubuntu-latest
@@ -11,6 +12,8 @@ jobs:
         uses: actions/checkout@v3
         with:
           fetch-depth: 0
+          ref: ${{github.event.pull_request.head.ref}}
+          repository: ${{github.event.pull_request.head.repo.full_name}}
 
       - name: Calculate diff
         id: calculate_diff
diff --git a/.github/workflows/validate-merge.yml b/.github/workflows/validate-merge.yml
index e85dc3de..07f1baca 100644
--- a/.github/workflows/validate-merge.yml
+++ b/.github/workflows/validate-merge.yml
@@ -12,7 +12,7 @@ jobs:
     - name: Install Go
       uses: actions/setup-go@v4
       with:
-        go-version: 1.19.2
+        go-version: 1.20.5
 
     - name: Checkout Merged Branch
       uses: actions/checkout@v3
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index 83b29709..9047e1f4 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -10,7 +10,7 @@ jobs:
   validate:
     strategy:
       matrix:
-        go-version: [1.18.x, 1.19.x]
+        go-version: [1.19.x, 1.20.x]
         os: [ubuntu-20.04]
     runs-on: ${{ matrix.os }}
     
diff --git a/Dockerfile b/Dockerfile
index 80269c90..090d9a79 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -3,8 +3,8 @@ RUN apt-get update && \
     apt-get -y upgrade && \
     apt-get install -y wget
 WORKDIR /tmp
-RUN wget https://dl.google.com/go/go1.19.2.linux-amd64.tar.gz && \
-    tar -xf go1.19.2.linux-amd64.tar.gz && \
+RUN wget https://dl.google.com/go/go1.20.5.linux-amd64.tar.gz && \
+    tar -xf go1.20.5.linux-amd64.tar.gz && \
     mv go /usr/local
 RUN mkdir -p /app/prebid-server/
 WORKDIR /app/prebid-server/
diff --git a/README.md b/README.md
index 95e0262b..9b657f35 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ Please consider [registering your Prebid Server](https://docs.prebid.org/prebid-
 
 ## Installation
 
-First install [Go](https://golang.org/doc/install) version 1.18 or newer.
+First install [Go](https://golang.org/doc/install) version 1.19 or newer.
 
 Note that prebid-server is using [Go modules](https://blog.golang.org/using-go-modules).
 We officially support the most recent two major versions of the Go runtime. However, if you'd like to use a version <1.13 and are inside GOPATH `GO111MODULE` needs to be set to `GO111MODULE=on`.
diff --git a/adapters/bidder.go b/adapters/bidder.go
index 28fb555d..6e163057 100644
--- a/adapters/bidder.go
+++ b/adapters/bidder.go
@@ -149,12 +149,6 @@ type ExtraRequestInfo struct {
 	BidderRequestStartTime     time.Time
 	GlobalPrivacyControlHeader string
 	CurrencyConversions        currency.Conversions
-	MakeBidsTimeInfo           MakeBidsTimeInfo
-}
-
-type MakeBidsTimeInfo struct {
-	AfterMakeBidsStartTime time.Time
-	Durations              []time.Duration
 }
 
 func NewExtraRequestInfo(c currency.Conversions) ExtraRequestInfo {
diff --git a/adapters/gothamads/gothamads.go b/adapters/gothamads/gothamads.go
new file mode 100644
index 00000000..e486585c
--- /dev/null
+++ b/adapters/gothamads/gothamads.go
@@ -0,0 +1,170 @@
+package gothamads
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"text/template"
+
+	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/macros"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+type adapter struct {
+	endpoint *template.Template
+}
+
+func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
+	template, err := template.New("endpointTemplate").Parse(config.Endpoint)
+	if err != nil {
+		return nil, fmt.Errorf("unable to parse endpoint url template: %v", err)
+	}
+
+	bidder := &adapter{
+		endpoint: template,
+	}
+	return bidder, nil
+}
+
+func getHeaders(request *openrtb2.BidRequest) http.Header {
+	headers := http.Header{}
+	headers.Add("Content-Type", "application/json;charset=utf-8")
+	headers.Add("Accept", "application/json")
+	headers.Add("X-Openrtb-Version", "2.5")
+
+	if request.Device != nil {
+		if len(request.Device.UA) > 0 {
+			headers.Add("User-Agent", request.Device.UA)
+		}
+
+		if len(request.Device.IPv6) > 0 {
+			headers.Add("X-Forwarded-For", request.Device.IPv6)
+		}
+
+		if len(request.Device.IP) > 0 {
+			headers.Add("X-Forwarded-For", request.Device.IP)
+		}
+	}
+
+	return headers
+}
+
+func (a *adapter) MakeRequests(openRTBRequest *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) (requestsToBidder []*adapters.RequestData, errs []error) {
+	impExt, err := getImpressionExt(&openRTBRequest.Imp[0])
+	if err != nil {
+		return nil, []error{err}
+	}
+
+	openRTBRequest.Imp[0].Ext = nil
+
+	url, err := a.buildEndpointURL(impExt)
+	if err != nil {
+		return nil, []error{err}
+	}
+
+	reqJSON, err := json.Marshal(openRTBRequest)
+	if err != nil {
+		return nil, []error{err}
+	}
+
+	return []*adapters.RequestData{{
+		Method:  http.MethodPost,
+		Body:    reqJSON,
+		Uri:     url,
+		Headers: getHeaders(openRTBRequest),
+	}}, nil
+}
+
+func getImpressionExt(imp *openrtb2.Imp) (*openrtb_ext.ExtGothamAds, error) {
+	var bidderExt adapters.ExtImpBidder
+	if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
+		return nil, &errortypes.BadInput{
+			Message: err.Error(),
+		}
+	}
+	var gothamadsExt openrtb_ext.ExtGothamAds
+	if err := json.Unmarshal(bidderExt.Bidder, &gothamadsExt); err != nil {
+		return nil, &errortypes.BadInput{
+			Message: err.Error(),
+		}
+	}
+
+	return &gothamadsExt, nil
+}
+
+func (a *adapter) buildEndpointURL(params *openrtb_ext.ExtGothamAds) (string, error) {
+	endpointParams := macros.EndpointTemplateParams{AccountID: params.AccountID}
+	return macros.ResolveMacros(a.endpoint, endpointParams)
+}
+
+func checkResponseStatusCodes(response *adapters.ResponseData) error {
+	if response.StatusCode == http.StatusServiceUnavailable {
+		return &errortypes.BadServerResponse{
+			Message: fmt.Sprintf("Something went wrong Status Code: [ %d ] ", response.StatusCode),
+		}
+	}
+
+	return adapters.CheckResponseStatusCodeForErrors(response)
+}
+
+func (a *adapter) MakeBids(openRTBRequest *openrtb2.BidRequest, requestToBidder *adapters.RequestData, bidderRawResponse *adapters.ResponseData) (bidderResponse *adapters.BidderResponse, errs []error) {
+	if adapters.IsResponseStatusCodeNoContent(bidderRawResponse) {
+		return nil, nil
+	}
+
+	httpStatusError := checkResponseStatusCodes(bidderRawResponse)
+	if httpStatusError != nil {
+		return nil, []error{httpStatusError}
+	}
+
+	responseBody := bidderRawResponse.Body
+	var bidResp openrtb2.BidResponse
+	if err := json.Unmarshal(responseBody, &bidResp); err != nil {
+		return nil, []error{&errortypes.BadServerResponse{
+			Message: "Bad Server Response",
+		}}
+	}
+
+	if len(bidResp.SeatBid) == 0 {
+		return nil, []error{&errortypes.BadServerResponse{
+			Message: "Empty SeatBid array",
+		}}
+	}
+
+	bidResponse := adapters.NewBidderResponseWithBidsCapacity(5)
+	var bidsArray []*adapters.TypedBid
+
+	for _, sb := range bidResp.SeatBid {
+		for idx, bid := range sb.Bid {
+			bidType, err := getMediaTypeForImp(bid)
+			if err != nil {
+				return nil, []error{err}
+			}
+
+			bidsArray = append(bidsArray, &adapters.TypedBid{
+				Bid:     &sb.Bid[idx],
+				BidType: bidType,
+			})
+		}
+	}
+
+	bidResponse.Bids = bidsArray
+	return bidResponse, nil
+}
+
+func getMediaTypeForImp(bid openrtb2.Bid) (openrtb_ext.BidType, error) {
+	switch bid.MType {
+	case openrtb2.MarkupBanner:
+		return openrtb_ext.BidTypeBanner, nil
+	case openrtb2.MarkupVideo:
+		return openrtb_ext.BidTypeVideo, nil
+	case openrtb2.MarkupNative:
+		return openrtb_ext.BidTypeNative, nil
+	default:
+		return "", fmt.Errorf("unsupported MType %d", bid.MType)
+	}
+}
diff --git a/adapters/mobilefuse/mobilefuse.go b/adapters/mobilefuse/mobilefuse.go
index f46fc591..07ecd61b 100644
--- a/adapters/mobilefuse/mobilefuse.go
+++ b/adapters/mobilefuse/mobilefuse.go
@@ -27,6 +27,10 @@ type BidExt struct {
 	Mf ExtMf `json:"mf"`
 }
 
+type ExtSkadn struct {
+	Skadn json.RawMessage `json:"skadn"`
+}
+
 // Builder builds a new instance of the MobileFuse adapter for the given bidder with the given config.
 func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
 	template, err := template.New("endpointTemplate").Parse(config.Endpoint)
@@ -79,7 +83,7 @@ func (adapter *MobileFuseAdapter) MakeBids(incomingRequest *openrtb2.BidRequest,
 
 	for _, seatbid := range incomingBidResponse.SeatBid {
 		for i := range seatbid.Bid {
-			bidType := adapter.getBidType(seatbid.Bid[i])
+			bidType := getBidType(seatbid.Bid[i])
 			seatbid.Bid[i].Ext = nil
 
 			outgoingBidResponse.Bids = append(outgoingBidResponse.Bids, &adapters.TypedBid{
@@ -95,7 +99,7 @@ func (adapter *MobileFuseAdapter) MakeBids(incomingRequest *openrtb2.BidRequest,
 func (adapter *MobileFuseAdapter) makeRequest(bidRequest *openrtb2.BidRequest) (*adapters.RequestData, []error) {
 	var errs []error
 
-	mobileFuseExtension, errs := adapter.getFirstMobileFuseExtension(bidRequest)
+	mobileFuseExtension, errs := getFirstMobileFuseExtension(bidRequest)
 
 	if errs != nil {
 		return nil, errs
@@ -107,10 +111,9 @@ func (adapter *MobileFuseAdapter) makeRequest(bidRequest *openrtb2.BidRequest) (
 		return nil, append(errs, err)
 	}
 
-	validImps := adapter.getValidImps(bidRequest, mobileFuseExtension)
+	validImps, err := getValidImps(bidRequest, mobileFuseExtension)
 
-	if len(validImps) == 0 {
-		err := fmt.Errorf("No valid imps")
+	if err != nil {
 		errs = append(errs, err)
 		return nil, errs
 	}
@@ -135,7 +138,7 @@ func (adapter *MobileFuseAdapter) makeRequest(bidRequest *openrtb2.BidRequest) (
 	}, errs
 }
 
-func (adapter *MobileFuseAdapter) getFirstMobileFuseExtension(request *openrtb2.BidRequest) (*openrtb_ext.ExtImpMobileFuse, []error) {
+func getFirstMobileFuseExtension(request *openrtb2.BidRequest) (*openrtb_ext.ExtImpMobileFuse, []error) {
 	var mobileFuseImpExtension openrtb_ext.ExtImpMobileFuse
 	var errs []error
 
@@ -178,23 +181,42 @@ func (adapter *MobileFuseAdapter) getEndpoint(ext *openrtb_ext.ExtImpMobileFuse)
 	return url, nil
 }
 
-func (adapter *MobileFuseAdapter) getValidImps(bidRequest *openrtb2.BidRequest, ext *openrtb_ext.ExtImpMobileFuse) []openrtb2.Imp {
+func getValidImps(bidRequest *openrtb2.BidRequest, ext *openrtb_ext.ExtImpMobileFuse) ([]openrtb2.Imp, error) {
 	var validImps []openrtb2.Imp
 
 	for _, imp := range bidRequest.Imp {
 		if imp.Banner != nil || imp.Video != nil || imp.Native != nil {
 			imp.TagID = strconv.Itoa(ext.PlacementId)
-			imp.Ext = nil
+
+			var extSkadn ExtSkadn
+			err := json.Unmarshal(imp.Ext, &extSkadn)
+			if err != nil {
+				return nil, err
+			}
+
+			if extSkadn.Skadn != nil {
+				imp.Ext, err = json.Marshal(map[string]json.RawMessage{"skadn": extSkadn.Skadn})
+				if err != nil {
+					return nil, err
+				}
+			} else {
+				imp.Ext = nil
+			}
+
 			validImps = append(validImps, imp)
 
 			break
 		}
 	}
 
-	return validImps
+	if len(validImps) == 0 {
+		return nil, fmt.Errorf("No valid imps")
+	}
+
+	return validImps, nil
 }
 
-func (adapter *MobileFuseAdapter) getBidType(bid openrtb2.Bid) openrtb_ext.BidType {
+func getBidType(bid openrtb2.Bid) openrtb_ext.BidType {
 	if bid.Ext != nil {
 		var bidExt BidExt
 		err := json.Unmarshal(bid.Ext, &bidExt)
diff --git a/endpoints/cookie_sync.go b/endpoints/cookie_sync.go
index fa154bbc..ef3c40e8 100644
--- a/endpoints/cookie_sync.go
+++ b/endpoints/cookie_sync.go
@@ -90,8 +90,10 @@ func (c *cookieSyncEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ ht
 		c.handleError(w, err, http.StatusBadRequest)
 		return
 	}
+	decoder := usersync.Base64Decoder{}
 
-	cookie := usersync.ParseCookieFromRequest(r, &c.config.HostCookie)
+	cookie := usersync.ReadCookie(r, decoder, &c.config.HostCookie)
+	usersync.SyncHostCookie(r, cookie, &c.config.HostCookie)
 
 	result := c.chooser.Choose(request, cookie)
 	switch result.Status {
diff --git a/endpoints/getuids.go b/endpoints/getuids.go
index ad984a8d..f420c64f 100644
--- a/endpoints/getuids.go
+++ b/endpoints/getuids.go
@@ -18,9 +18,11 @@ type userSyncs struct {
 // returns all the existing syncs for the user
 func NewGetUIDsEndpoint(cfg config.HostCookie) httprouter.Handle {
 	return httprouter.Handle(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
-		pc := usersync.ParseCookieFromRequest(r, &cfg)
+		cookie := usersync.ReadCookie(r, usersync.Base64Decoder{}, &cfg)
+		usersync.SyncHostCookie(r, cookie, &cfg)
+
 		userSyncs := new(userSyncs)
-		userSyncs.BuyerUIDs = pc.GetUIDs()
+		userSyncs.BuyerUIDs = cookie.GetUIDs()
 		json.NewEncoder(w).Encode(userSyncs)
 	})
 }
diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go
index 8d328b7c..9324b15d 100644
--- a/endpoints/openrtb2/amp_auction.go
+++ b/endpoints/openrtb2/amp_auction.go
@@ -182,12 +182,15 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
 	}
 	defer cancel()
 
-	usersyncs := usersync.ParseCookieFromRequest(r, &(deps.cfg.HostCookie))
+	// Read UserSyncs/Cookie from Request
+	usersyncs := usersync.ReadCookie(r, usersync.Base64Decoder{}, &deps.cfg.HostCookie)
+	usersync.SyncHostCookie(r, usersyncs, &deps.cfg.HostCookie)
 	if usersyncs.HasAnyLiveSyncs() {
 		labels.CookieFlag = metrics.CookieFlagYes
 	} else {
 		labels.CookieFlag = metrics.CookieFlagNo
 	}
+
 	labels.PubID = getAccountID(reqWrapper.Site.Publisher)
 	// Look up account now that we have resolved the pubID value
 	account, acctIDErrs := accountService.GetAccount(ctx, deps.cfg, deps.accounts, labels.PubID, deps.metricsEngine)
@@ -284,9 +287,6 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
 	}
 
 	labels, ao = sendAmpResponse(w, hookExecutor, auctionResponse, reqWrapper, account, labels, ao, errL)
-	if len(ao.Errors) == 0 {
-		recordResponsePreparationMetrics(auctionRequest.MakeBidsTimeInfo, deps.metricsEngine)
-	}
 }
 
 func rejectAmpRequest(
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index 3f01967a..9a0d0d07 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -27,7 +27,6 @@ import (
 	nativeRequests "github.com/prebid/openrtb/v19/native1/request"
 	"github.com/prebid/openrtb/v19/openrtb2"
 	"github.com/prebid/openrtb/v19/openrtb3"
-	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/bidadjustment"
 	"github.com/prebid/prebid-server/hooks"
 	"github.com/prebid/prebid-server/ortb"
@@ -210,7 +209,11 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 		defer cancel()
 	}
 
-	usersyncs := usersync.ParseCookieFromRequest(r, &(deps.cfg.HostCookie))
+	// Read Usersyncs/Cookie
+	decoder := usersync.Base64Decoder{}
+	usersyncs := usersync.ReadCookie(r, decoder, &deps.cfg.HostCookie)
+	usersync.SyncHostCookie(r, usersyncs, &deps.cfg.HostCookie)
+
 	if req.Site != nil {
 		if usersyncs.HasAnyLiveSyncs() {
 			labels.CookieFlag = metrics.CookieFlagYes
@@ -280,9 +283,6 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 		glog.Errorf("Error setting seat non-bid: %v", err)
 	}
 	labels, ao = sendAuctionResponse(w, hookExecutor, response, req.BidRequest, account, labels, ao)
-	if len(ao.Errors) == 0 {
-		recordResponsePreparationMetrics(auctionRequest.MakeBidsTimeInfo, deps.metricsEngine)
-	}
 }
 
 // setSeatNonBidRaw is transitional function for setting SeatNonBid inside bidResponse.Ext
@@ -2446,13 +2446,3 @@ func validateStoredBidRespAndImpExtBidders(bidderExts map[string]json.RawMessage
 func generateStoredBidResponseValidationError(impID string) error {
 	return fmt.Errorf("request validation failed. Stored bid responses are specified for imp %s. Bidders specified in imp.ext should match with bidders specified in imp.ext.prebid.storedbidresponse", impID)
 }
-
-func recordResponsePreparationMetrics(mbti map[openrtb_ext.BidderName]adapters.MakeBidsTimeInfo, me metrics.MetricsEngine) {
-	for _, info := range mbti {
-		duration := time.Since(info.AfterMakeBidsStartTime)
-		for _, makeBidsDuration := range info.Durations {
-			duration += makeBidsDuration
-		}
-		me.RecordOverheadTime(metrics.MakeAuctionResponse, duration)
-	}
-}
diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go
index 9401f4da..872ea2bd 100644
--- a/endpoints/openrtb2/video_auction.go
+++ b/endpoints/openrtb2/video_auction.go
@@ -275,7 +275,11 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 		defer cancel()
 	}
 
-	usersyncs := usersync.ParseCookieFromRequest(r, &(deps.cfg.HostCookie))
+	// Read Usersyncs/Cookie
+	decoder := usersync.Base64Decoder{}
+	usersyncs := usersync.ReadCookie(r, decoder, &deps.cfg.HostCookie)
+	usersync.SyncHostCookie(r, usersyncs, &deps.cfg.HostCookie)
+
 	if bidReqWrapper.App != nil {
 		labels.Source = metrics.DemandApp
 		labels.PubID = getAccountID(bidReqWrapper.App.Publisher)
@@ -363,10 +367,6 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 		return
 	}
 
-	if len(vo.Errors) == 0 {
-		recordResponsePreparationMetrics(auctionRequest.MakeBidsTimeInfo, deps.metricsEngine)
-	}
-
 	w.Header().Set("Content-Type", "application/json")
 	w.Write(resp)
 
diff --git a/endpoints/setuid.go b/endpoints/setuid.go
index a4d04749..905fa26b 100644
--- a/endpoints/setuid.go
+++ b/endpoints/setuid.go
@@ -3,21 +3,26 @@ package endpoints
 import (
 	"context"
 	"errors"
+	"fmt"
 	"net/http"
 	"net/url"
 	"strconv"
 	"strings"
-	"time"
 
 	"github.com/julienschmidt/httprouter"
+	gpplib "github.com/prebid/go-gpp"
+	gppConstants "github.com/prebid/go-gpp/constants"
 	accountService "github.com/prebid/prebid-server/account"
 	"github.com/prebid/prebid-server/analytics"
 	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/gdpr"
 	"github.com/prebid/prebid-server/metrics"
+	gppPrivacy "github.com/prebid/prebid-server/privacy/gpp"
 	"github.com/prebid/prebid-server/stored_requests"
 	"github.com/prebid/prebid-server/usersync"
 	"github.com/prebid/prebid-server/util/httputil"
+	stringutil "github.com/prebid/prebid-server/util/stringutil"
 )
 
 const (
@@ -28,8 +33,11 @@ const (
 	chromeiOSStrLen = len(chromeiOSStr)
 )
 
+const uidCookieName = "uids"
+
 func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]usersync.Syncer, gdprPermsBuilder gdpr.PermissionsBuilder, tcf2CfgBuilder gdpr.TCF2ConfigBuilder, pbsanalytics analytics.PBSAnalyticsModule, accountsFetcher stored_requests.AccountFetcher, metricsEngine metrics.MetricsEngine) httprouter.Handle {
-	cookieTTL := time.Duration(cfg.HostCookie.TTL) * 24 * time.Hour
+	encoder := usersync.Base64Encoder{}
+	decoder := usersync.Base64Decoder{}
 
 	// convert map of syncers by bidder to map of syncers by key
 	// - its safe to assume that if multiple bidders map to the same key, the syncers are interchangeable.
@@ -46,13 +54,14 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use
 
 		defer pbsanalytics.LogSetUIDObject(&so)
 
-		pc := usersync.ParseCookieFromRequest(r, &cfg.HostCookie)
-		if !pc.AllowSyncs() {
+		cookie := usersync.ReadCookie(r, decoder, &cfg.HostCookie)
+		if !cookie.AllowSyncs() {
 			w.WriteHeader(http.StatusUnauthorized)
 			metricsEngine.RecordSetUid(metrics.SetUidOptOut)
 			so.Status = http.StatusUnauthorized
 			return
 		}
+		usersync.SyncHostCookie(r, cookie, &cfg.HostCookie)
 
 		query := r.URL.Query()
 
@@ -101,9 +110,23 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use
 			return
 		}
 
+		gdprRequestInfo, err := extractGDPRInfo(query)
+		if err != nil {
+			// Only exit if non-warning
+			if !errortypes.IsWarning(err) {
+				w.WriteHeader(http.StatusBadRequest)
+				w.Write([]byte(err.Error()))
+				metricsEngine.RecordSetUid(metrics.SetUidBadRequest)
+				so.Errors = []error{err}
+				so.Status = http.StatusBadRequest
+				return
+			}
+			w.Write([]byte("Warning: " + err.Error()))
+		}
+
 		tcf2Cfg := tcf2CfgBuilder(cfg.GDPR.TCF2, account.GDPR)
 
-		if shouldReturn, status, body := preventSyncsGDPR(query.Get("gdpr"), query.Get("gdpr_consent"), gdprPermsBuilder, tcf2Cfg); shouldReturn {
+		if shouldReturn, status, body := preventSyncsGDPR(gdprRequestInfo, gdprPermsBuilder, tcf2Cfg); shouldReturn {
 			w.WriteHeader(status)
 			w.Write([]byte(body))
 			switch status {
@@ -121,18 +144,28 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use
 		so.UID = uid
 
 		if uid == "" {
-			pc.Unsync(syncer.Key())
+			cookie.Unsync(syncer.Key())
 			metricsEngine.RecordSetUid(metrics.SetUidOK)
 			metricsEngine.RecordSyncerSet(syncer.Key(), metrics.SyncerSetUidCleared)
 			so.Success = true
-		} else if err = pc.TrySync(syncer.Key(), uid); err == nil {
+		} else if err = cookie.Sync(syncer.Key(), uid); err == nil {
 			metricsEngine.RecordSetUid(metrics.SetUidOK)
 			metricsEngine.RecordSyncerSet(syncer.Key(), metrics.SyncerSetUidOK)
 			so.Success = true
 		}
 
 		setSiteCookie := siteCookieCheck(r.UserAgent())
-		pc.SetCookieOnResponse(w, setSiteCookie, &cfg.HostCookie, cookieTTL)
+
+		// Write Cookie
+		encodedCookie, err := cookie.PrepareCookieForWrite(&cfg.HostCookie, encoder)
+		if err != nil {
+			w.WriteHeader(http.StatusBadRequest)
+			metricsEngine.RecordSetUid(metrics.SetUidBadRequest)
+			so.Errors = []error{err}
+			so.Status = http.StatusBadRequest
+			return
+		}
+		usersync.WriteCookie(w, encodedCookie, &cfg.HostCookie, setSiteCookie)
 
 		switch responseFormat {
 		case "i":
@@ -148,6 +181,138 @@ func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]use
 	})
 }
 
+// extractGDPRInfo looks for the GDPR consent string and GDPR signal in the GPP query params
+// first and the 'gdpr' and 'gdpr_consent' query params second. If found in both, throws a
+// warning. Can also throw a parsing or validation error
+func extractGDPRInfo(query url.Values) (reqInfo gdpr.RequestInfo, err error) {
+
+	reqInfo, err = parseGDPRFromGPP(query)
+	if err != nil {
+		return gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, err
+	}
+
+	legacySignal, legacyConsent, err := parseLegacyGDPRFields(query, reqInfo.GDPRSignal, reqInfo.Consent)
+	isWarning := errortypes.IsWarning(err)
+
+	if err != nil && !isWarning {
+		return gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, err
+	}
+
+	// If no GDPR data in the GPP fields, use legacy instead
+	if reqInfo.Consent == "" && reqInfo.GDPRSignal == gdpr.SignalAmbiguous {
+		reqInfo.GDPRSignal = legacySignal
+		reqInfo.Consent = legacyConsent
+	}
+
+	if reqInfo.Consent == "" && reqInfo.GDPRSignal == gdpr.SignalYes {
+		return gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, errors.New("GDPR consent is required when gdpr signal equals 1")
+	}
+
+	return reqInfo, err
+}
+
+// parseGDPRFromGPP parses and validates the "gpp_sid" and "gpp" query fields.
+func parseGDPRFromGPP(query url.Values) (gdpr.RequestInfo, error) {
+	var gdprSignal gdpr.Signal = gdpr.SignalAmbiguous
+	var gdprConsent string = ""
+	var err error
+
+	gdprSignal, err = parseSignalFromGppSidStr(query.Get("gpp_sid"))
+	if err != nil {
+		return gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, err
+	}
+
+	gdprConsent, err = parseConsentFromGppStr(query.Get("gpp"))
+	if err != nil {
+		return gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, err
+	}
+
+	return gdpr.RequestInfo{
+		Consent:    gdprConsent,
+		GDPRSignal: gdprSignal,
+	}, nil
+}
+
+// parseLegacyGDPRFields parses and validates the "gdpr" and "gdpr_consent" query fields which
+// are considered deprecated in favor of the "gpp" and "gpp_sid". The parsed and validated GDPR
+// values contained in "gpp" and "gpp_sid" are passed in the parameters gppGDPRSignal and
+// gppGDPRConsent. If the GPP parameters come with non-default values, this function discards
+// "gdpr" and "gdpr_consent" and returns a warning.
+func parseLegacyGDPRFields(query url.Values, gppGDPRSignal gdpr.Signal, gppGDPRConsent string) (gdpr.Signal, string, error) {
+	var gdprSignal gdpr.Signal = gdpr.SignalAmbiguous
+	var gdprConsent string
+	var warning error
+
+	if gdprQuerySignal := query.Get("gdpr"); len(gdprQuerySignal) > 0 {
+		if gppGDPRSignal == gdpr.SignalAmbiguous {
+			switch gdprQuerySignal {
+			case "0":
+				fallthrough
+			case "1":
+				if zeroOrOne, err := strconv.Atoi(gdprQuerySignal); err == nil {
+					gdprSignal = gdpr.Signal(zeroOrOne)
+				}
+			default:
+				return gdpr.SignalAmbiguous, "", errors.New("the gdpr query param must be either 0 or 1. You gave " + gdprQuerySignal)
+			}
+		} else {
+			warning = &errortypes.Warning{
+				Message:     "'gpp_sid' signal value will be used over the one found in the deprecated 'gdpr' field.",
+				WarningCode: errortypes.UnknownWarningCode,
+			}
+		}
+	}
+
+	if gdprLegacyConsent := query.Get("gdpr_consent"); len(gdprLegacyConsent) > 0 {
+		if len(gppGDPRConsent) > 0 {
+			warning = &errortypes.Warning{
+				Message:     "'gpp' value will be used over the one found in the deprecated 'gdpr_consent' field.",
+				WarningCode: errortypes.UnknownWarningCode,
+			}
+		} else {
+			gdprConsent = gdprLegacyConsent
+		}
+	}
+	return gdprSignal, gdprConsent, warning
+}
+
+func parseSignalFromGppSidStr(strSID string) (gdpr.Signal, error) {
+	gdprSignal := gdpr.SignalAmbiguous
+
+	if len(strSID) > 0 {
+		gppSID, err := stringutil.StrToInt8Slice(strSID)
+		if err != nil {
+			return gdpr.SignalAmbiguous, fmt.Errorf("Error parsing gpp_sid %s", err.Error())
+		}
+
+		if len(gppSID) > 0 {
+			gdprSignal = gdpr.SignalNo
+			if gppPrivacy.IsSIDInList(gppSID, gppConstants.SectionTCFEU2) {
+				gdprSignal = gdpr.SignalYes
+			}
+		}
+	}
+
+	return gdprSignal, nil
+}
+
+func parseConsentFromGppStr(gppQueryValue string) (string, error) {
+	var gdprConsent string
+
+	if len(gppQueryValue) > 0 {
+		gpp, err := gpplib.Parse(gppQueryValue)
+		if err != nil {
+			return "", err
+		}
+
+		if i := gppPrivacy.IndexOfSID(gpp, gppConstants.SectionTCFEU2); i >= 0 {
+			gdprConsent = gpp.Sections[i].GetValue()
+		}
+	}
+
+	return gdprConsent, nil
+}
+
 func getSyncer(query url.Values, syncersByKey map[string]usersync.Syncer) (usersync.Syncer, error) {
 	key := query.Get("bidder")
 
@@ -216,26 +381,7 @@ func checkChromeBrowserVersion(ua string, index int, chromeStrLength int) bool {
 	return result
 }
 
-func preventSyncsGDPR(gdprEnabled string, gdprConsent string, permsBuilder gdpr.PermissionsBuilder, tcf2Cfg gdpr.TCF2ConfigReader) (shouldReturn bool, status int, body string) {
-	if gdprEnabled != "" && gdprEnabled != "0" && gdprEnabled != "1" {
-		return true, http.StatusBadRequest, "the gdpr query param must be either 0 or 1. You gave " + gdprEnabled
-	}
-
-	if gdprEnabled == "1" && gdprConsent == "" {
-		return true, http.StatusBadRequest, "gdpr_consent is required when gdpr=1"
-	}
-
-	gdprSignal := gdpr.SignalAmbiguous
-
-	if i, err := strconv.Atoi(gdprEnabled); err == nil {
-		gdprSignal = gdpr.Signal(i)
-	}
-
-	gdprRequestInfo := gdpr.RequestInfo{
-		Consent:    gdprConsent,
-		GDPRSignal: gdprSignal,
-	}
-
+func preventSyncsGDPR(gdprRequestInfo gdpr.RequestInfo, permsBuilder gdpr.PermissionsBuilder, tcf2Cfg gdpr.TCF2ConfigReader) (shouldReturn bool, status int, body string) {
 	perms := permsBuilder(tcf2Cfg, gdprRequestInfo)
 
 	allowed, err := perms.HostCookiesAllowed(context.Background())
diff --git a/errortypes/errortypes.go b/errortypes/errortypes.go
index d93075b7..aff04822 100644
--- a/errortypes/errortypes.go
+++ b/errortypes/errortypes.go
@@ -183,7 +183,8 @@ func (err *MalformedAcct) Severity() Severity {
 	return SeverityFatal
 }
 
-// Warning is a generic non-fatal error.
+// Warning is a generic non-fatal error. Throughout the codebase, an error can
+// only be a warning if it's of the type defined below
 type Warning struct {
 	Message     string
 	WarningCode int
diff --git a/errortypes/severity.go b/errortypes/severity.go
index 0838b095..5f9cd80d 100644
--- a/errortypes/severity.go
+++ b/errortypes/severity.go
@@ -20,7 +20,10 @@ func isFatal(err error) bool {
 	return !ok || s.Severity() == SeverityFatal
 }
 
-func isWarning(err error) bool {
+// IsWarning returns true if an error is labeled with a Severity of SeverityWarning
+// Throughout the codebase, errors with SeverityWarning are of the type Warning
+// defined in this package
+func IsWarning(err error) bool {
 	s, ok := err.(Coder)
 	return ok && s.Severity() == SeverityWarning
 }
@@ -54,7 +57,7 @@ func WarningOnly(errs []error) []error {
 	errsWarning := make([]error, 0, len(errs))
 
 	for _, err := range errs {
-		if isWarning(err) {
+		if IsWarning(err) {
 			errsWarning = append(errsWarning, err)
 		}
 	}
diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go
index 3818a16a..f8344525 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -85,6 +85,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/gamma"
 	"github.com/prebid/prebid-server/adapters/gamoshi"
 	"github.com/prebid/prebid-server/adapters/globalsun"
+	"github.com/prebid/prebid-server/adapters/gothamads"
 	"github.com/prebid/prebid-server/adapters/grid"
 	"github.com/prebid/prebid-server/adapters/gumgum"
 	"github.com/prebid/prebid-server/adapters/huaweiads"
@@ -274,6 +275,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderGamma:             gamma.Builder,
 		openrtb_ext.BidderGamoshi:           gamoshi.Builder,
 		openrtb_ext.BidderGlobalsun:         globalsun.Builder,
+		openrtb_ext.BidderGothamads:         gothamads.Builder,
 		openrtb_ext.BidderGrid:              grid.Builder,
 		openrtb_ext.BidderGumGum:            gumgum.Builder,
 		openrtb_ext.BidderHuaweiAds:         huaweiads.Builder,
diff --git a/exchange/bidder.go b/exchange/bidder.go
index f5efcff3..27012ff4 100644
--- a/exchange/bidder.go
+++ b/exchange/bidder.go
@@ -58,7 +58,7 @@ type AdaptedBidder interface {
 	//
 	// Any errors will be user-facing in the API.
 	// Error messages should help publishers understand what might account for "bad" bids.
-	requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, hookExecutor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) ([]*entities.PbsOrtbSeatBid, []error)
+	requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, hookExecutor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) ([]*entities.PbsOrtbSeatBid, extraBidderRespInfo, []error)
 }
 
 // bidRequestOptions holds additional options for bid request execution to maintain clean code and reasonable number of parameters
@@ -69,6 +69,16 @@ type bidRequestOptions struct {
 	bidAdjustments      map[string]float64
 }
 
+type extraBidderRespInfo struct {
+	respProcessingStartTime time.Time
+}
+
+type extraAuctionResponseInfo struct {
+	fledge                  *openrtb_ext.Fledge
+	bidsFound               bool
+	bidderResponseStartTime time.Time
+}
+
 const ImpIdReqBody = "Stored bid response for impression id: "
 
 // Possible values of compression types Prebid Server can support for bidder compression
@@ -117,16 +127,17 @@ type bidderAdapterConfig struct {
 	EndpointCompression string
 }
 
-func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, hookExecutor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) ([]*entities.PbsOrtbSeatBid, []error) {
+func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, hookExecutor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) ([]*entities.PbsOrtbSeatBid, extraBidderRespInfo, []error) {
 	reject := hookExecutor.ExecuteBidderRequestStage(bidderRequest.BidRequest, string(bidderRequest.BidderName))
 	if reject != nil {
-		return nil, []error{reject}
+		return nil, extraBidderRespInfo{}, []error{reject}
 	}
 
 	var (
 		reqData         []*adapters.RequestData
 		errs            []error
 		responseChannel chan *httpCallInfo
+		extraRespInfo   extraBidderRespInfo
 	)
 
 	//check if real request exists for this bidder or it only has stored responses
@@ -139,7 +150,7 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest Bidde
 			if len(errs) == 0 {
 				errs = append(errs, &errortypes.FailedToRequestBids{Message: "The adapter failed to generate any bid requests, but also failed to generate an error explaining why"})
 			}
-			return nil, errs
+			return nil, extraBidderRespInfo{}, errs
 		}
 		xPrebidHeader := version.BuildXPrebidHeaderForRequest(bidderRequest.BidRequest, version.Ver)
 
@@ -231,7 +242,7 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest Bidde
 		}
 
 		if httpInfo.err == nil {
-			startTime := time.Now()
+			extraRespInfo.respProcessingStartTime = time.Now()
 			bidResponse, moreErrs := bidder.Bidder.MakeBids(bidderRequest.BidRequest, httpInfo.request, httpInfo.response)
 			errs = append(errs, moreErrs...)
 
@@ -373,18 +384,16 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest Bidde
 					errs = append(errs, err)
 				}
 			}
-			reqInfo.MakeBidsTimeInfo.Durations = append(reqInfo.MakeBidsTimeInfo.Durations, time.Since(startTime))
 		} else {
 			errs = append(errs, httpInfo.err)
 		}
 	}
-	reqInfo.MakeBidsTimeInfo.AfterMakeBidsStartTime = time.Now()
 	seatBids := make([]*entities.PbsOrtbSeatBid, 0, len(seatBidMap))
 	for _, seatBid := range seatBidMap {
 		seatBids = append(seatBids, seatBid)
 	}
 
-	return seatBids, errs
+	return seatBids, extraRespInfo, errs
 }
 
 func addNativeTypes(bid *openrtb2.Bid, request *openrtb2.BidRequest) (*nativeResponse.Response, []error) {
diff --git a/exchange/bidder_validate_bids.go b/exchange/bidder_validate_bids.go
index 8283a674..9b5771a3 100644
--- a/exchange/bidder_validate_bids.go
+++ b/exchange/bidder_validate_bids.go
@@ -31,14 +31,14 @@ type validatedBidder struct {
 	bidder AdaptedBidder
 }
 
-func (v *validatedBidder) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, hookExecutor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) ([]*entities.PbsOrtbSeatBid, []error) {
-	seatBids, errs := v.bidder.requestBid(ctx, bidderRequest, conversions, reqInfo, adsCertSigner, bidRequestOptions, alternateBidderCodes, hookExecutor, ruleToAdjustments)
+func (v *validatedBidder) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, hookExecutor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) ([]*entities.PbsOrtbSeatBid, extraBidderRespInfo, []error) {
+	seatBids, extraBidderRespInfo, errs := v.bidder.requestBid(ctx, bidderRequest, conversions, reqInfo, adsCertSigner, bidRequestOptions, alternateBidderCodes, hookExecutor, ruleToAdjustments)
 	for _, seatBid := range seatBids {
 		if validationErrors := removeInvalidBids(bidderRequest.BidRequest, seatBid); len(validationErrors) > 0 {
 			errs = append(errs, validationErrors...)
 		}
 	}
-	return seatBids, errs
+	return seatBids, extraBidderRespInfo, errs
 }
 
 // validateBids will run some validation checks on the returned bids and excise any invalid bids
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 07dce45a..e1a9868d 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -89,15 +89,15 @@ type seatResponseExtra struct {
 	Warnings           []openrtb_ext.ExtBidderMessage
 	// httpCalls is the list of debugging info. It should only be populated if the request.test == 1.
 	// This will become response.ext.debug.httpcalls.{bidder} on the final Response.
-	HttpCalls        []*openrtb_ext.ExtHttpCall
-	MakeBidsTimeInfo adapters.MakeBidsTimeInfo
+	HttpCalls []*openrtb_ext.ExtHttpCall
 }
 
 type bidResponseWrapper struct {
-	adapterSeatBids []*entities.PbsOrtbSeatBid
-	adapterExtra    *seatResponseExtra
-	bidder          openrtb_ext.BidderName
-	adapter         openrtb_ext.BidderName
+	adapterSeatBids         []*entities.PbsOrtbSeatBid
+	adapterExtra            *seatResponseExtra
+	bidder                  openrtb_ext.BidderName
+	adapter                 openrtb_ext.BidderName
+	bidderResponseStartTime time.Time
 }
 
 type BidIDGenerator interface {
@@ -210,8 +210,6 @@ type AuctionRequest struct {
 	PubID                 string
 	HookExecutor          hookexecution.StageExecutor
 	QueryParams           url.Values
-	// map of bidder to store duration needed for the MakeBids() calls and start time after MakeBids() calls
-	MakeBidsTimeInfo map[openrtb_ext.BidderName]adapters.MakeBidsTimeInfo
 }
 
 // BidderRequest holds the bidder specific request and all other
@@ -368,8 +366,10 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog
 		} else if r.Account.AlternateBidderCodes != nil {
 			alternateBidderCodes = *r.Account.AlternateBidderCodes
 		}
-		adapterBids, adapterExtra, fledge, anyBidsReturned = e.getAllBids(auctionCtx, bidderRequests, bidAdjustmentFactors, conversions, accountDebugAllow, r.GlobalPrivacyControlHeader, debugLog.DebugOverride, alternateBidderCodes, requestExtLegacy.Prebid.Experiment, r.HookExecutor, r.StartTime, bidAdjustmentRules)
-		r.MakeBidsTimeInfo = buildMakeBidsTimeInfoMap(adapterExtra)
+		var extraRespInfo extraAuctionResponseInfo
+		adapterBids, adapterExtra, extraRespInfo = e.getAllBids(auctionCtx, bidderRequests, bidAdjustmentFactors, conversions, accountDebugAllow, r.GlobalPrivacyControlHeader, debugLog.DebugOverride, alternateBidderCodes, requestExtLegacy.Prebid.Experiment, r.HookExecutor, r.StartTime, bidAdjustmentRules)
+		fledge = extraRespInfo.fledge
+		anyBidsReturned = extraRespInfo.bidsFound
 	}
 
 	var (
@@ -504,14 +504,6 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog
 	}, nil
 }
 
-func buildMakeBidsTimeInfoMap(adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra) map[openrtb_ext.BidderName]adapters.MakeBidsTimeInfo {
-	makeBidsInfoMap := make(map[openrtb_ext.BidderName]adapters.MakeBidsTimeInfo)
-	for bidderName, responseExtra := range adapterExtra {
-		makeBidsInfoMap[bidderName] = responseExtra.MakeBidsTimeInfo
-	}
-	return makeBidsInfoMap
-}
-
 func buildMultiBidMap(prebid *openrtb_ext.ExtRequestPrebid) map[string]openrtb_ext.ExtMultiBid {
 	if prebid == nil || prebid.MultiBid == nil {
 		return nil
@@ -669,13 +661,12 @@ func (e *exchange) getAllBids(
 	bidAdjustmentRules map[string][]openrtb_ext.Adjustment) (
 	map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid,
 	map[openrtb_ext.BidderName]*seatResponseExtra,
-	*openrtb_ext.Fledge,
-	bool) {
+	extraAuctionResponseInfo) {
 	// Set up pointers to the bid results
 	adapterBids := make(map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, len(bidderRequests))
 	adapterExtra := make(map[openrtb_ext.BidderName]*seatResponseExtra, len(bidderRequests))
 	chBids := make(chan *bidResponseWrapper, len(bidderRequests))
-	bidsFound := false
+	extraRespInfo := extraAuctionResponseInfo{}
 
 	e.me.RecordOverheadTime(metrics.MakeBidderRequests, time.Since(pbsRequestStartTime))
 
@@ -707,7 +698,8 @@ func (e *exchange) getAllBids(
 				addCallSignHeader:   isAdsCertEnabled(experiment, e.bidderInfo[string(bidderRequest.BidderName)]),
 				bidAdjustments:      bidAdjustments,
 			}
-			seatBids, err := e.adapterMap[bidderRequest.BidderCoreName].requestBid(ctx, bidderRequest, conversions, &reqInfo, e.adsCertSigner, bidReqOptions, alternateBidderCodes, hookExecutor, bidAdjustmentRules)
+			seatBids, extraBidderRespInfo, err := e.adapterMap[bidderRequest.BidderCoreName].requestBid(ctx, bidderRequest, conversions, &reqInfo, e.adsCertSigner, bidReqOptions, alternateBidderCodes, hookExecutor, bidAdjustmentRules)
+			brw.bidderResponseStartTime = extraBidderRespInfo.respProcessingStartTime
 
 			// Add in time reporting
 			elapsed := time.Since(start)
@@ -718,11 +710,6 @@ func (e *exchange) getAllBids(
 			if len(seatBids) != 0 {
 				ae.HttpCalls = seatBids[0].HttpCalls
 			}
-			// SeatBidsPreparationStartTime is needed to calculate duration for openrtb response preparation time metric
-			//  No metric needs to be logged for requests which error out
-			if err == nil {
-				ae.MakeBidsTimeInfo = reqInfo.MakeBidsTimeInfo
-			}
 			// Timing statistics
 			e.me.RecordAdapterTime(bidderRequest.BidderLabels, elapsed)
 			bidderRequest.BidderLabels.AdapterBids = bidsToMetric(brw.adapterSeatBids)
@@ -744,12 +731,13 @@ func (e *exchange) getAllBids(
 		}, chBids)
 		go bidderRunner(bidder, conversions)
 	}
-	var fledge *openrtb_ext.Fledge
 
 	// Wait for the bidders to do their thing
 	for i := 0; i < len(bidderRequests); i++ {
 		brw := <-chBids
-
+		if !brw.bidderResponseStartTime.IsZero() {
+			extraRespInfo.bidderResponseStartTime = brw.bidderResponseStartTime
+		}
 		//if bidder returned no bids back - remove bidder from further processing
 		for _, seatBid := range brw.adapterSeatBids {
 			if seatBid != nil {
@@ -762,18 +750,18 @@ func (e *exchange) getAllBids(
 					}
 				}
 				// collect fledgeAuctionConfigs separately from bids, as empty seatBids may be discarded
-				fledge = collectFledgeFromSeatBid(fledge, bidderName, brw.adapter, seatBid)
+				extraRespInfo.fledge = collectFledgeFromSeatBid(extraRespInfo.fledge, bidderName, brw.adapter, seatBid)
 			}
 		}
 		//but we need to add all bidders data to adapterExtra to have metrics and other metadata
 		adapterExtra[brw.bidder] = brw.adapterExtra
 
-		if !bidsFound && adapterBids[brw.bidder] != nil && len(adapterBids[brw.bidder].Bids) > 0 {
-			bidsFound = true
+		if !extraRespInfo.bidsFound && adapterBids[brw.bidder] != nil && len(adapterBids[brw.bidder].Bids) > 0 {
+			extraRespInfo.bidsFound = true
 		}
 	}
 
-	return adapterBids, adapterExtra, fledge, bidsFound
+	return adapterBids, adapterExtra, extraRespInfo
 }
 
 func collectFledgeFromSeatBid(fledge *openrtb_ext.Fledge, bidderName openrtb_ext.BidderName, adapterName openrtb_ext.BidderName, seatBid *entities.PbsOrtbSeatBid) *openrtb_ext.Fledge {
diff --git a/go.mod b/go.mod
index e7a6f7e3..6731c177 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/prebid/prebid-server
 
-go 1.19
+go 1.20
 
 require (
 	github.com/DATA-DOG/go-sqlmock v1.5.0
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 9906d12e..978af5e0 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -175,6 +175,7 @@ const (
 	BidderGamma             BidderName = "gamma"
 	BidderGamoshi           BidderName = "gamoshi"
 	BidderGlobalsun         BidderName = "globalsun"
+	BidderGothamads         BidderName = "gothamads"
 	BidderGrid              BidderName = "grid"
 	BidderGumGum            BidderName = "gumgum"
 	BidderHuaweiAds         BidderName = "huaweiads"
@@ -373,6 +374,7 @@ func CoreBidderNames() []BidderName {
 		BidderGamma,
 		BidderGamoshi,
 		BidderGlobalsun,
+		BidderGothamads,
 		BidderGrid,
 		BidderGumGum,
 		BidderHuaweiAds,
diff --git a/openrtb_ext/imp_gothamads.go b/openrtb_ext/imp_gothamads.go
new file mode 100644
index 00000000..fb945fbb
--- /dev/null
+++ b/openrtb_ext/imp_gothamads.go
@@ -0,0 +1,5 @@
+package openrtb_ext
+
+type ExtGothamAds struct {
+	AccountID string `json:"accountId"`
+}
diff --git a/pbs/usersync.go b/pbs/usersync.go
index 748581af..7b468cb0 100644
--- a/pbs/usersync.go
+++ b/pbs/usersync.go
@@ -58,6 +58,8 @@ func (deps *UserSyncDeps) VerifyRecaptcha(response string) error {
 func (deps *UserSyncDeps) OptOut(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
 	optout := r.FormValue("optout")
 	rr := r.FormValue("g-recaptcha-response")
+	encoder := usersync.Base64Encoder{}
+	decoder := usersync.Base64Decoder{}
 
 	if rr == "" {
 		http.Redirect(w, r, fmt.Sprintf("%s/static/optout.html", deps.ExternalUrl), http.StatusMovedPermanently)
@@ -73,10 +75,18 @@ func (deps *UserSyncDeps) OptOut(w http.ResponseWriter, r *http.Request, _ httpr
 		return
 	}
 
-	pc := usersync.ParseCookieFromRequest(r, deps.HostCookieConfig)
+	// Read Cookie
+	pc := usersync.ReadCookie(r, decoder, deps.HostCookieConfig)
+	usersync.SyncHostCookie(r, pc, deps.HostCookieConfig)
 	pc.SetOptOut(optout != "")
 
-	pc.SetCookieOnResponse(w, false, deps.HostCookieConfig, deps.HostCookieConfig.TTLDuration())
+	// Write Cookie
+	encodedCookie, err := pc.PrepareCookieForWrite(deps.HostCookieConfig, encoder)
+	if err != nil {
+		w.WriteHeader(http.StatusBadRequest)
+		return
+	}
+	usersync.WriteCookie(w, encodedCookie, deps.HostCookieConfig, false)
 
 	if optout == "" {
 		http.Redirect(w, r, deps.HostCookieConfig.OptInURL, http.StatusMovedPermanently)
diff --git a/privacy/gpp/gpp.go b/privacy/gpp/gpp.go
index 2ebf4b97..35000406 100644
--- a/privacy/gpp/gpp.go
+++ b/privacy/gpp/gpp.go
@@ -23,9 +23,10 @@ func IsSIDInList(gppSIDs []int8, sid gppConstants.SectionID) bool {
 	return false
 }
 
-// IndexOfSID returns a zero or non-negative integer that represents the position of the 'sid' value in the
-// 'gpp.SectionTypes' array. If the 'sid' value is not found, returns -1. This logic is used in
-// more than one place in our codebase, therefore it was decided to make it its own function.
+// IndexOfSID returns a zero or non-negative integer that represents the position of
+// the 'sid' value in the 'gpp.SectionTypes' array. If the 'sid' value is not found,
+// returns -1. This logic is used in more than one place in our codebase, therefore
+// it was decided to make it its own function.
 func IndexOfSID(gpp gpplib.GppContainer, sid gppConstants.SectionID) int {
 	for i, id := range gpp.SectionTypes {
 		if id == sid {
diff --git a/static/bidder-info/adkernel.yaml b/static/bidder-info/adkernel.yaml
index 864ca71a..1afbdc8d 100644
--- a/static/bidder-info/adkernel.yaml
+++ b/static/bidder-info/adkernel.yaml
@@ -12,6 +12,6 @@ capabilities:
       - video
 userSync:
   redirect:
-    url: "https://sync.adkernel.com/user-sync?t=image&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r={{.RedirectURL}}"
+    url: "https://sync.adkernel.com/user-sync?t=image&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&r={{.RedirectURL}}"
     userMacro: "{UID}"
-endpointCompression: "GZIP"
\ No newline at end of file
+endpointCompression: "GZIP"
diff --git a/static/bidder-info/adsinteractive.yaml b/static/bidder-info/adsinteractive.yaml
index d430e63b..09518472 100644
--- a/static/bidder-info/adsinteractive.yaml
+++ b/static/bidder-info/adsinteractive.yaml
@@ -12,6 +12,6 @@ capabilities:
       - banner
 userSync:
   redirect:
-    url: "https://bid.adsinteractive.com/getuid?{{.RedirectURL}}"
+    url: "https://sync.adsinteractive.com/getuid?{{.RedirectURL}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}"
     userMacro: "$AUID"
  
diff --git a/static/bidder-info/freewheel-ssp.yaml b/static/bidder-info/freewheel-ssp.yaml
index f013445a..0c0a11ed 100644
--- a/static/bidder-info/freewheel-ssp.yaml
+++ b/static/bidder-info/freewheel-ssp.yaml
@@ -12,5 +12,5 @@ capabilities:
       - video
 userSync:
   iframe:
-    url: "https://ads.stickyadstv.com/pbs-user-sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r={{.RedirectURL}}"
+    url: "https://ads.stickyadstv.com/pbs-user-sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&r={{.RedirectURL}}"
     userMacro: "{viewerid}"
diff --git a/static/bidder-info/freewheelssp.yaml b/static/bidder-info/freewheelssp.yaml
index 5cef0de1..8c9286cb 100644
--- a/static/bidder-info/freewheelssp.yaml
+++ b/static/bidder-info/freewheelssp.yaml
@@ -12,5 +12,5 @@ capabilities:
       - video
 userSync:
   iframe:
-    url: "https://ads.stickyadstv.com/pbs-user-sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r={{.RedirectURL}}"
+    url: "https://ads.stickyadstv.com/pbs-user-sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&r={{.RedirectURL}}"
     userMacro: "{viewerid}"
\ No newline at end of file
diff --git a/static/bidder-info/gothamads.yaml b/static/bidder-info/gothamads.yaml
new file mode 100644
index 00000000..c9f4e66b
--- /dev/null
+++ b/static/bidder-info/gothamads.yaml
@@ -0,0 +1,14 @@
+endpoint: "http://us-e-node1.gothamads.com/?pass={{.AccountID}}"
+maintainer:
+  email: "support@gothamads.com"
+capabilities:
+  app:
+    mediaTypes:
+      - banner
+      - video
+      - native
+  site:
+    mediaTypes:
+      - banner
+      - video
+      - native
\ No newline at end of file
diff --git a/static/bidder-params/gothamads.json b/static/bidder-params/gothamads.json
new file mode 100644
index 00000000..030d43eb
--- /dev/null
+++ b/static/bidder-params/gothamads.json
@@ -0,0 +1,16 @@
+{
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "title": "Gothamads Adapter Params",
+    "description": "A schema which validates params accepted by the Gothamads adapter",
+    "type": "object",
+    "properties": {
+        "accountId": {
+            "type": "string",
+            "description": "Account id",
+            "minLength": 1
+        }
+    },
+    "required": [
+        "accountId"
+    ]
+}
\ No newline at end of file
diff --git a/usersync/cookie.go b/usersync/cookie.go
index 4f80fa5d..c0eb898c 100644
--- a/usersync/cookie.go
+++ b/usersync/cookie.go
@@ -1,11 +1,10 @@
 package usersync
 
 import (
-	"encoding/base64"
 	"encoding/json"
 	"errors"
-	"math"
 	"net/http"
+	"sort"
 	"time"
 
 	"github.com/prebid/prebid-server/config"
@@ -20,70 +19,155 @@ const uidTTL = 14 * 24 * time.Hour
 
 // Cookie is the cookie used in Prebid Server.
 //
-// To get an instance of this from a request, use ParseCookieFromRequest.
-// To write an instance onto a response, use SetCookieOnResponse.
+// To get an instance of this from a request, use ReadCookie.
+// To write an instance onto a response, use WriteCookie.
 type Cookie struct {
-	uids   map[string]uidWithExpiry
+	uids   map[string]UIDEntry
 	optOut bool
 }
 
-// uidWithExpiry bundles the UID with an Expiration date.
-// After the expiration, the UID is no longer valid.
-type uidWithExpiry struct {
+// UIDEntry bundles the UID with an Expiration date.
+type UIDEntry struct {
 	// UID is the ID given to a user by a particular bidder
 	UID string `json:"uid"`
 	// Expires is the time at which this UID should no longer apply.
 	Expires time.Time `json:"expires"`
 }
 
-// ParseCookieFromRequest parses the UserSyncMap from an HTTP Request.
-func ParseCookieFromRequest(r *http.Request, cookie *config.HostCookie) *Cookie {
-	if cookie.OptOutCookie.Name != "" {
-		optOutCookie, err1 := r.Cookie(cookie.OptOutCookie.Name)
-		if err1 == nil && optOutCookie.Value == cookie.OptOutCookie.Value {
-			pc := NewCookie()
-			pc.SetOptOut(true)
-			return pc
-		}
+// NewCookie returns a new empty cookie.
+func NewCookie() *Cookie {
+	return &Cookie{
+		uids: make(map[string]UIDEntry),
 	}
-	var parsed *Cookie
-	uidCookie, err2 := r.Cookie(uidCookieName)
-	if err2 == nil {
-		parsed = ParseCookie(uidCookie)
-	} else {
-		parsed = NewCookie()
+}
+
+// ReadCookie reads the cookie from the request
+func ReadCookie(r *http.Request, decoder Decoder, host *config.HostCookie) *Cookie {
+	if hostOptOutCookie := checkHostCookieOptOut(r, host); hostOptOutCookie != nil {
+		return hostOptOutCookie
+	}
+
+	// Read cookie from request
+	cookieFromRequest, err := r.Cookie(uidCookieName)
+	if err != nil {
+		return NewCookie()
 	}
-	// Fixes #582
-	if uid, _, _ := parsed.GetUID(cookie.Family); uid == "" && cookie.CookieName != "" {
-		if hostCookie, err := r.Cookie(cookie.CookieName); err == nil {
-			parsed.TrySync(cookie.Family, hostCookie.Value)
+	decodedCookie := decoder.Decode(cookieFromRequest.Value)
+
+	return decodedCookie
+}
+
+// PrepareCookieForWrite ejects UIDs as long as the cookie is too full
+func (cookie *Cookie) PrepareCookieForWrite(cfg *config.HostCookie, encoder Encoder) (string, error) {
+	uuidKeys := sortUIDs(cookie.uids)
+
+	i := 0
+	for len(cookie.uids) > 0 {
+		encodedCookie, err := encoder.Encode(cookie)
+		if err != nil {
+			return encodedCookie, nil
+		}
+
+		// Convert to HTTP Cookie to Get Size
+		httpCookie := &http.Cookie{
+			Name:    uidCookieName,
+			Value:   encodedCookie,
+			Expires: time.Now().Add(cfg.TTLDuration()),
+			Path:    "/",
+		}
+		cookieSize := len([]byte(httpCookie.String()))
+
+		isCookieTooBig := cookieSize > cfg.MaxCookieSizeBytes && cfg.MaxCookieSizeBytes > 0
+		if !isCookieTooBig {
+			return encodedCookie, nil
 		}
+
+		uidToDelete := uuidKeys[i]
+		delete(cookie.uids, uidToDelete)
+
+		i++
 	}
-	return parsed
+	return "", nil
 }
 
-// ParseCookie parses the UserSync cookie from a raw HTTP cookie.
-func ParseCookie(httpCookie *http.Cookie) *Cookie {
-	jsonValue, err := base64.URLEncoding.DecodeString(httpCookie.Value)
-	if err != nil {
-		// corrupted cookie; we should reset
-		return NewCookie()
+// WriteCookie sets the prepared cookie onto the header
+func WriteCookie(w http.ResponseWriter, encodedCookie string, cfg *config.HostCookie, setSiteCookie bool) {
+	ttl := cfg.TTLDuration()
+
+	httpCookie := &http.Cookie{
+		Name:    uidCookieName,
+		Value:   encodedCookie,
+		Expires: time.Now().Add(ttl),
+		Path:    "/",
 	}
 
-	var cookie Cookie
-	if err = json.Unmarshal(jsonValue, &cookie); err != nil {
-		// corrupted cookie; we should reset
-		return NewCookie()
+	if cfg.Domain != "" {
+		httpCookie.Domain = cfg.Domain
+	}
+
+	if setSiteCookie {
+		httpCookie.Secure = true
+		httpCookie.SameSite = http.SameSiteNoneMode
 	}
 
-	return &cookie
+	w.Header().Add("Set-Cookie", httpCookie.String())
 }
 
-// NewCookie returns a new empty cookie.
-func NewCookie() *Cookie {
-	return &Cookie{
-		uids: make(map[string]uidWithExpiry),
+// Sync tries to set the UID for some syncer key. It returns an error if the set didn't happen.
+func (cookie *Cookie) Sync(key string, uid string) error {
+	if !cookie.AllowSyncs() {
+		return errors.New("the user has opted out of prebid server cookie syncs")
+	}
+
+	if checkAudienceNetwork(key, uid) {
+		return errors.New("audienceNetwork uses a UID of 0 as \"not yet recognized\"")
 	}
+
+	// Sync
+	cookie.uids[key] = UIDEntry{
+		UID:     uid,
+		Expires: time.Now().Add(uidTTL),
+	}
+
+	return nil
+}
+
+// sortUIDs is used to get a list of uids sorted from oldest to newest
+// This list is used to eject oldest uids from the cookie
+// This will be incorporated with a more complex ejection framework in a future PR
+func sortUIDs(uids map[string]UIDEntry) []string {
+	if len(uids) > 0 {
+		uuidKeys := make([]string, 0, len(uids))
+		for key := range uids {
+			uuidKeys = append(uuidKeys, key)
+		}
+		sort.SliceStable(uuidKeys, func(i, j int) bool {
+			return uids[uuidKeys[i]].Expires.Before(uids[uuidKeys[j]].Expires)
+		})
+		return uuidKeys
+	}
+	return nil
+}
+
+// SyncHostCookie syncs the request cookie with the host cookie
+func SyncHostCookie(r *http.Request, requestCookie *Cookie, host *config.HostCookie) {
+	if uid, _, _ := requestCookie.GetUID(host.Family); uid == "" && host.CookieName != "" {
+		if hostCookie, err := r.Cookie(host.CookieName); err == nil {
+			requestCookie.Sync(host.Family, hostCookie.Value)
+		}
+	}
+}
+
+func checkHostCookieOptOut(r *http.Request, host *config.HostCookie) *Cookie {
+	if host.OptOutCookie.Name != "" {
+		optOutCookie, err := r.Cookie(host.OptOutCookie.Name)
+		if err == nil && optOutCookie.Value == host.OptOutCookie.Value {
+			hostOptOut := NewCookie()
+			hostOptOut.SetOptOut(true)
+			return hostOptOut
+		}
+	}
+	return nil
 }
 
 // AllowSyncs is true if the user lets bidders sync cookies, and false otherwise.
@@ -96,30 +180,12 @@ func (cookie *Cookie) SetOptOut(optOut bool) {
 	cookie.optOut = optOut
 
 	if optOut {
-		cookie.uids = make(map[string]uidWithExpiry)
-	}
-}
-
-// Gets an HTTP cookie containing all the data from this UserSyncMap. This is a snapshot--not a live view.
-func (cookie *Cookie) ToHTTPCookie(ttl time.Duration) *http.Cookie {
-	j, _ := json.Marshal(cookie)
-	b64 := base64.URLEncoding.EncodeToString(j)
-
-	return &http.Cookie{
-		Name:    uidCookieName,
-		Value:   b64,
-		Expires: time.Now().Add(ttl),
-		Path:    "/",
+		cookie.uids = make(map[string]UIDEntry)
 	}
 }
 
 // GetUID Gets this user's ID for the given syncer key.
-// The first returned value is the user's ID.
-// The second returned value is true if we had a value stored, and false if we didn't.
-// The third returned value is true if that value is "active", and false if it's expired.
-//
-// If no value was stored, then the "isActive" return value will be false.
-func (cookie *Cookie) GetUID(key string) (string, bool, bool) {
+func (cookie *Cookie) GetUID(key string) (uid string, isUIDFound bool, isUIDActive bool) {
 	if cookie != nil {
 		if uid, ok := cookie.uids[key]; ok {
 			return uid.UID, true, time.Now().Before(uid.Expires)
@@ -140,41 +206,6 @@ func (cookie *Cookie) GetUIDs() map[string]string {
 	return uids
 }
 
-// SetCookieOnResponse is a shortcut for "ToHTTPCookie(); cookie.setDomain(domain); setCookie(w, cookie)"
-func (cookie *Cookie) SetCookieOnResponse(w http.ResponseWriter, setSiteCookie bool, cfg *config.HostCookie, ttl time.Duration) {
-	httpCookie := cookie.ToHTTPCookie(ttl)
-	var domain string = cfg.Domain
-
-	if domain != "" {
-		httpCookie.Domain = domain
-	}
-
-	var currSize int = len([]byte(httpCookie.String()))
-	for cfg.MaxCookieSizeBytes > 0 && currSize > cfg.MaxCookieSizeBytes && len(cookie.uids) > 0 {
-		var oldestElem string = ""
-		var oldestDate int64 = math.MaxInt64
-		for key, value := range cookie.uids {
-			timeUntilExpiration := time.Until(value.Expires)
-			if timeUntilExpiration < time.Duration(oldestDate) {
-				oldestElem = key
-				oldestDate = int64(timeUntilExpiration)
-			}
-		}
-		delete(cookie.uids, oldestElem)
-		httpCookie = cookie.ToHTTPCookie(ttl)
-		if domain != "" {
-			httpCookie.Domain = domain
-		}
-		currSize = len([]byte(httpCookie.String()))
-	}
-
-	if setSiteCookie {
-		httpCookie.Secure = true
-		httpCookie.SameSite = http.SameSiteNoneMode
-	}
-	w.Header().Add("Set-Cookie", httpCookie.String())
-}
-
 // Unsync removes the user's ID for the given syncer key from this cookie.
 func (cookie *Cookie) Unsync(key string) {
 	delete(cookie.uids, key)
@@ -199,24 +230,8 @@ func (cookie *Cookie) HasAnyLiveSyncs() bool {
 	return false
 }
 
-// TrySync tries to set the UID for some syncer key. It returns an error if the set didn't happen.
-func (cookie *Cookie) TrySync(key string, uid string) error {
-	if !cookie.AllowSyncs() {
-		return errors.New("The user has opted out of prebid server cookie syncs.")
-	}
-
-	// At the moment, Facebook calls /setuid with a UID of 0 if the user isn't logged into Facebook.
-	// They shouldn't be sending us a sentinel value... but since they are, we're refusing to save that ID.
-	if key == string(openrtb_ext.BidderAudienceNetwork) && uid == "0" {
-		return errors.New("audienceNetwork uses a UID of 0 as \"not yet recognized\".")
-	}
-
-	cookie.uids[key] = uidWithExpiry{
-		UID:     uid,
-		Expires: time.Now().Add(uidTTL),
-	}
-
-	return nil
+func checkAudienceNetwork(key string, uid string) bool {
+	return key == string(openrtb_ext.BidderAudienceNetwork) && uid == "0"
 }
 
 // cookieJson defines the JSON contract for the cookie data's storage format.
@@ -224,8 +239,8 @@ func (cookie *Cookie) TrySync(key string, uid string) error {
 // This exists so that Cookie (which is public) can have private fields, and the rest of
 // the code doesn't have to worry about the cookie data storage format.
 type cookieJson struct {
-	UIDs   map[string]uidWithExpiry `json:"tempUIDs,omitempty"`
-	OptOut bool                     `json:"optout,omitempty"`
+	UIDs   map[string]UIDEntry `json:"tempUIDs,omitempty"`
+	OptOut bool                `json:"optout,omitempty"`
 }
 
 func (cookie *Cookie) MarshalJSON() ([]byte, error) {
@@ -250,10 +265,10 @@ func (cookie *Cookie) UnmarshalJSON(b []byte) error {
 	}
 
 	if cookie.uids == nil {
-		cookie.uids = make(map[string]uidWithExpiry)
+		cookie.uids = make(map[string]UIDEntry)
 	}
 
-	// Audience Network / Facebook Handling
+	// Audience Network Handling
 	if id, ok := cookie.uids[string(openrtb_ext.BidderAudienceNetwork)]; ok && id.UID == "0" {
 		delete(cookie.uids, string(openrtb_ext.BidderAudienceNetwork))
 	}
diff --git a/usersync/decoder.go b/usersync/decoder.go
new file mode 100644
index 00000000..3ff13aa3
--- /dev/null
+++ b/usersync/decoder.go
@@ -0,0 +1,27 @@
+package usersync
+
+import (
+	"encoding/base64"
+	"encoding/json"
+)
+
+type Decoder interface {
+	// Decode takes an encoded string and decodes it into a cookie
+	Decode(v string) *Cookie
+}
+
+type Base64Decoder struct{}
+
+func (d Base64Decoder) Decode(encodedValue string) *Cookie {
+	jsonValue, err := base64.URLEncoding.DecodeString(encodedValue)
+	if err != nil {
+		return NewCookie()
+	}
+
+	var cookie Cookie
+	if err = json.Unmarshal(jsonValue, &cookie); err != nil {
+		return NewCookie()
+	}
+
+	return &cookie
+}
diff --git a/usersync/encoder.go b/usersync/encoder.go
new file mode 100644
index 00000000..eef7e2ef
--- /dev/null
+++ b/usersync/encoder.go
@@ -0,0 +1,23 @@
+package usersync
+
+import (
+	"encoding/base64"
+	"encoding/json"
+)
+
+type Encoder interface {
+	// Encode a cookie into a base 64 string
+	Encode(c *Cookie) (string, error)
+}
+
+type Base64Encoder struct{}
+
+func (e Base64Encoder) Encode(c *Cookie) (string, error) {
+	j, err := json.Marshal(c)
+	if err != nil {
+		return "", err
+	}
+	b64 := base64.URLEncoding.EncodeToString(j)
+
+	return b64, nil
+}


Test patch:
diff --git a/adapters/gothamads/gothamads_test.go b/adapters/gothamads/gothamads_test.go
new file mode 100644
index 00000000..82e34dde
--- /dev/null
+++ b/adapters/gothamads/gothamads_test.go
@@ -0,0 +1,28 @@
+package gothamads
+
+import (
+	"testing"
+
+	"github.com/prebid/prebid-server/adapters/adapterstest"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestJsonSamples(t *testing.T) {
+	bidder, buildErr := Builder(openrtb_ext.BidderGothamads, config.Adapter{
+		Endpoint: "http://us-e-node1.gothamads.com/?pass={{.AccountID}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})
+
+	if buildErr != nil {
+		t.Fatalf("Builder returned unexpected error %v", buildErr)
+	}
+
+	adapterstest.RunJSONBidderTest(t, "gothamadstest", bidder)
+}
+
+func TestEndpointTemplateMalformed(t *testing.T) {
+	_, buildErr := Builder(openrtb_ext.BidderGothamads, config.Adapter{
+		Endpoint: "{{Malformed}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})
+
+	assert.Error(t, buildErr)
+}
diff --git a/adapters/gothamads/gothamadstest/exemplary/banner-app.json b/adapters/gothamads/gothamadstest/exemplary/banner-app.json
new file mode 100644
index 00000000..44966ea8
--- /dev/null
+++ b/adapters/gothamads/gothamadstest/exemplary/banner-app.json
@@ -0,0 +1,151 @@
+{
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "device": {
+      "ua": "test-user-agent",
+      "ip": "123.123.123.123",
+      "language": "en",
+      "dnt": 0
+    },
+    "tmax": 1000,
+    "user": {
+      "buyeruid": "awesome-user"
+    },
+    "app": {
+      "publisher": {
+        "id": "123456789"
+      },
+      "cat": [
+        "IAB22-1"
+      ],
+      "bundle": "com.app.awesome",
+      "name": "Awesome App",
+      "domain": "awesomeapp.com",
+      "id": "123456789"
+    },
+    "imp": [
+      {
+        "id": "1",
+        "tagid": "ogTAGID",
+        "banner": {
+          "w": 320,
+          "h": 50
+        },
+        "ext": {
+          "bidder": {
+            "accountId": "accountId"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "headers": {
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "Accept": [
+            "application/json"
+          ],
+          "X-Openrtb-Version": [
+            "2.5"
+          ],
+          "User-Agent": [
+            "test-user-agent"
+          ],
+          "X-Forwarded-For": [
+            "123.123.123.123"
+          ]
+        },
+        "uri": "http://us-e-node1.gothamads.com/?pass=accountId",
+        "body": {
+          "id": "some-request-id",
+          "device": {
+            "ua": "test-user-agent",
+            "ip": "123.123.123.123",
+            "language": "en",
+            "dnt": 0
+          },
+          "imp": [
+            {
+              "id": "1",
+              "banner": {
+                "w": 320,
+                "h": 50
+              },
+              "tagid": "ogTAGID"
+            }
+          ],
+          "app": {
+            "id": "123456789",
+            "name": "Awesome App",
+            "bundle": "com.app.awesome",
+            "domain": "awesomeapp.com",
+            "cat": [
+              "IAB22-1"
+            ],
+            "publisher": {
+              "id": "123456789"
+            }
+          },
+          "user": {
+            "buyeruid": "awesome-user"
+          },
+          "tmax": 1000
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "awesome-resp-id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+                  "impid": "1",
+                  "price": 3.5,
+                  "adm": "awesome-markup",
+                  "adomain": [
+                    "awesome.com"
+                  ],
+                  "crid": "20",
+                  "w": 320,
+                  "h": 50,
+                  "mtype": 1
+                }
+              ],
+              "type": "banner",
+              "seat": "gothamAds"
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "bids": [
+        {
+          "bid": {
+            "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+            "impid": "1",
+            "price": 3.5,
+            "adm": "awesome-markup",
+            "adomain": [
+              "awesome.com"
+            ],
+            "crid": "20",
+            "w": 320,
+            "h": 50,
+            "mtype": 1
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/gothamads/gothamadstest/exemplary/banner-web.json b/adapters/gothamads/gothamadstest/exemplary/banner-web.json
new file mode 100644
index 00000000..baac33a4
--- /dev/null
+++ b/adapters/gothamads/gothamadstest/exemplary/banner-web.json
@@ -0,0 +1,194 @@
+{
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "device": {
+      "ua": "test-user-agent",
+      "ip": "123.123.123.123",
+      "language": "en",
+      "dnt": 0
+    },
+    "tmax": 1000,
+    "user": {
+      "buyeruid": "awesome-user"
+    },
+    "site": {
+      "page": "test.com",
+      "publisher": {
+        "id": "123456789"
+      }
+    },
+    "imp": [
+      {
+        "id": "some-impression-id1",
+        "tagid": "ogTAGID",
+        "banner": {
+          "w": 320,
+          "h": 50
+        },
+        "ext": {
+          "bidder": {
+            "accountId": "accountId"
+          }
+        }
+      },
+      {
+        "id": "some-impression-id2",
+        "tagid": "ogTAGID",
+        "banner": {
+          "w": 320,
+          "h": 50
+        },
+        "ext": {
+          "bidder": {
+            "accountId": "accountId"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "headers": {
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "Accept": [
+            "application/json"
+          ],
+          "X-Openrtb-Version": [
+            "2.5"
+          ],
+          "User-Agent": [
+            "test-user-agent"
+          ],
+          "X-Forwarded-For": [
+            "123.123.123.123"
+          ]
+        },
+        "uri": "http://us-e-node1.gothamads.com/?pass=accountId",
+        "body": {
+          "id": "some-request-id",
+          "device": {
+            "ua": "test-user-agent",
+            "ip": "123.123.123.123",
+            "language": "en",
+            "dnt": 0
+          },
+          "imp": [
+            {
+              "id": "some-impression-id1",
+              "tagid": "ogTAGID",
+              "banner": {
+                "w": 320,
+                "h": 50
+              }
+            },
+            {
+              "id": "some-impression-id2",
+              "tagid": "ogTAGID",
+              "banner": {
+                "w": 320,
+                "h": 50
+              },
+              "ext": {
+                "bidder": {
+                  "accountId": "accountId"
+                }
+              }
+            }
+          ],
+          "site": {
+            "page": "test.com",
+            "publisher": {
+              "id": "123456789"
+            }
+          },
+          "user": {
+            "buyeruid": "awesome-user"
+          },
+          "tmax": 1000
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "awesome-resp-id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+                  "impid": "some-impression-id1",
+                  "price": 3.5,
+                  "adm": "awesome-markup",
+                  "adomain": [
+                    "awesome.com"
+                  ],
+                  "crid": "20",
+                  "w": 320,
+                  "h": 50,
+                  "mtype": 1
+                },
+                {
+                  "id": "a3ae1b4e2fc24a4fb45540082e98e162",
+                  "impid": "some-impression-id2",
+                  "price": 3.5,
+                  "adm": "awesome-markup",
+                  "adomain": [
+                    "awesome.com"
+                  ],
+                  "crid": "20",
+                  "w": 320,
+                  "h": 50,
+                  "mtype": 1
+                }
+              ],
+              "type": "banner",
+              "seat": "gothamads"
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "bids": [
+        {
+          "bid": {
+            "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+            "impid": "some-impression-id1",
+            "price": 3.5,
+            "adm": "awesome-markup",
+            "crid": "20",
+            "adomain": [
+              "awesome.com"
+            ],
+            "w": 320,
+            "h": 50,
+            "mtype": 1
+          },
+          "type": "banner"
+        },
+        {
+          "bid": {
+            "id": "a3ae1b4e2fc24a4fb45540082e98e162",
+            "impid": "some-impression-id2",
+            "price": 3.5,
+            "adm": "awesome-markup",
+            "crid": "20",
+            "adomain": [
+              "awesome.com"
+            ],
+            "w": 320,
+            "h": 50,
+            "mtype": 1
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/gothamads/gothamadstest/exemplary/native-app.json b/adapters/gothamads/gothamadstest/exemplary/native-app.json
new file mode 100644
index 00000000..5c54d526
--- /dev/null
+++ b/adapters/gothamads/gothamadstest/exemplary/native-app.json
@@ -0,0 +1,147 @@
+{
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "device": {
+      "ua": "test-user-agent",
+      "ip": "123.123.123.123",
+      "language": "en",
+      "dnt": 0
+    },
+    "tmax": 1000,
+    "user": {
+      "buyeruid": "awesome-user"
+    },
+    "app": {
+      "publisher": {
+        "id": "123456789"
+      },
+      "cat": [
+        "IAB22-1"
+      ],
+      "bundle": "com.app.awesome",
+      "name": "Awesome App",
+      "domain": "awesomeapp.com",
+      "id": "123456789"
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "tagid": "ogTAGID",
+        "native": {
+          "ver": "1.1",
+          "request": "{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}"
+        },
+        "ext": {
+          "bidder": {
+            "accountId": "accountId"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "headers": {
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "Accept": [
+            "application/json"
+          ],
+          "X-Openrtb-Version": [
+            "2.5"
+          ],
+          "User-Agent": [
+            "test-user-agent"
+          ],
+          "X-Forwarded-For": [
+            "123.123.123.123"
+          ]
+        },
+        "uri": "http://us-e-node1.gothamads.com/?pass=accountId",
+        "body": {
+          "id": "some-request-id",
+          "device": {
+            "ua": "test-user-agent",
+            "ip": "123.123.123.123",
+            "language": "en",
+            "dnt": 0
+          },
+          "imp": [
+            {
+              "id": "some-impression-id",
+              "native": {
+                "ver": "1.1",
+                "request": "{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}"
+              },
+              "tagid": "ogTAGID"
+            }
+          ],
+          "app": {
+            "id": "123456789",
+            "name": "Awesome App",
+            "bundle": "com.app.awesome",
+            "domain": "awesomeapp.com",
+            "cat": [
+              "IAB22-1"
+            ],
+            "publisher": {
+              "id": "123456789"
+            }
+          },
+          "user": {
+            "buyeruid": "awesome-user"
+          },
+          "tmax": 1000
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "awesome-resp-id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+                  "impid": "some-impression-id",
+                  "price": 3.5,
+                  "adm": "awesome-markup",
+                  "adomain": [
+                    "awesome.com"
+                  ],
+                  "crid": "20",
+                  "mtype": 4
+                }
+              ],
+              "type": "native",
+              "seat": "gothamads"
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "bids": [
+        {
+          "bid": {
+            "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+            "impid": "some-impression-id",
+            "price": 3.5,
+            "adm": "awesome-markup",
+            "crid": "20",
+            "adomain": [
+              "awesome.com"
+            ],
+            "mtype": 4
+          },
+          "type": "native"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/gothamads/gothamadstest/exemplary/native-web.json b/adapters/gothamads/gothamadstest/exemplary/native-web.json
new file mode 100644
index 00000000..a2f16e78
--- /dev/null
+++ b/adapters/gothamads/gothamadstest/exemplary/native-web.json
@@ -0,0 +1,134 @@
+{
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "device": {
+      "ua": "test-user-agent",
+      "ipv6": "2607:fb90:f27:4512:d800:cb23:a603:e245",
+      "language": "en",
+      "dnt": 0
+    },
+    "tmax": 1000,
+    "user": {
+      "buyeruid": "awesome-user"
+    },
+    "site": {
+      "page": "test.com",
+      "publisher": {
+        "id": "123456789"
+      }
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "tagid": "ogTAGID",
+        "native": {
+          "ver": "1.1",
+          "request": "{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}"
+        },
+        "ext": {
+          "bidder": {
+            "accountId": "accountId"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "headers": {
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "Accept": [
+            "application/json"
+          ],
+          "X-Openrtb-Version": [
+            "2.5"
+          ],
+          "User-Agent": [
+            "test-user-agent"
+          ],
+          "X-Forwarded-For": [
+            "2607:fb90:f27:4512:d800:cb23:a603:e245"
+          ]
+        },
+        "uri": "http://us-e-node1.gothamads.com/?pass=accountId",
+        "body": {
+          "id": "some-request-id",
+          "device": {
+            "ua": "test-user-agent",
+            "ipv6": "2607:fb90:f27:4512:d800:cb23:a603:e245",
+            "language": "en",
+            "dnt": 0
+          },
+          "imp": [
+            {
+              "id": "some-impression-id",
+              "tagid": "ogTAGID",
+              "native": {
+                "ver": "1.1",
+                "request": "{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}"
+              }
+            }
+          ],
+          "site": {
+            "page": "test.com",
+            "publisher": {
+              "id": "123456789"
+            }
+          },
+          "user": {
+            "buyeruid": "awesome-user"
+          },
+          "tmax": 1000
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "awesome-resp-id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+                  "impid": "some-impression-id",
+                  "price": 3.5,
+                  "adm": "awesome-markup",
+                  "adomain": [
+                    "awesome.com"
+                  ],
+                  "crid": "20",
+                  "mtype": 4
+                }
+              ],
+              "seat": "gothamads"
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "bids": [
+        {
+          "bid": {
+            "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+            "impid": "some-impression-id",
+            "price": 3.5,
+            "adm": "awesome-markup",
+            "crid": "20",
+            "adomain": [
+              "awesome.com"
+            ],
+            "mtype": 4
+          },
+          "type": "native"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/gothamads/gothamadstest/exemplary/video-app.json b/adapters/gothamads/gothamadstest/exemplary/video-app.json
new file mode 100644
index 00000000..a08c5a03
--- /dev/null
+++ b/adapters/gothamads/gothamadstest/exemplary/video-app.json
@@ -0,0 +1,160 @@
+{
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "device": {
+      "ua": "test-user-agent",
+      "ip": "123.123.123.123",
+      "language": "en",
+      "dnt": 0
+    },
+    "tmax": 1000,
+    "user": {
+      "buyeruid": "awesome-user"
+    },
+    "app": {
+      "publisher": {
+        "id": "123456789"
+      },
+      "cat": [
+        "IAB22-1"
+      ],
+      "bundle": "com.app.awesome",
+      "name": "Awesome App",
+      "domain": "awesomeapp.com",
+      "id": "123456789"
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "tagid": "ogTAGID",
+        "video": {
+          "mimes": [
+            "video/mp4"
+          ],
+          "w": 640,
+          "h": 480,
+          "minduration": 120,
+          "maxduration": 150
+        },
+        "ext": {
+          "bidder": {
+            "accountId": "accountId"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "headers": {
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "Accept": [
+            "application/json"
+          ],
+          "X-Openrtb-Version": [
+            "2.5"
+          ],
+          "User-Agent": [
+            "test-user-agent"
+          ],
+          "X-Forwarded-For": [
+            "123.123.123.123"
+          ]
+        },
+        "uri": "http://us-e-node1.gothamads.com/?pass=accountId",
+        "body": {
+          "id": "some-request-id",
+          "device": {
+            "ua": "test-user-agent",
+            "ip": "123.123.123.123",
+            "language": "en",
+            "dnt": 0
+          },
+          "imp": [
+            {
+              "id": "some-impression-id",
+              "video": {
+                "mimes": [
+                  "video/mp4"
+                ],
+                "minduration": 120,
+                "maxduration": 150,
+                "w": 640,
+                "h": 480
+              },
+              "tagid": "ogTAGID"
+            }
+          ],
+          "app": {
+            "id": "123456789",
+            "name": "Awesome App",
+            "bundle": "com.app.awesome",
+            "domain": "awesomeapp.com",
+            "cat": [
+              "IAB22-1"
+            ],
+            "publisher": {
+              "id": "123456789"
+            }
+          },
+          "user": {
+            "buyeruid": "awesome-user"
+          },
+          "tmax": 1000
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "awesome-resp-id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+                  "impid": "some-impression-id",
+                  "price": 3.5,
+                  "adm": "awesome-markup",
+                  "adomain": [
+                    "awesome.com"
+                  ],
+                  "crid": "20",
+                  "w": 1280,
+                  "h": 720,
+                  "mtype": 2
+                }
+              ],
+              "seat": "gothamads"
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "bids": [
+        {
+          "bid": {
+            "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+            "impid": "some-impression-id",
+            "price": 3.5,
+            "adm": "awesome-markup",
+            "crid": "20",
+            "adomain": [
+              "awesome.com"
+            ],
+            "w": 1280,
+            "h": 720,
+            "mtype": 2
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/gothamads/gothamadstest/exemplary/video-web.json b/adapters/gothamads/gothamadstest/exemplary/video-web.json
new file mode 100644
index 00000000..64293e74
--- /dev/null
+++ b/adapters/gothamads/gothamadstest/exemplary/video-web.json
@@ -0,0 +1,158 @@
+{
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "device": {
+      "ua": "test-user-agent",
+      "ip": "123.123.123.123",
+      "language": "en",
+      "dnt": 0
+    },
+    "tmax": 1000,
+    "user": {
+      "buyeruid": "awesome-user"
+    },
+    "site": {
+      "page": "test.com",
+      "publisher": {
+        "id": "123456789"
+      }
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "tagid": "ogTAGID",
+        "video": {
+          "mimes": [
+            "video/mp4"
+          ],
+          "w": 640,
+          "h": 480,
+          "minduration": 120,
+          "maxduration": 150
+        },
+        "ext": {
+          "bidder": {
+            "accountId": "accountId"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "headers": {
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "Accept": [
+            "application/json"
+          ],
+          "X-Openrtb-Version": [
+            "2.5"
+          ],
+          "User-Agent": [
+            "test-user-agent"
+          ],
+          "X-Forwarded-For": [
+            "123.123.123.123"
+          ]
+        },
+        "uri": "http://us-e-node1.gothamads.com/?pass=accountId",
+        "body": {
+          "id": "some-request-id",
+          "device": {
+            "ua": "test-user-agent",
+            "ip": "123.123.123.123",
+            "language": "en",
+            "dnt": 0
+          },
+          "imp": [
+            {
+              "id": "some-impression-id",
+              "tagid": "ogTAGID",
+              "video": {
+                "mimes": [
+                  "video/mp4"
+                ],
+                "minduration": 120,
+                "maxduration": 150,
+                "w": 640,
+                "h": 480
+              }
+            }
+          ],
+          "site": {
+            "page": "test.com",
+            "publisher": {
+              "id": "123456789"
+            }
+          },
+          "user": {
+            "buyeruid": "awesome-user"
+          },
+          "tmax": 1000
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "awesome-resp-id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+                  "impid": "some-impression-id",
+                  "price": 3.5,
+                  "adm": "awesome-markup",
+                  "adomain": [
+                    "awesome.com"
+                  ],
+                  "crid": "20",
+                  "w": 1280,
+                  "h": 720,
+                  "ext": {
+                    "prebid": {
+                      "type": "video"
+                    }
+                  },
+                  "mtype": 2
+                }
+              ],
+              "seat": "gothamads"
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "bids": [
+        {
+          "bid": {
+            "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+            "impid": "some-impression-id",
+            "price": 3.5,
+            "adm": "awesome-markup",
+            "adomain": [
+              "awesome.com"
+            ],
+            "crid": "20",
+            "w": 1280,
+            "h": 720,
+            "ext": {
+              "prebid": {
+                "type": "video"
+              }
+            },
+            "mtype": 2
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/gothamads/gothamadstest/supplemental/bad_media_type.json b/adapters/gothamads/gothamadstest/supplemental/bad_media_type.json
new file mode 100644
index 00000000..349ea5da
--- /dev/null
+++ b/adapters/gothamads/gothamadstest/supplemental/bad_media_type.json
@@ -0,0 +1,134 @@
+{
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "device": {
+      "ua": "test-user-agent",
+      "ip": "123.123.123.123",
+      "language": "en",
+      "dnt": 0
+    },
+    "tmax": 1000,
+    "user": {
+      "buyeruid": "awesome-user"
+    },
+    "app": {
+      "publisher": {
+        "id": "123456789"
+      },
+      "cat": [
+        "IAB22-1"
+      ],
+      "bundle": "com.app.awesome",
+      "name": "Awesome App",
+      "domain": "awesomeapp.com",
+      "id": "123456789"
+    },
+    "imp": [
+      {
+        "id": "1",
+        "tagid": "ogTAGID",
+        "banner": {
+          "w": 320,
+          "h": 50
+        },
+        "ext": {
+          "bidder": {
+            "accountId": "accountId"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "headers": {
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "Accept": [
+            "application/json"
+          ],
+          "X-Openrtb-Version": [
+            "2.5"
+          ],
+          "User-Agent": [
+            "test-user-agent"
+          ],
+          "X-Forwarded-For": [
+            "123.123.123.123"
+          ]
+        },
+        "uri": "http://us-e-node1.gothamads.com/?pass=accountId",
+        "body": {
+          "id": "some-request-id",
+          "device": {
+            "ua": "test-user-agent",
+            "ip": "123.123.123.123",
+            "language": "en",
+            "dnt": 0
+          },
+          "imp": [
+            {
+              "id": "1",
+              "banner": {
+                "w": 320,
+                "h": 50
+              },
+              "tagid": "ogTAGID"
+            }
+          ],
+          "app": {
+            "id": "123456789",
+            "name": "Awesome App",
+            "bundle": "com.app.awesome",
+            "domain": "awesomeapp.com",
+            "cat": [
+              "IAB22-1"
+            ],
+            "publisher": {
+              "id": "123456789"
+            }
+          },
+          "user": {
+            "buyeruid": "awesome-user"
+          },
+          "tmax": 1000
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "awesome-resp-id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "a3ae1b4e2fc24a4fb45540082e98e161",
+                  "impid": "test-imp-id",
+                  "price": 3.5,
+                  "adm": "awesome-markup",
+                  "adomain": [
+                    "awesome.com"
+                  ],
+                  "crid": "20",
+                  "w": 320,
+                  "h": 50
+                }
+              ],
+              "mtype": 0,
+              "seat": "gothamads"
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "unsupported MType 0",
+      "comparison": "literal"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/gothamads/gothamadstest/supplemental/empty-seatbid-array.json b/adapters/gothamads/gothamadstest/supplemental/empty-seatbid-array.json
new file mode 100644
index 00000000..50eafd98
--- /dev/null
+++ b/adapters/gothamads/gothamadstest/supplemental/empty-seatbid-array.json
@@ -0,0 +1,129 @@
+{
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "device": {
+      "ua": "test-user-agent",
+      "ip": "123.123.123.123",
+      "language": "en",
+      "dnt": 0
+    },
+    "tmax": 1000,
+    "user": {
+      "buyeruid": "awesome-user"
+    },
+    "app": {
+      "publisher": {
+        "id": "123456789"
+      },
+      "cat": [
+        "IAB22-1"
+      ],
+      "bundle": "com.app.awesome",
+      "name": "Awesome App",
+      "domain": "awesomeapp.com",
+      "id": "123456789"
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "tagid": "ogTAGID",
+        "video": {
+          "mimes": [
+            "video/mp4"
+          ],
+          "w": 640,
+          "h": 480,
+          "minduration": 120,
+          "maxduration": 150
+        },
+        "ext": {
+          "bidder": {
+            "accountId": "accountId"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "headers": {
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "Accept": [
+            "application/json"
+          ],
+          "X-Openrtb-Version": [
+            "2.5"
+          ],
+          "User-Agent": [
+            "test-user-agent"
+          ],
+          "X-Forwarded-For": [
+            "123.123.123.123"
+          ]
+        },
+        "uri": "http://us-e-node1.gothamads.com/?pass=accountId",
+        "body": {
+          "id": "some-request-id",
+          "device": {
+            "ua": "test-user-agent",
+            "ip": "123.123.123.123",
+            "language": "en",
+            "dnt": 0
+          },
+          "imp": [
+            {
+              "id": "some-impression-id",
+              "video": {
+                "mimes": [
+                  "video/mp4"
+                ],
+                "minduration": 120,
+                "maxduration": 150,
+                "w": 640,
+                "h": 480
+              },
+              "tagid": "ogTAGID"
+            }
+          ],
+          "app": {
+            "id": "123456789",
+            "name": "Awesome App",
+            "bundle": "com.app.awesome",
+            "domain": "awesomeapp.com",
+            "cat": [
+              "IAB22-1"
+            ],
+            "publisher": {
+              "id": "123456789"
+            }
+          },
+          "user": {
+            "buyeruid": "awesome-user"
+          },
+          "tmax": 1000
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "awesome-resp-id",
+          "seatbid": [],
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "mockResponse": {
+    "status": 200,
+    "body": "invalid response"
+  },
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Empty SeatBid array",
+      "comparison": "literal"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/gothamads/gothamadstest/supplemental/invalid-bidder-ext-object.json b/adapters/gothamads/gothamadstest/supplemental/invalid-bidder-ext-object.json
new file mode 100644
index 00000000..e412d39c
--- /dev/null
+++ b/adapters/gothamads/gothamadstest/supplemental/invalid-bidder-ext-object.json
@@ -0,0 +1,33 @@
+{
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "json: cannot unmarshal string into Go value of type openrtb_ext.ExtGothamAds",
+      "comparison": "literal"
+    }
+  ],
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "tagid": "my-adcode",
+        "video": {
+          "mimes": [
+            "video/mp4"
+          ],
+          "w": 640,
+          "h": 480,
+          "minduration": 120,
+          "maxduration": 150
+        },
+        "ext": {
+          "bidder": "bidderExt"
+        }
+      }
+    ],
+    "site": {
+      "page": "test.com"
+    }
+  },
+  "httpCalls": []
+}
\ No newline at end of file
diff --git a/adapters/gothamads/gothamadstest/supplemental/invalid-gotham-ext-object.json b/adapters/gothamads/gothamadstest/supplemental/invalid-gotham-ext-object.json
new file mode 100644
index 00000000..6075e2a4
--- /dev/null
+++ b/adapters/gothamads/gothamadstest/supplemental/invalid-gotham-ext-object.json
@@ -0,0 +1,31 @@
+{
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "json: cannot unmarshal string into Go value of type adapters.ExtImpBidder",
+      "comparison": "literal"
+    }
+  ],
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "tagid": "my-adcode",
+        "video": {
+          "mimes": [
+            "video/mp4"
+          ],
+          "w": 640,
+          "h": 480,
+          "minduration": 120,
+          "maxduration": 150
+        },
+        "ext": "wrongExt"
+      }
+    ],
+    "site": {
+      "page": "test.com"
+    }
+  },
+  "httpCalls": []
+}
\ No newline at end of file
diff --git a/adapters/gothamads/gothamadstest/supplemental/invalid-response.json b/adapters/gothamads/gothamadstest/supplemental/invalid-response.json
new file mode 100644
index 00000000..eb928b41
--- /dev/null
+++ b/adapters/gothamads/gothamadstest/supplemental/invalid-response.json
@@ -0,0 +1,111 @@
+{
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "device": {
+      "ua": "test-user-agent",
+      "ip": "123.123.123.123",
+      "language": "en",
+      "dnt": 0
+    },
+    "tmax": 1000,
+    "user": {
+      "buyeruid": "awesome-user"
+    },
+    "app": {
+      "publisher": {
+        "id": "123456789"
+      },
+      "cat": [
+        "IAB22-1"
+      ],
+      "bundle": "com.app.awesome",
+      "name": "Awesome App",
+      "domain": "awesomeapp.com",
+      "id": "123456789"
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "tagid": "ogTAGID",
+        "banner": {
+          "w": 320,
+          "h": 50
+        },
+        "ext": {
+          "bidder": {
+            "accountId": "accountId"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "headers": {
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "Accept": [
+            "application/json"
+          ],
+          "X-Openrtb-Version": [
+            "2.5"
+          ],
+          "User-Agent": [
+            "test-user-agent"
+          ],
+          "X-Forwarded-For": [
+            "123.123.123.123"
+          ]
+        },
+        "uri": "http://us-e-node1.gothamads.com/?pass=accountId",
+        "body": {
+          "id": "some-request-id",
+          "device": {
+            "ua": "test-user-agent",
+            "ip": "123.123.123.123",
+            "language": "en",
+            "dnt": 0
+          },
+          "imp": [
+            {
+              "id": "some-impression-id",
+              "banner": {
+                "w": 320,
+                "h": 50
+              },
+              "tagid": "ogTAGID"
+            }
+          ],
+          "app": {
+            "id": "123456789",
+            "name": "Awesome App",
+            "bundle": "com.app.awesome",
+            "domain": "awesomeapp.com",
+            "cat": [
+              "IAB22-1"
+            ],
+            "publisher": {
+              "id": "123456789"
+            }
+          },
+          "user": {
+            "buyeruid": "awesome-user"
+          },
+          "tmax": 1000
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": "invalid response"
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Bad Server Response",
+      "comparison": "literal"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/gothamads/gothamadstest/supplemental/status-code-bad-request.json b/adapters/gothamads/gothamadstest/supplemental/status-code-bad-request.json
new file mode 100644
index 00000000..37c047ed
--- /dev/null
+++ b/adapters/gothamads/gothamadstest/supplemental/status-code-bad-request.json
@@ -0,0 +1,92 @@
+{
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "tmax": 1000,
+    "user": {
+      "buyeruid": "awesome-user"
+    },
+    "app": {
+      "publisher": {
+        "id": "123456789"
+      },
+      "cat": [
+        "IAB22-1"
+      ],
+      "bundle": "com.app.awesome",
+      "name": "Awesome App",
+      "domain": "awesomeapp.com",
+      "id": "123456789"
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "tagid": "ogTAGID",
+        "video": {
+          "mimes": [
+            "video/mp4"
+          ],
+          "w": 640,
+          "h": 480,
+          "minduration": 120,
+          "maxduration": 150
+        },
+        "ext": {
+          "bidder": {
+            "accountId": "accountId"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://us-e-node1.gothamads.com/?pass=accountId",
+        "body": {
+          "id": "some-request-id",
+          "imp": [
+            {
+              "id": "some-impression-id",
+              "video": {
+                "mimes": [
+                  "video/mp4"
+                ],
+                "minduration": 120,
+                "maxduration": 150,
+                "w": 640,
+                "h": 480
+              },
+              "tagid": "ogTAGID"
+            }
+          ],
+          "app": {
+            "publisher": {
+              "id": "123456789"
+            },
+            "cat": [
+              "IAB22-1"
+            ],
+            "bundle": "com.app.awesome",
+            "name": "Awesome App",
+            "domain": "awesomeapp.com",
+            "id": "123456789"
+          },
+          "user": {
+            "buyeruid": "awesome-user"
+          },
+          "tmax": 1000
+        }
+      },
+      "mockResponse": {
+        "status": 400
+      }
+    }
+  ],
+  "expectedBidResponses": [],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Unexpected status code: 400. Run with request.debug = 1 for more info",
+      "comparison": "literal"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/gothamads/gothamadstest/supplemental/status-code-no-content.json b/adapters/gothamads/gothamadstest/supplemental/status-code-no-content.json
new file mode 100644
index 00000000..76a3a4fc
--- /dev/null
+++ b/adapters/gothamads/gothamadstest/supplemental/status-code-no-content.json
@@ -0,0 +1,75 @@
+{
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "tmax": 1000,
+    "user": {
+      "buyeruid": "awesome-user"
+    },
+    "site": {
+      "page": "test.com",
+      "publisher": {
+        "id": "123456789"
+      }
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "tagid": "ogTAGID",
+        "video": {
+          "mimes": [
+            "video/mp4"
+          ],
+          "w": 640,
+          "h": 480,
+          "minduration": 120,
+          "maxduration": 150
+        },
+        "ext": {
+          "bidder": {
+            "accountId": "accountId"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://us-e-node1.gothamads.com/?pass=accountId",
+        "body": {
+          "id": "some-request-id",
+          "imp": [
+            {
+              "id": "some-impression-id",
+              "tagid": "ogTAGID",
+              "video": {
+                "mimes": [
+                  "video/mp4"
+                ],
+                "minduration": 120,
+                "maxduration": 150,
+                "w": 640,
+                "h": 480
+              }
+            }
+          ],
+          "site": {
+            "page": "test.com",
+            "publisher": {
+              "id": "123456789"
+            }
+          },
+          "user": {
+            "buyeruid": "awesome-user"
+          },
+          "tmax": 1000
+        }
+      },
+      "mockResponse": {
+        "status": 204
+      }
+    }
+  ],
+  "expectedBidResponses": [],
+  "expectedMakeBidsErrors": []
+}
\ No newline at end of file
diff --git a/adapters/gothamads/gothamadstest/supplemental/status-code-other-error.json b/adapters/gothamads/gothamadstest/supplemental/status-code-other-error.json
new file mode 100644
index 00000000..45a0f67f
--- /dev/null
+++ b/adapters/gothamads/gothamadstest/supplemental/status-code-other-error.json
@@ -0,0 +1,80 @@
+{
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "tmax": 1000,
+    "user": {
+      "buyeruid": "awesome-user"
+    },
+    "site": {
+      "page": "test.com",
+      "publisher": {
+        "id": "123456789"
+      }
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "tagid": "ogTAGID",
+        "video": {
+          "mimes": [
+            "video/mp4"
+          ],
+          "w": 640,
+          "h": 480,
+          "minduration": 120,
+          "maxduration": 150
+        },
+        "ext": {
+          "bidder": {
+            "accountId": "accountId"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://us-e-node1.gothamads.com/?pass=accountId",
+        "body": {
+          "id": "some-request-id",
+          "imp": [
+            {
+              "id": "some-impression-id",
+              "tagid": "ogTAGID",
+              "video": {
+                "mimes": [
+                  "video/mp4"
+                ],
+                "minduration": 120,
+                "maxduration": 150,
+                "w": 640,
+                "h": 480
+              }
+            }
+          ],
+          "site": {
+            "page": "test.com",
+            "publisher": {
+              "id": "123456789"
+            }
+          },
+          "user": {
+            "buyeruid": "awesome-user"
+          },
+          "tmax": 1000
+        }
+      },
+      "mockResponse": {
+        "status": 306
+      }
+    }
+  ],
+  "expectedBidResponses": [],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Unexpected status code: 306. Run with request.debug = 1 for more info",
+      "comparison": "literal"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/gothamads/gothamadstest/supplemental/status-code-service-unavailable.json b/adapters/gothamads/gothamadstest/supplemental/status-code-service-unavailable.json
new file mode 100644
index 00000000..97773cd0
--- /dev/null
+++ b/adapters/gothamads/gothamadstest/supplemental/status-code-service-unavailable.json
@@ -0,0 +1,80 @@
+{
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "tmax": 1000,
+    "user": {
+      "buyeruid": "awesome-user"
+    },
+    "site": {
+      "page": "test.com",
+      "publisher": {
+        "id": "123456789"
+      }
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "tagid": "ogTAGID",
+        "video": {
+          "mimes": [
+            "video/mp4"
+          ],
+          "w": 640,
+          "h": 480,
+          "minduration": 120,
+          "maxduration": 150
+        },
+        "ext": {
+          "bidder": {
+            "accountId": "accountId"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://us-e-node1.gothamads.com/?pass=accountId",
+        "body": {
+          "id": "some-request-id",
+          "imp": [
+            {
+              "id": "some-impression-id",
+              "tagid": "ogTAGID",
+              "video": {
+                "mimes": [
+                  "video/mp4"
+                ],
+                "minduration": 120,
+                "maxduration": 150,
+                "w": 640,
+                "h": 480
+              }
+            }
+          ],
+          "site": {
+            "page": "test.com",
+            "publisher": {
+              "id": "123456789"
+            }
+          },
+          "user": {
+            "buyeruid": "awesome-user"
+          },
+          "tmax": 1000
+        }
+      },
+      "mockResponse": {
+        "status": 503
+      }
+    }
+  ],
+  "expectedBidResponses": [],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Something went wrong Status Code: [ 503 ] ",
+      "comparison": "literal"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/gothamads/params_test.go b/adapters/gothamads/params_test.go
new file mode 100644
index 00000000..1290216d
--- /dev/null
+++ b/adapters/gothamads/params_test.go
@@ -0,0 +1,50 @@
+package gothamads
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+var validParams = []string{
+	`{"accountId": "hash"}`,
+}
+
+func TestValidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json-schemas. %v", err)
+	}
+
+	for _, validParam := range validParams {
+		if err := validator.Validate(openrtb_ext.BidderGothamads, json.RawMessage(validParam)); err != nil {
+			t.Errorf("Schema rejected Gothamads params: %s", validParam)
+		}
+	}
+}
+
+var invalidParams = []string{
+	``,
+	`null`,
+	`true`,
+	`5`,
+	`4.2`,
+	`[]`,
+	`{}`,
+	`{"adCode": "string", "seatCode": 5, "originalPublisherid": "string"}`,
+	`{  "accountid": "" }`,
+}
+
+func TestInvalidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json-schemas. %v", err)
+	}
+
+	for _, invalidParam := range invalidParams {
+		if err := validator.Validate(openrtb_ext.BidderGothamads, json.RawMessage(invalidParam)); err == nil {
+			t.Errorf("Schema allowed unexpected params: %s", invalidParam)
+		}
+	}
+}
diff --git a/adapters/mobilefuse/mobilefusetest/exemplary/skadn.json b/adapters/mobilefuse/mobilefusetest/exemplary/skadn.json
new file mode 100644
index 00000000..e8fd66b1
--- /dev/null
+++ b/adapters/mobilefuse/mobilefusetest/exemplary/skadn.json
@@ -0,0 +1,78 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "1",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        }
+                    ]
+                },
+                "ext": {
+                    "skadn": {
+                        "versions": [
+                            "2.0",
+                            "2.1"
+                        ],
+                        "sourceapp": "11111",
+                        "skadnetids": [
+                            "424m5254lk.skadnetwork",
+                            "4fzdc2evr5.skadnetwork"
+                        ]
+                    },
+                    "bidder": {
+                        "placement_id": 999999,
+                        "pub_id": 1111,
+                        "tagid_src": "ext"
+                    }
+                }
+            }
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://mfx.mobilefuse.com/openrtb?pub_id=1111&tagid_src=ext",
+                "body": {
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "1",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 300,
+                                        "h": 250
+                                    }
+                                ]
+                            },
+                            "tagid": "999999",
+                            "ext": {
+                                "skadn": {
+                                    "versions": [
+                                        "2.0",
+                                        "2.1"
+                                    ],
+                                    "sourceapp": "11111",
+                                    "skadnetids": [
+                                        "424m5254lk.skadnetwork",
+                                        "4fzdc2evr5.skadnetwork"
+                                    ]
+                                }
+                            }
+                        }
+                    ]
+                }
+            },
+            "mockResponse": {
+                "status": 204,
+                "body": {}
+            }
+        }
+    ],
+    "expectedBidResponses": []
+}
\ No newline at end of file
diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go
index 37acf0c2..a69a2cdb 100644
--- a/endpoints/cookie_sync_test.go
+++ b/endpoints/cookie_sync_test.go
@@ -11,7 +11,6 @@ import (
 	"strings"
 	"testing"
 	"testing/iotest"
-	"time"
 
 	"github.com/prebid/prebid-server/analytics"
 	"github.com/prebid/prebid-server/config"
@@ -106,7 +105,7 @@ func TestCookieSyncHandle(t *testing.T) {
 	syncer.On("GetSync", syncTypeExpected, privacy.Policies{}).Return(sync, nil).Maybe()
 
 	cookieWithSyncs := usersync.NewCookie()
-	cookieWithSyncs.TrySync("foo", "anyID")
+	cookieWithSyncs.Sync("foo", "anyID")
 
 	testCases := []struct {
 		description              string
@@ -271,7 +270,9 @@ func TestCookieSyncHandle(t *testing.T) {
 
 		request := httptest.NewRequest("POST", "/cookiesync", test.givenBody)
 		if test.givenCookie != nil {
-			request.AddCookie(test.givenCookie.ToHTTPCookie(24 * time.Hour))
+			httpCookie, err := ToHTTPCookie(test.givenCookie)
+			assert.NoError(t, err)
+			request.AddCookie(httpCookie)
 		}
 
 		writer := httptest.NewRecorder()
@@ -1663,7 +1664,7 @@ func TestCookieSyncHandleResponse(t *testing.T) {
 
 		cookie := usersync.NewCookie()
 		if test.givenCookieHasSyncs {
-			if err := cookie.TrySync("foo", "anyID"); err != nil {
+			if err := cookie.Sync("foo", "anyID"); err != nil {
 				assert.FailNow(t, test.description+":set_cookie")
 			}
 		}
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index e277411b..c6604fcf 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -22,7 +22,6 @@ import (
 	"github.com/prebid/openrtb/v19/native1"
 	nativeRequests "github.com/prebid/openrtb/v19/native1/request"
 	"github.com/prebid/openrtb/v19/openrtb2"
-	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/analytics"
 	analyticsConf "github.com/prebid/prebid-server/analytics/config"
 	"github.com/prebid/prebid-server/config"
@@ -39,7 +38,6 @@ import (
 	"github.com/prebid/prebid-server/util/iputil"
 	"github.com/prebid/prebid-server/util/ptrutil"
 	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/mock"
 )
 
 const jsonFileExtension string = ".json"
@@ -5658,15 +5656,6 @@ func (e mockStageExecutor) GetOutcomes() []hookexecution.StageOutcome {
 	return e.outcomes
 }
 
-func TestRecordResponsePreparationMetrics(t *testing.T) {
-	mbi := map[openrtb_ext.BidderName]adapters.MakeBidsTimeInfo{
-		openrtb_ext.BidderAppnexus: {Durations: []time.Duration{10, 15}, AfterMakeBidsStartTime: time.Now()},
-	}
-	mockMetricEngine := &metrics.MetricsEngineMock{}
-	mockMetricEngine.On("RecordOverheadTime", metrics.MakeAuctionResponse, mock.Anything)
-	recordResponsePreparationMetrics(mbi, mockMetricEngine)
-}
-
 func TestSetSeatNonBidRaw(t *testing.T) {
 	type args struct {
 		request         *openrtb_ext.RequestWrapper
diff --git a/endpoints/setuid_test.go b/endpoints/setuid_test.go
index 609d8539..9176041e 100644
--- a/endpoints/setuid_test.go
+++ b/endpoints/setuid_test.go
@@ -13,6 +13,7 @@ import (
 
 	"github.com/prebid/prebid-server/analytics"
 	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/gdpr"
 	"github.com/prebid/prebid-server/metrics"
 	"github.com/prebid/prebid-server/openrtb_ext"
@@ -157,7 +158,7 @@ func TestSetUIDEndpoint(t *testing.T) {
 			expectedSyncs:          nil,
 			gdprAllowsHostCookies:  true,
 			expectedStatusCode:     http.StatusBadRequest,
-			expectedBody:           "gdpr_consent is required when gdpr=1",
+			expectedBody:           "GDPR consent is required when gdpr signal equals 1",
 			description:            "Return an error if GDPR is set to 1 but GDPR consent string is missing",
 		},
 		{
@@ -193,6 +194,28 @@ func TestSetUIDEndpoint(t *testing.T) {
 			expectedHeaders:        map[string]string{"Content-Type": "text/html", "Content-Length": "0"},
 			description:            "Should set uid for a bidder that is allowed by the GDPR consent string",
 		},
+		{
+			uri:                    "/setuid?bidder=pubmatic&uid=123&gpp_sid=2,4&gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+			syncersBidderNameToKey: map[string]string{"pubmatic": "pubmatic"},
+			gdprAllowsHostCookies:  true,
+			existingSyncs:          nil,
+			expectedSyncs:          map[string]string{"pubmatic": "123"},
+			expectedStatusCode:     http.StatusOK,
+			expectedHeaders:        map[string]string{"Content-Type": "text/html", "Content-Length": "0"},
+			description:            "Sets uid for a bidder allowed by GDPR consent string in the GPP query field",
+		},
+		{
+			uri: "/setuid?bidder=pubmatic&uid=123&gpp_sid=2,4&gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA" +
+				"gdpr=1&gdpr_consent=BONciguONcjGKADACHENAOLS1rAHDAFAAEAASABQAMwAeACEAFw",
+			syncersBidderNameToKey: map[string]string{"pubmatic": "pubmatic"},
+			gdprAllowsHostCookies:  true,
+			existingSyncs:          nil,
+			expectedSyncs:          map[string]string{"pubmatic": "123"},
+			expectedStatusCode:     http.StatusOK,
+			expectedBody:           "Warning: 'gpp' value will be used over the one found in the deprecated 'gdpr_consent' field.",
+			expectedHeaders:        map[string]string{"Content-Type": "text/plain; charset=utf-8"},
+			description:            "Sets uid for a bidder allowed by GDPR in GPP, throws warning because GDPR legacy values weren't used",
+		},
 		{
 			uri: "/setuid?bidder=pubmatic&uid=123&gdpr=1&gdpr_consent=" +
 				"malformed",
@@ -288,6 +311,597 @@ func TestSetUIDEndpoint(t *testing.T) {
 	}
 }
 
+func TestParseSignalFromGPPSID(t *testing.T) {
+	type testOutput struct {
+		signal gdpr.Signal
+		err    error
+	}
+	testCases := []struct {
+		desc     string
+		strSID   string
+		expected testOutput
+	}{
+		{
+			desc:   "Empty gpp_sid, expect gdpr.SignalAmbiguous",
+			strSID: "",
+			expected: testOutput{
+				signal: gdpr.SignalAmbiguous,
+				err:    nil,
+			},
+		},
+		{
+			desc:   "Malformed gpp_sid, expect gdpr.SignalAmbiguous",
+			strSID: "malformed",
+			expected: testOutput{
+				signal: gdpr.SignalAmbiguous,
+				err:    errors.New(`Error parsing gpp_sid strconv.ParseInt: parsing "malformed": invalid syntax`),
+			},
+		},
+		{
+			desc:   "Valid gpp_sid doesn't come with TCF2, expect gdpr.SignalNo",
+			strSID: "6",
+			expected: testOutput{
+				signal: gdpr.SignalNo,
+				err:    nil,
+			},
+		},
+		{
+			desc:   "Valid gpp_sid comes with TCF2, expect gdpr.SignalYes",
+			strSID: "2",
+			expected: testOutput{
+				signal: gdpr.SignalYes,
+				err:    nil,
+			},
+		},
+	}
+	for _, tc := range testCases {
+		outSignal, outErr := parseSignalFromGppSidStr(tc.strSID)
+
+		assert.Equal(t, tc.expected.signal, outSignal, tc.desc)
+		assert.Equal(t, tc.expected.err, outErr, tc.desc)
+	}
+}
+
+func TestParseConsentFromGppStr(t *testing.T) {
+	type testOutput struct {
+		gdprConsent string
+		err         error
+	}
+	testCases := []struct {
+		desc       string
+		inGppQuery string
+		expected   testOutput
+	}{
+		{
+			desc:       "Empty gpp field, expect empty GDPR consent",
+			inGppQuery: "",
+			expected: testOutput{
+				gdprConsent: "",
+				err:         nil,
+			},
+		},
+		{
+			desc:       "Malformed gpp field value, expect empty GDPR consent and error",
+			inGppQuery: "malformed",
+			expected: testOutput{
+				gdprConsent: "",
+				err:         errors.New(`error parsing GPP header, base64 decoding: illegal base64 data at input byte 8`),
+			},
+		},
+		{
+			desc:       "Valid gpp string comes with TCF2 in its gppConstants.SectionID's, expect non-empty GDPR consent",
+			inGppQuery: "DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+			expected: testOutput{
+				gdprConsent: "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+				err:         nil,
+			},
+		},
+		{
+			desc:       "Valid gpp string doesn't come with TCF2 in its gppConstants.SectionID's, expect blank GDPR consent",
+			inGppQuery: "DBABjw~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN",
+			expected: testOutput{
+				gdprConsent: "",
+				err:         nil,
+			},
+		},
+	}
+	for _, tc := range testCases {
+		outConsent, outErr := parseConsentFromGppStr(tc.inGppQuery)
+
+		assert.Equal(t, tc.expected.gdprConsent, outConsent, tc.desc)
+		assert.Equal(t, tc.expected.err, outErr, tc.desc)
+	}
+}
+
+func TestParseGDPRFromGPP(t *testing.T) {
+	type testOutput struct {
+		reqInfo gdpr.RequestInfo
+		err     error
+	}
+	type aTest struct {
+		desc     string
+		inUri    string
+		expected testOutput
+	}
+	testGroups := []struct {
+		groupDesc string
+		testCases []aTest
+	}{
+		{
+			groupDesc: "No gpp_sid nor gpp",
+			testCases: []aTest{
+				{
+					desc:  "Input URL is mising gpp_sid and gpp, expect signal ambiguous and no error",
+					inUri: "/setuid?bidder=pubmatic&uid=123",
+					expected: testOutput{
+						reqInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous},
+						err:     nil,
+					},
+				},
+			},
+		},
+		{
+			groupDesc: "gpp only",
+			testCases: []aTest{
+				{
+					desc:  "gpp is malformed, expect error",
+					inUri: "/setuid?gpp=malformed",
+					expected: testOutput{
+						reqInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous},
+						err:     errors.New("error parsing GPP header, base64 decoding: illegal base64 data at input byte 8"),
+					},
+				},
+				{
+					desc:  "gpp with a valid TCF2 value. Expect valid consent string and no error",
+					inUri: "/setuid?gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+					expected: testOutput{
+						reqInfo: gdpr.RequestInfo{
+							GDPRSignal: gdpr.SignalAmbiguous,
+							Consent:    "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+						},
+						err: nil,
+					},
+				},
+				{
+					desc:  "gpp does not include TCF2 string. Expect empty consent string and no error",
+					inUri: "/setuid?gpp=DBABjw~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN",
+					expected: testOutput{
+						reqInfo: gdpr.RequestInfo{
+							GDPRSignal: gdpr.SignalAmbiguous,
+							Consent:    "",
+						},
+						err: nil,
+					},
+				},
+			},
+		},
+		{
+			groupDesc: "gpp_sid only",
+			testCases: []aTest{
+				{
+					desc:  "gpp_sid is malformed, expect error",
+					inUri: "/setuid?gpp_sid=malformed",
+					expected: testOutput{
+						reqInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous},
+						err:     errors.New("Error parsing gpp_sid strconv.ParseInt: parsing \"malformed\": invalid syntax"),
+					},
+				},
+				{
+					desc:  "TCF2 found in gpp_sid list. Given that the consent string will be empty, expect an error",
+					inUri: "/setuid?gpp_sid=2,6",
+					expected: testOutput{
+						reqInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalYes},
+						err:     nil,
+					},
+				},
+				{
+					desc:  "TCF2 not found in gpp_sid list. Expect SignalNo and no error",
+					inUri: "/setuid?gpp_sid=6,8",
+					expected: testOutput{
+						reqInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalNo},
+						err:     nil,
+					},
+				},
+			},
+		},
+		{
+			groupDesc: "both gpp_sid and gpp",
+			testCases: []aTest{
+				{
+					desc:  "TCF2 found in gpp_sid list and gpp has a valid GDPR string. Expect no error",
+					inUri: "/setuid?gpp_sid=2,6&gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+					expected: testOutput{
+						reqInfo: gdpr.RequestInfo{
+							GDPRSignal: gdpr.SignalYes,
+							Consent:    "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+						},
+						err: nil,
+					},
+				},
+			},
+		},
+	}
+	for _, tgroup := range testGroups {
+		for _, tc := range tgroup.testCases {
+			// set test
+			testURL, err := url.Parse(tc.inUri)
+			assert.NoError(t, err, "%s - %s", tgroup.groupDesc, tc.desc)
+
+			query := testURL.Query()
+
+			// run
+			outReqInfo, outErr := parseGDPRFromGPP(query)
+
+			// assertions
+			assert.Equal(t, tc.expected.reqInfo, outReqInfo, "%s - %s", tgroup.groupDesc, tc.desc)
+			assert.Equal(t, tc.expected.err, outErr, "%s - %s", tgroup.groupDesc, tc.desc)
+		}
+	}
+}
+
+func TestParseLegacyGDPRFields(t *testing.T) {
+	type testInput struct {
+		uri            string
+		gppGDPRSignal  gdpr.Signal
+		gppGDPRConsent string
+	}
+	type testOutput struct {
+		signal  gdpr.Signal
+		consent string
+		err     error
+	}
+	testCases := []struct {
+		desc     string
+		in       testInput
+		expected testOutput
+	}{
+		{
+			desc: `both "gdpr" and "gdpr_consent" missing from URI, expect SignalAmbiguous, blank consent and no error`,
+			in: testInput{
+				uri: "/setuid?bidder=pubmatic&uid=123",
+			},
+			expected: testOutput{
+				signal:  gdpr.SignalAmbiguous,
+				consent: "",
+				err:     nil,
+			},
+		},
+		{
+			desc: `invalid "gdpr" value, expect SignalAmbiguous, blank consent and error`,
+			in: testInput{
+				uri:           "/setuid?gdpr=2",
+				gppGDPRSignal: gdpr.SignalAmbiguous,
+			},
+			expected: testOutput{
+				signal:  gdpr.SignalAmbiguous,
+				consent: "",
+				err:     errors.New("the gdpr query param must be either 0 or 1. You gave 2"),
+			},
+		},
+		{
+			desc: `valid "gdpr" value but valid GDPRSignal was previously parsed before, expect SignalAmbiguous, blank consent and a warning`,
+			in: testInput{
+				uri:           "/setuid?gdpr=1",
+				gppGDPRSignal: gdpr.SignalYes,
+			},
+			expected: testOutput{
+				signal:  gdpr.SignalAmbiguous,
+				consent: "",
+				err: &errortypes.Warning{
+					Message:     "'gpp_sid' signal value will be used over the one found in the deprecated 'gdpr' field.",
+					WarningCode: errortypes.UnknownWarningCode,
+				},
+			},
+		},
+		{
+			desc: `valid "gdpr_consent" value but valid GDPRSignal was previously parsed before, expect SignalAmbiguous, blank consent and a warning`,
+			in: testInput{
+				uri:            "/setuid?gdpr_consent=someConsent",
+				gppGDPRConsent: "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+			},
+			expected: testOutput{
+				signal:  gdpr.SignalAmbiguous,
+				consent: "",
+				err: &errortypes.Warning{
+					Message:     "'gpp' value will be used over the one found in the deprecated 'gdpr_consent' field.",
+					WarningCode: errortypes.UnknownWarningCode,
+				},
+			},
+		},
+	}
+	for _, tc := range testCases {
+		// set test
+		testURL, err := url.Parse(tc.in.uri)
+		assert.NoError(t, err, tc.desc)
+
+		query := testURL.Query()
+
+		// run
+		outSignal, outConsent, outErr := parseLegacyGDPRFields(query, tc.in.gppGDPRSignal, tc.in.gppGDPRConsent)
+
+		// assertions
+		assert.Equal(t, tc.expected.signal, outSignal, tc.desc)
+		assert.Equal(t, tc.expected.consent, outConsent, tc.desc)
+		assert.Equal(t, tc.expected.err, outErr, tc.desc)
+	}
+}
+
+func TestExtractGDPRInfo(t *testing.T) {
+	type testOutput struct {
+		requestInfo gdpr.RequestInfo
+		err         error
+	}
+	type testCase struct {
+		desc     string
+		inUri    string
+		expected testOutput
+	}
+	testSuite := []struct {
+		sDesc string
+		tests []testCase
+	}{
+		{
+			sDesc: "no gdpr nor gpp values in query",
+			tests: []testCase{
+				{
+					desc:  "expect blank consent, signalNo and nil error",
+					inUri: "/setuid?bidder=pubmatic&uid=123",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "",
+							GDPRSignal: gdpr.SignalAmbiguous,
+						},
+						err: nil,
+					},
+				},
+			},
+		},
+		{
+			sDesc: "missing gpp, gdpr only",
+			tests: []testCase{
+				{
+					desc:  "Invalid gdpr signal value in query, expect blank request info and error",
+					inUri: "/setuid?gdpr=2",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous},
+						err:         errors.New("the gdpr query param must be either 0 or 1. You gave 2"),
+					},
+				},
+				{
+					desc:  "GDPR equals 0, blank consent, expect blank consent, signalNo and nil error",
+					inUri: "/setuid?gdpr=0",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalNo},
+						err:         nil,
+					},
+				},
+				{
+					desc:  "GDPR equals 1, blank consent, expect blank request info and error",
+					inUri: "/setuid?gdpr=1",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous},
+						err:         errors.New("GDPR consent is required when gdpr signal equals 1"),
+					},
+				},
+				{
+					desc:  "GDPR equals 0, non-blank consent, expect non-blank request info and nil error",
+					inUri: "/setuid?gdpr=0&gdpr_consent=someConsent",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "someConsent",
+							GDPRSignal: gdpr.SignalNo,
+						},
+						err: nil,
+					},
+				},
+				{
+					desc:  "GDPR equals 1, non-blank consent, expect non-blank request info and nil error",
+					inUri: "/setuid?gdpr=1&gdpr_consent=someConsent",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "someConsent",
+							GDPRSignal: gdpr.SignalYes,
+						},
+						err: nil,
+					},
+				},
+			},
+		},
+		{
+			sDesc: "missing gdpr, gpp only",
+			tests: []testCase{
+				{
+					desc:  "Malformed GPP_SID string, expect blank request info and error",
+					inUri: "/setuid?gpp_sid=malformed",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous},
+						err:         errors.New("Error parsing gpp_sid strconv.ParseInt: parsing \"malformed\": invalid syntax"),
+					},
+				},
+				{
+					desc:  "Valid GPP_SID string but invalid GPP string in query, expect blank request info and error",
+					inUri: "/setuid?gpp=malformed&gpp_sid=2",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous},
+						err:         errors.New("error parsing GPP header, base64 decoding: illegal base64 data at input byte 8"),
+					},
+				},
+				{
+					desc:  "SectionTCFEU2 not found in GPP string, expect blank consent and signalAmbiguous",
+					inUri: "/setuid?gpp=DBABBgA~xlgWEYCZAA",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "",
+							GDPRSignal: gdpr.SignalAmbiguous,
+						},
+						err: nil,
+					},
+				},
+				{
+					desc:  "No GPP string, nor SectionTCFEU2 found in SID list in query, expect blank consent and signalAmbiguous",
+					inUri: "/setuid?gpp_sid=3,6",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "",
+							GDPRSignal: gdpr.SignalNo,
+						},
+						err: nil,
+					},
+				},
+				{
+					desc:  "No GPP string, SectionTCFEU2 found in SID list in query, expect blank request info and error",
+					inUri: "/setuid?gpp_sid=2",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous},
+						err:         errors.New("GDPR consent is required when gdpr signal equals 1"),
+					},
+				},
+				{
+					desc:  "SectionTCFEU2 only found in SID list, expect blank request info and error",
+					inUri: "/setuid?gpp=DBABBgA~xlgWEYCZAA&gpp_sid=2",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous},
+						err:         errors.New("GDPR consent is required when gdpr signal equals 1"),
+					},
+				},
+				{
+					desc:  "SectionTCFEU2 found in GPP string but SID list is nil, expect valid consent and SignalAmbiguous",
+					inUri: "/setuid?gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+							GDPRSignal: gdpr.SignalAmbiguous,
+						},
+						err: nil,
+					},
+				},
+				{
+					desc:  "SectionTCFEU2 found in GPP string but not in the non-nil SID list, expect valid consent and signalNo",
+					inUri: "/setuid?gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA&gpp_sid=6",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+							GDPRSignal: gdpr.SignalNo,
+						},
+						err: nil,
+					},
+				},
+				{
+					desc:  "SectionTCFEU2 found both in GPP string and SID list, expect valid consent and signalYes",
+					inUri: "/setuid?gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA&gpp_sid=2,4",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+							GDPRSignal: gdpr.SignalYes,
+						},
+						err: nil,
+					},
+				},
+			},
+		},
+		{
+			sDesc: "GPP values take priority over GDPR",
+			tests: []testCase{
+				{
+					desc:  "SignalNo in gdpr field but SignalYes in SID list, CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA consent in gpp but legacyConsent in gdpr_consent, expect GPP values to prevail",
+					inUri: "/setuid?gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA&gpp_sid=2,4&gdpr=0&gdpr_consent=legacyConsent",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+							GDPRSignal: gdpr.SignalYes,
+						},
+						err: &errortypes.Warning{
+							Message:     "'gpp' value will be used over the one found in the deprecated 'gdpr_consent' field.",
+							WarningCode: errortypes.UnknownWarningCode,
+						},
+					},
+				},
+				{
+					desc:  "SignalNo in gdpr field but SignalYes in SID list because SectionTCFEU2 is listed, expect GPP to prevail",
+					inUri: "/setuid?gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA&gpp_sid=2,4&gdpr=0",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+							GDPRSignal: gdpr.SignalYes,
+						},
+						err: &errortypes.Warning{
+							Message:     "'gpp_sid' signal value will be used over the one found in the deprecated 'gdpr' field.",
+							WarningCode: errortypes.UnknownWarningCode,
+						},
+					},
+				},
+				{
+					desc:  "No gpp string in URL query, use gdpr_consent and SignalYes found in SID list because SectionTCFEU2 is listed",
+					inUri: "/setuid?gpp_sid=2,4&gdpr_consent=legacyConsent",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "",
+							GDPRSignal: gdpr.SignalAmbiguous,
+						},
+						err: errors.New("GDPR consent is required when gdpr signal equals 1"),
+					},
+				},
+				{
+					desc:  "SectionTCFEU2 not found in GPP string but found in SID list, choose the GDPR_CONSENT and GPP_SID signal",
+					inUri: "/setuid?gpp=DBABBgA~xlgWEYCZAA&gpp_sid=2&gdpr=0&gdpr_consent=legacyConsent",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "",
+							GDPRSignal: gdpr.SignalAmbiguous,
+						},
+						err: errors.New("GDPR consent is required when gdpr signal equals 1"),
+					},
+				},
+				{
+					desc:  "SectionTCFEU2 found in GPP string but not in SID list, choose GDPR signal GPP consent value",
+					inUri: "/setuid?gpp=DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA&gpp_sid=6&gdpr=1&gdpr_consent=legacyConsent",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+							GDPRSignal: gdpr.SignalNo,
+						},
+						err: &errortypes.Warning{
+							Message:     "'gpp' value will be used over the one found in the deprecated 'gdpr_consent' field.",
+							WarningCode: errortypes.UnknownWarningCode,
+						},
+					},
+				},
+				{
+					desc:  "SectionTCFEU2 not found in GPP, use GDPR_CONSENT value. SignalYes found in gdpr field, but not in the valid SID list, expect SignalNo",
+					inUri: "/setuid?gpp=DBABBgA~xlgWEYCZAA&gpp_sid=6&gdpr=1&gdpr_consent=legacyConsent",
+					expected: testOutput{
+						requestInfo: gdpr.RequestInfo{
+							Consent:    "",
+							GDPRSignal: gdpr.SignalNo,
+						},
+						err: &errortypes.Warning{
+							Message:     "'gpp_sid' signal value will be used over the one found in the deprecated 'gdpr' field.",
+							WarningCode: errortypes.UnknownWarningCode,
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for _, ts := range testSuite {
+		for _, tc := range ts.tests {
+			// set test
+			testURL, err := url.Parse(tc.inUri)
+			assert.NoError(t, err, tc.desc)
+
+			query := testURL.Query()
+
+			// run
+			outReqInfo, outErr := extractGDPRInfo(query)
+
+			// assertions
+			assert.Equal(t, tc.expected.requestInfo, outReqInfo, tc.desc)
+			assert.Equal(t, tc.expected.err, outErr, tc.desc)
+		}
+	}
+}
+
 func TestSetUIDEndpointMetrics(t *testing.T) {
 	cookieWithOptOut := usersync.NewCookie()
 	cookieWithOptOut.SetOptOut(true)
@@ -425,7 +1039,7 @@ func TestSetUIDEndpointMetrics(t *testing.T) {
 					Status:  400,
 					Bidder:  "pubmatic",
 					UID:     "",
-					Errors:  []error{errors.New("gdpr_consent is required when gdpr=1")},
+					Errors:  []error{errors.New("GDPR consent is required when gdpr signal equals 1")},
 					Success: false,
 				}
 				a.On("LogSetUIDObject", &expected).Once()
@@ -697,7 +1311,7 @@ func makeRequest(uri string, existingSyncs map[string]string) *http.Request {
 	if len(existingSyncs) > 0 {
 		pbsCookie := usersync.NewCookie()
 		for key, value := range existingSyncs {
-			pbsCookie.TrySync(key, value)
+			pbsCookie.Sync(key, value)
 		}
 		addCookie(request, pbsCookie)
 	}
@@ -749,10 +1363,12 @@ func doRequest(req *http.Request, analytics analytics.PBSAnalyticsModule, metric
 }
 
 func addCookie(req *http.Request, cookie *usersync.Cookie) {
-	req.AddCookie(cookie.ToHTTPCookie(time.Duration(1) * time.Hour))
+	httpCookie, _ := ToHTTPCookie(cookie)
+	req.AddCookie(httpCookie)
 }
 
 func parseCookieString(t *testing.T, response *httptest.ResponseRecorder) *usersync.Cookie {
+	decoder := usersync.Base64Decoder{}
 	cookieString := response.Header().Get("Set-Cookie")
 	parser := regexp.MustCompile("uids=(.*?);")
 	res := parser.FindStringSubmatch(cookieString)
@@ -761,7 +1377,7 @@ func parseCookieString(t *testing.T, response *httptest.ResponseRecorder) *users
 		Name:  "uids",
 		Value: res[1],
 	}
-	return usersync.ParseCookie(&httpCookie)
+	return decoder.Decode(httpCookie.Value)
 }
 
 type fakePermissionsBuilder struct {
@@ -830,3 +1446,18 @@ func (s fakeSyncer) SupportsType(syncTypes []usersync.SyncType) bool {
 func (s fakeSyncer) GetSync(syncTypes []usersync.SyncType, privacyPolicies privacy.Policies) (usersync.Sync, error) {
 	return usersync.Sync{}, nil
 }
+
+func ToHTTPCookie(cookie *usersync.Cookie) (*http.Cookie, error) {
+	encoder := usersync.Base64Encoder{}
+	encodedCookie, err := encoder.Encode(cookie)
+	if err != nil {
+		return nil, nil
+	}
+
+	return &http.Cookie{
+		Name:    uidCookieName,
+		Value:   encodedCookie,
+		Expires: time.Now().Add((90 * 24 * time.Hour)),
+		Path:    "/",
+	}, nil
+}
diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go
index 87d04185..9d36808c 100644
--- a/exchange/bidder_test.go
+++ b/exchange/bidder_test.go
@@ -110,11 +110,10 @@ func TestSingleBidder(t *testing.T) {
 			bidAdjustments:      bidAdjustments,
 		}
 		extraInfo := &adapters.ExtraRequestInfo{}
-		seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
-		assert.NotEmpty(t, extraInfo.MakeBidsTimeInfo.Durations)
-		assert.False(t, extraInfo.MakeBidsTimeInfo.AfterMakeBidsStartTime.IsZero())
+		seatBids, extraBidderRespInfo, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
 
 		assert.Len(t, seatBids, 1)
+		assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero())
 		seatBid := seatBids[0]
 
 		// Make sure the goodSingleBidder was called with the expected arguments.
@@ -236,10 +235,9 @@ func TestSingleBidderGzip(t *testing.T) {
 			bidAdjustments:      bidAdjustments,
 		}
 		extraInfo := &adapters.ExtraRequestInfo{}
-		seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
-		assert.NotEmpty(t, extraInfo.MakeBidsTimeInfo.Durations)
-		assert.False(t, extraInfo.MakeBidsTimeInfo.AfterMakeBidsStartTime.IsZero())
+		seatBids, extraBidderRespInfo, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
 		assert.Len(t, seatBids, 1)
+		assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero())
 		seatBid := seatBids[0]
 
 		// Make sure the goodSingleBidder was called with the expected arguments.
@@ -339,7 +337,7 @@ func TestRequestBidRemovesSensitiveHeaders(t *testing.T) {
 		bidAdjustments:      bidAdjustments,
 	}
 	extraInfo := &adapters.ExtraRequestInfo{}
-	seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
+	seatBids, extraBidderRespInfo, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
 	expectedHttpCalls := []*openrtb_ext.ExtHttpCall{
 		{
 			Uri:            server.URL,
@@ -352,8 +350,7 @@ func TestRequestBidRemovesSensitiveHeaders(t *testing.T) {
 
 	assert.Empty(t, errs)
 	assert.Len(t, seatBids, 1)
-	assert.NotEmpty(t, extraInfo.MakeBidsTimeInfo.Durations)
-	assert.False(t, extraInfo.MakeBidsTimeInfo.AfterMakeBidsStartTime.IsZero())
+	assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero())
 	assert.ElementsMatch(t, seatBids[0].HttpCalls, expectedHttpCalls)
 }
 
@@ -393,7 +390,7 @@ func TestSetGPCHeader(t *testing.T) {
 		bidAdjustments:      bidAdjustments,
 	}
 	extraInfo := &adapters.ExtraRequestInfo{GlobalPrivacyControlHeader: "1"}
-	seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
+	seatBids, extraBidderRespInfo, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
 
 	expectedHttpCall := []*openrtb_ext.ExtHttpCall{
 		{
@@ -407,8 +404,7 @@ func TestSetGPCHeader(t *testing.T) {
 
 	assert.Empty(t, errs)
 	assert.Len(t, seatBids, 1)
-	assert.NotEmpty(t, extraInfo.MakeBidsTimeInfo.Durations)
-	assert.False(t, extraInfo.MakeBidsTimeInfo.AfterMakeBidsStartTime.IsZero())
+	assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero())
 	assert.ElementsMatch(t, seatBids[0].HttpCalls, expectedHttpCall)
 }
 
@@ -446,7 +442,7 @@ func TestSetGPCHeaderNil(t *testing.T) {
 		bidAdjustments:      bidAdjustments,
 	}
 	extraInfo := &adapters.ExtraRequestInfo{GlobalPrivacyControlHeader: "1"}
-	seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
+	seatBids, extraBidderRespInfo, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
 
 	expectedHttpCall := []*openrtb_ext.ExtHttpCall{
 		{
@@ -459,9 +455,8 @@ func TestSetGPCHeaderNil(t *testing.T) {
 	}
 
 	assert.Empty(t, errs)
-	assert.NotEmpty(t, extraInfo.MakeBidsTimeInfo.Durations)
-	assert.False(t, extraInfo.MakeBidsTimeInfo.AfterMakeBidsStartTime.IsZero())
 	assert.Len(t, seatBids, 1)
+	assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero())
 	assert.ElementsMatch(t, seatBids[0].HttpCalls, expectedHttpCall)
 }
 
@@ -518,7 +513,7 @@ func TestMultiBidder(t *testing.T) {
 		addCallSignHeader:   false,
 		bidAdjustments:      bidAdjustments,
 	}
-	seatBids, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
+	seatBids, extraBidderRespInfo, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
 
 	if len(seatBids) != 1 {
 		t.Fatalf("SeatBid should exist, because bids exist.")
@@ -530,6 +525,7 @@ func TestMultiBidder(t *testing.T) {
 	if len(seatBids[0].Bids) != len(bidderImpl.httpResponses)*len(mockBidderResponse.Bids) {
 		t.Errorf("Expected %d bids. Got %d", len(bidderImpl.httpResponses)*len(mockBidderResponse.Bids), len(seatBids[0].Bids))
 	}
+	assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero())
 
 }
 
@@ -885,7 +881,7 @@ func TestMultiCurrencies(t *testing.T) {
 			BidderName: openrtb_ext.BidderAppnexus,
 		}
 		bidAdjustments := map[string]float64{string(openrtb_ext.BidderAppnexus): 1}
-		seatBids, errs := bidder.requestBid(
+		seatBids, extraBidderRespInfo, errs := bidder.requestBid(
 			context.Background(),
 			bidderReq,
 			currencyConverter.Rates(),
@@ -903,6 +899,7 @@ func TestMultiCurrencies(t *testing.T) {
 		)
 		assert.Len(t, seatBids, 1)
 		seatBid := seatBids[0]
+		assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero())
 
 		// Verify:
 		resultLightBids := make([]bid, len(seatBid.Bids))
@@ -1045,7 +1042,7 @@ func TestMultiCurrencies_RateConverterNotSet(t *testing.T) {
 			BidderName: "test",
 		}
 		bidAdjustments := map[string]float64{"test": 1}
-		seatBids, errs := bidder.requestBid(
+		seatBids, extraBidderRespInfo, errs := bidder.requestBid(
 			context.Background(),
 			bidderReq,
 			currencyConverter.Rates(),
@@ -1068,6 +1065,7 @@ func TestMultiCurrencies_RateConverterNotSet(t *testing.T) {
 		assert.Equal(t, false, (seatBid == nil && tc.expectedBidsCount != 0), tc.description)
 		assert.Equal(t, tc.expectedBidsCount, uint(len(seatBid.Bids)), tc.description)
 		assert.ElementsMatch(t, tc.expectedBadCurrencyErrors, errs, tc.description)
+		assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero())
 	}
 }
 
@@ -1224,7 +1222,7 @@ func TestMultiCurrencies_RequestCurrencyPick(t *testing.T) {
 			BidderName: "test",
 		}
 		bidAdjustments := map[string]float64{"test": 1}
-		seatBids, errs := bidder.requestBid(
+		seatBids, extraBidderRespInfo, errs := bidder.requestBid(
 			context.Background(),
 			bidderReq,
 			currencyConverter.Rates(),
@@ -1249,6 +1247,7 @@ func TestMultiCurrencies_RequestCurrencyPick(t *testing.T) {
 		} else {
 			assert.Nil(t, errs, tc.description)
 			assert.Equal(t, tc.expectedPickedCurrency, seatBid.Currency, tc.description)
+			assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero())
 		}
 	}
 }
@@ -1542,7 +1541,7 @@ func TestMobileNativeTypes(t *testing.T) {
 			BidderName: "test",
 		}
 		bidAdjustments := map[string]float64{"test": 1.0}
-		seatBids, _ := bidder.requestBid(
+		seatBids, extraBidderRespInfo, _ := bidder.requestBid(
 			context.Background(),
 			bidderReq,
 			currencyConverter.Rates(),
@@ -1559,7 +1558,7 @@ func TestMobileNativeTypes(t *testing.T) {
 			nil,
 		)
 		assert.Len(t, seatBids, 1)
-
+		assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero())
 		var actualValue string
 		for _, bid := range seatBids[0].Bids {
 			actualValue = bid.Bid.AdM
@@ -1662,7 +1661,7 @@ func TestRequestBidsStoredBidResponses(t *testing.T) {
 			ImpReplaceImpId:       tc.impReplaceImpId,
 		}
 		bidAdjustments := map[string]float64{string(openrtb_ext.BidderAppnexus): 1.0}
-		seatBids, _ := bidder.requestBid(
+		seatBids, extraBidderRespInfo, _ := bidder.requestBid(
 			context.Background(),
 			bidderReq,
 			currencyConverter.Rates(),
@@ -1679,6 +1678,7 @@ func TestRequestBidsStoredBidResponses(t *testing.T) {
 			nil,
 		)
 		assert.Len(t, seatBids, 1)
+		assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero())
 
 		assert.Len(t, seatBids[0].Bids, len(tc.expectedBidIds), "Incorrect bids number for test case ", tc.description)
 		for _, bid := range seatBids[0].Bids {
@@ -1777,7 +1777,7 @@ func TestFledge(t *testing.T) {
 			},
 			BidderName: "openx",
 		}
-		seatBids, _ := bidder.requestBid(
+		seatBids, extraBidderRespInfo, _ := bidder.requestBid(
 			context.Background(),
 			bidderReq,
 			currencyConverter.Rates(),
@@ -1798,6 +1798,7 @@ func TestFledge(t *testing.T) {
 		assert.Len(t, seatBids[0].FledgeAuctionConfigs, len(tc.expectedFledge))
 
 		assert.ElementsMatch(t, seatBids[0].FledgeAuctionConfigs, tc.expectedFledge)
+		assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero())
 	}
 }
 
@@ -1815,7 +1816,7 @@ func TestErrorReporting(t *testing.T) {
 		addCallSignHeader:   false,
 		bidAdjustments:      bidAdjustments,
 	}
-	bids, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
+	bids, extraBidderRespInfo, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
 	if bids != nil {
 		t.Errorf("There should be no seatbid if no http requests are returned.")
 	}
@@ -1825,6 +1826,7 @@ func TestErrorReporting(t *testing.T) {
 	if errs[0].Error() != "Invalid params on BidRequest." {
 		t.Errorf(`Error message was mutated. Expected "%s", Got "%s"`, "Invalid params on BidRequest.", errs[0].Error())
 	}
+	assert.True(t, extraBidderRespInfo.respProcessingStartTime.IsZero())
 }
 
 func TestSetAssetTypes(t *testing.T) {
@@ -2049,7 +2051,7 @@ func TestCallRecordAdapterConnections(t *testing.T) {
 		addCallSignHeader:   false,
 		bidAdjustments:      bidAdjustments,
 	}
-	_, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{PbsEntryPoint: metrics.ReqTypeORTB2Web}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
+	_, _, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{PbsEntryPoint: metrics.ReqTypeORTB2Web}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
 
 	// Assert no errors
 	assert.Equal(t, 0, len(errs), "bidder.requestBid returned errors %v \n", errs)
@@ -2296,7 +2298,7 @@ func TestRequestBidsWithAdsCertsSigner(t *testing.T) {
 		addCallSignHeader:   true,
 		bidAdjustments:      bidAdjustments,
 	}
-	_, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
+	_, _, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
 
 	assert.Empty(t, errs, "no errors should be returned")
 }
@@ -2510,7 +2512,7 @@ func TestExtraBid(t *testing.T) {
 		bidAdjustments:      bidAdjustments,
 	}
 
-	seatBids, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions,
+	seatBids, extraBidderRespInfo, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions,
 		openrtb_ext.ExtAlternateBidderCodes{
 			Enabled: true,
 			Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{
@@ -2528,6 +2530,7 @@ func TestExtraBid(t *testing.T) {
 		return len(seatBids[i].Seat) < len(seatBids[j].Seat)
 	})
 	assert.Equal(t, wantSeatBids, seatBids)
+	assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero())
 }
 
 func TestExtraBidWithAlternateBidderCodeDisabled(t *testing.T) {
@@ -2624,7 +2627,7 @@ func TestExtraBidWithAlternateBidderCodeDisabled(t *testing.T) {
 		bidAdjustments:      bidAdjustments,
 	}
 
-	seatBids, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions,
+	seatBids, extraBidderRespInfo, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions,
 		openrtb_ext.ExtAlternateBidderCodes{
 			Enabled: true,
 			Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{
@@ -2639,6 +2642,7 @@ func TestExtraBidWithAlternateBidderCodeDisabled(t *testing.T) {
 	assert.Equal(t, wantErrs, errs)
 	assert.Len(t, seatBids, 2)
 	assert.ElementsMatch(t, wantSeatBids, seatBids)
+	assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero())
 }
 
 func TestExtraBidWithBidAdjustments(t *testing.T) {
@@ -2735,7 +2739,7 @@ func TestExtraBidWithBidAdjustments(t *testing.T) {
 		bidAdjustments:      bidAdjustments,
 	}
 
-	seatBids, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions,
+	seatBids, extraBidderRespInfo, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions,
 		openrtb_ext.ExtAlternateBidderCodes{
 			Enabled: true,
 			Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{
@@ -2753,6 +2757,7 @@ func TestExtraBidWithBidAdjustments(t *testing.T) {
 		return len(seatBids[i].Seat) < len(seatBids[j].Seat)
 	})
 	assert.Equal(t, wantSeatBids, seatBids)
+	assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero())
 }
 
 func TestExtraBidWithBidAdjustmentsUsingAdapterCode(t *testing.T) {
@@ -2848,7 +2853,7 @@ func TestExtraBidWithBidAdjustmentsUsingAdapterCode(t *testing.T) {
 		bidAdjustments:      bidAdjustments,
 	}
 
-	seatBids, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions,
+	seatBids, extraBidderRespInfo, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions,
 		openrtb_ext.ExtAlternateBidderCodes{
 			Enabled: true,
 			Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{
@@ -2866,6 +2871,7 @@ func TestExtraBidWithBidAdjustmentsUsingAdapterCode(t *testing.T) {
 		return len(seatBids[i].Seat) < len(seatBids[j].Seat)
 	})
 	assert.Equal(t, wantSeatBids, seatBids)
+	assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero())
 }
 
 func TestExtraBidWithMultiCurrencies(t *testing.T) {
@@ -2973,7 +2979,7 @@ func TestExtraBidWithMultiCurrencies(t *testing.T) {
 		bidAdjustments:      bidAdjustments,
 	}
 
-	seatBids, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions,
+	seatBids, extraBidderRespInfo, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions,
 		openrtb_ext.ExtAlternateBidderCodes{
 			Enabled: true,
 			Bidders: map[string]openrtb_ext.ExtAdapterAlternateBidderCodes{
@@ -2991,6 +2997,7 @@ func TestExtraBidWithMultiCurrencies(t *testing.T) {
 		return len(seatBids[i].Seat) < len(seatBids[j].Seat)
 	})
 	assert.Equal(t, wantSeatBids, seatBids)
+	assert.False(t, extraBidderRespInfo.respProcessingStartTime.IsZero())
 }
 
 func TestGetBidType(t *testing.T) {
diff --git a/exchange/bidder_validate_bids_test.go b/exchange/bidder_validate_bids_test.go
index e7340e7d..ed6173b6 100644
--- a/exchange/bidder_validate_bids_test.go
+++ b/exchange/bidder_validate_bids_test.go
@@ -65,7 +65,7 @@ func TestAllValidBids(t *testing.T) {
 		addCallSignHeader:   false,
 		bidAdjustments:      bidAdjustments,
 	}
-	seatBids, errs := bidder.requestBid(context.Background(), bidderReq, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
+	seatBids, _, errs := bidder.requestBid(context.Background(), bidderReq, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
 	assert.Len(t, seatBids, 1)
 	assert.Len(t, seatBids[0].Bids, 4)
 	assert.Len(t, errs, 0)
@@ -135,7 +135,7 @@ func TestAllBadBids(t *testing.T) {
 		addCallSignHeader:   false,
 		bidAdjustments:      bidAdjustments,
 	}
-	seatBids, errs := bidder.requestBid(context.Background(), bidderReq, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
+	seatBids, _, errs := bidder.requestBid(context.Background(), bidderReq, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
 	assert.Len(t, seatBids, 1)
 	assert.Len(t, seatBids[0].Bids, 0)
 	assert.Len(t, errs, 7)
@@ -216,7 +216,7 @@ func TestMixedBids(t *testing.T) {
 		addCallSignHeader:   false,
 		bidAdjustments:      bidAdjustments,
 	}
-	seatBids, errs := bidder.requestBid(context.Background(), bidderReq, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
+	seatBids, _, errs := bidder.requestBid(context.Background(), bidderReq, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
 	assert.Len(t, seatBids, 1)
 	assert.Len(t, seatBids[0].Bids, 3)
 	assert.Len(t, errs, 5)
@@ -345,7 +345,7 @@ func TestCurrencyBids(t *testing.T) {
 			addCallSignHeader:   false,
 			bidAdjustments:      bidAdjustments,
 		}
-		seatBids, errs := bidder.requestBid(context.Background(), bidderRequest, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
+		seatBids, _, errs := bidder.requestBid(context.Background(), bidderRequest, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
 		assert.Len(t, seatBids, 1)
 		assert.Len(t, seatBids[0].Bids, expectedValidBids)
 		assert.Len(t, errs, expectedErrs)
@@ -354,9 +354,10 @@ func TestCurrencyBids(t *testing.T) {
 
 type mockAdaptedBidder struct {
 	bidResponse   []*entities.PbsOrtbSeatBid
+	extraRespInfo extraBidderRespInfo
 	errorResponse []error
 }
 
-func (b *mockAdaptedBidder) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestMetadata bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, executor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) ([]*entities.PbsOrtbSeatBid, []error) {
-	return b.bidResponse, b.errorResponse
+func (b *mockAdaptedBidder) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestMetadata bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, executor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) ([]*entities.PbsOrtbSeatBid, extraBidderRespInfo, []error) {
+	return b.bidResponse, b.extraRespInfo, b.errorResponse
 }
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index 8306849a..2a35ef57 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -5271,7 +5271,7 @@ type validatingBidder struct {
 	mockResponses map[string]bidderResponse
 }
 
-func (b *validatingBidder) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, executor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) (seatBids []*entities.PbsOrtbSeatBid, errs []error) {
+func (b *validatingBidder) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, executor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) (seatBids []*entities.PbsOrtbSeatBid, extaInfo extraBidderRespInfo, errs []error) {
 	if expectedRequest, ok := b.expectations[string(bidderRequest.BidderName)]; ok {
 		if expectedRequest != nil {
 			if !reflect.DeepEqual(expectedRequest.BidAdjustments, bidRequestOptions.bidAdjustments) {
@@ -5437,7 +5437,7 @@ func (e *emptyUsersync) HasAnyLiveSyncs() bool {
 
 type panicingAdapter struct{}
 
-func (panicingAdapter) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestMetadata bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, executor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) (posb []*entities.PbsOrtbSeatBid, errs []error) {
+func (panicingAdapter) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestMetadata bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, executor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) (posb []*entities.PbsOrtbSeatBid, extraInfo extraBidderRespInfo, errs []error) {
 	panic("Panic! Panic! The world is ending!")
 }
 
diff --git a/usersync/chooser_test.go b/usersync/chooser_test.go
index 3b820b99..0071a2e3 100644
--- a/usersync/chooser_test.go
+++ b/usersync/chooser_test.go
@@ -238,8 +238,8 @@ func TestChooserEvaluate(t *testing.T) {
 		Redirect: NewUniformBidderFilter(BidderFilterModeExclude)}
 
 	cookieNeedsSync := Cookie{}
-	cookieAlreadyHasSyncForA := Cookie{uids: map[string]uidWithExpiry{"keyA": {Expires: time.Now().Add(time.Duration(24) * time.Hour)}}}
-	cookieAlreadyHasSyncForB := Cookie{uids: map[string]uidWithExpiry{"keyB": {Expires: time.Now().Add(time.Duration(24) * time.Hour)}}}
+	cookieAlreadyHasSyncForA := Cookie{uids: map[string]UIDEntry{"keyA": {Expires: time.Now().Add(time.Duration(24) * time.Hour)}}}
+	cookieAlreadyHasSyncForB := Cookie{uids: map[string]UIDEntry{"keyB": {Expires: time.Now().Add(time.Duration(24) * time.Hour)}}}
 
 	testCases := []struct {
 		description        string
diff --git a/usersync/cookie_test.go b/usersync/cookie_test.go
index ee5062ea..5b86df54 100644
--- a/usersync/cookie_test.go
+++ b/usersync/cookie_test.go
@@ -1,177 +1,597 @@
 package usersync
 
 import (
-	"encoding/base64"
+	"errors"
 	"net/http"
 	"net/http/httptest"
-	"strings"
 	"testing"
 	"time"
 
 	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/stretchr/testify/assert"
 )
 
-func TestOptOutCookie(t *testing.T) {
-	cookie := &Cookie{
-		uids:   map[string]uidWithExpiry{"appnexus": {UID: "test"}},
-		optOut: true,
+func TestReadCookie(t *testing.T) {
+	testCases := []struct {
+		name            string
+		givenRequest    *http.Request
+		givenHttpCookie *http.Cookie
+		givenCookie     *Cookie
+		givenDecoder    Decoder
+		expectedCookie  *Cookie
+	}{
+		{
+			name:         "simple-cookie",
+			givenRequest: httptest.NewRequest("POST", "http://www.prebid.com", nil),
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID:     "UID",
+						Expires: time.Time{},
+					},
+				},
+				optOut: false,
+			},
+			expectedCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID: "UID",
+					},
+				},
+				optOut: false,
+			},
+		},
+		{
+			name:         "empty-cookie",
+			givenRequest: httptest.NewRequest("POST", "http://www.prebid.com", nil),
+			givenCookie:  &Cookie{},
+			expectedCookie: &Cookie{
+				uids:   map[string]UIDEntry{},
+				optOut: false,
+			},
+		},
+		{
+			name:         "nil-cookie",
+			givenRequest: httptest.NewRequest("POST", "http://www.prebid.com", nil),
+			givenCookie:  nil,
+			expectedCookie: &Cookie{
+				uids:   map[string]UIDEntry{},
+				optOut: false,
+			},
+		},
+		{
+			name:         "corruptted-http-cookie",
+			givenRequest: httptest.NewRequest("POST", "http://www.prebid.com", nil),
+			givenHttpCookie: &http.Cookie{
+				Name:  "uids",
+				Value: "bad base64 encoding",
+			},
+			givenCookie: nil,
+			expectedCookie: &Cookie{
+				uids:   map[string]UIDEntry{},
+				optOut: false,
+			},
+		},
 	}
-	ensureConsistency(t, cookie)
-}
 
-func TestEmptyOptOutCookie(t *testing.T) {
-	cookie := &Cookie{
-		uids:   make(map[string]uidWithExpiry),
-		optOut: true,
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			if test.givenCookie != nil {
+				httpCookie, err := ToHTTPCookie(test.givenCookie)
+				assert.NoError(t, err)
+				test.givenRequest.AddCookie(httpCookie)
+			} else if test.givenCookie == nil && test.givenHttpCookie != nil {
+				test.givenRequest.AddCookie(test.givenHttpCookie)
+			}
+			actualCookie := ReadCookie(test.givenRequest, Base64Decoder{}, &config.HostCookie{})
+			assert.Equal(t, test.expectedCookie.uids, actualCookie.uids)
+			assert.Equal(t, test.expectedCookie.optOut, actualCookie.optOut)
+		})
 	}
-	ensureConsistency(t, cookie)
 }
 
-func TestEmptyCookie(t *testing.T) {
-	cookie := &Cookie{
-		uids:   make(map[string]uidWithExpiry),
-		optOut: false,
+func TestWriteCookie(t *testing.T) {
+	encoder := Base64Encoder{}
+	decoder := Base64Decoder{}
+
+	testCases := []struct {
+		name               string
+		givenCookie        *Cookie
+		givenSetSiteCookie bool
+		expectedCookie     *Cookie
+	}{
+		{
+			name: "simple-cookie",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID:     "UID",
+						Expires: time.Time{},
+					},
+				},
+				optOut: false,
+			},
+			givenSetSiteCookie: false,
+			expectedCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID:     "UID",
+						Expires: time.Time{},
+					},
+				},
+				optOut: false,
+			},
+		},
+		{
+			name: "simple-cookie-opt-out",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID:     "UID",
+						Expires: time.Time{},
+					},
+				},
+				optOut: true,
+			},
+			givenSetSiteCookie: true,
+			expectedCookie: &Cookie{
+				uids:   map[string]UIDEntry{},
+				optOut: true,
+			},
+		},
+		{
+			name: "cookie-multiple-uids",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID:     "UID",
+						Expires: time.Time{},
+					},
+					"rubicon": {
+						UID:     "UID2",
+						Expires: time.Time{},
+					},
+				},
+				optOut: false,
+			},
+			givenSetSiteCookie: true,
+			expectedCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID:     "UID",
+						Expires: time.Time{},
+					},
+					"rubicon": {
+						UID:     "UID2",
+						Expires: time.Time{},
+					},
+				},
+				optOut: false,
+			},
+		},
 	}
-	ensureConsistency(t, cookie)
-}
 
-func TestCookieWithData(t *testing.T) {
-	cookie := newSampleCookie()
-	ensureConsistency(t, cookie)
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			// Write Cookie
+			w := httptest.NewRecorder()
+			encodedCookie, err := encoder.Encode(test.givenCookie)
+			assert.NoError(t, err)
+			WriteCookie(w, encodedCookie, &config.HostCookie{}, test.givenSetSiteCookie)
+			writtenCookie := w.Header().Get("Set-Cookie")
+
+			// Read Cookie
+			header := http.Header{}
+			header.Add("Cookie", writtenCookie)
+			r := &http.Request{Header: header}
+			actualCookie := ReadCookie(r, decoder, &config.HostCookie{})
+
+			assert.Equal(t, test.expectedCookie, actualCookie)
+		})
+	}
 }
 
-func TestBidderNameGets(t *testing.T) {
-	cookie := newSampleCookie()
-	id, exists, _ := cookie.GetUID("adnxs")
-	if !exists {
-		t.Errorf("Cookie missing expected Appnexus ID")
-	}
-	if id != "123" {
-		t.Errorf("Bad appnexus id. Expected %s, got %s", "123", id)
+func TestSync(t *testing.T) {
+	testCases := []struct {
+		name           string
+		givenCookie    *Cookie
+		givenSyncerKey string
+		givenUID       string
+		expectedCookie *Cookie
+		expectedError  error
+	}{
+		{
+			name: "simple-sync",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{},
+			},
+			givenSyncerKey: "adnxs",
+			givenUID:       "123",
+			expectedCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID: "123",
+					},
+				},
+			},
+		},
+		{
+			name: "dont-allow-syncs",
+			givenCookie: &Cookie{
+				uids:   map[string]UIDEntry{},
+				optOut: true,
+			},
+			givenSyncerKey: "adnxs",
+			givenUID:       "123",
+			expectedCookie: &Cookie{
+				uids: map[string]UIDEntry{},
+			},
+			expectedError: errors.New("the user has opted out of prebid server cookie syncs"),
+		},
+		{
+			name: "audienceNetwork",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{},
+			},
+			givenSyncerKey: string(openrtb_ext.BidderAudienceNetwork),
+			givenUID:       "0",
+			expectedError:  errors.New("audienceNetwork uses a UID of 0 as \"not yet recognized\""),
+		},
 	}
 
-	id, exists, _ = cookie.GetUID("rubicon")
-	if !exists {
-		t.Errorf("Cookie missing expected Rubicon ID")
-	}
-	if id != "456" {
-		t.Errorf("Bad rubicon id. Expected %s, got %s", "456", id)
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			err := test.givenCookie.Sync(test.givenSyncerKey, test.givenUID)
+			if test.expectedError != nil {
+				assert.Equal(t, test.expectedError, err)
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, test.expectedCookie.uids[test.givenSyncerKey].UID, test.givenCookie.uids[test.givenSyncerKey].UID)
+			}
+		})
 	}
 }
 
-func TestRejectAudienceNetworkCookie(t *testing.T) {
-	raw := &Cookie{
-		uids: map[string]uidWithExpiry{
-			"audienceNetwork": newTempId("0", 10),
+func TestGetUIDs(t *testing.T) {
+	testCases := []struct {
+		name           string
+		givenCookie    *Cookie
+		expectedCookie *Cookie
+		expectedLen    int
+	}{
+		{
+			name: "two-uids",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID: "123",
+					},
+					"rubicon": {
+						UID: "456",
+					},
+				},
+			},
+			expectedLen: 2,
+		},
+		{
+			name: "one-uid",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID: "123",
+					},
+				},
+			},
+			expectedLen: 1,
+		},
+		{
+			name:        "empty",
+			givenCookie: &Cookie{},
+			expectedLen: 0,
+		},
+		{
+			name:        "nil",
+			givenCookie: nil,
+			expectedLen: 0,
 		},
-		optOut: false,
-	}
-	parsed := ParseCookie(raw.ToHTTPCookie(90 * 24 * time.Hour))
-	if parsed.HasLiveSync("audienceNetwork") {
-		t.Errorf("Cookie serializing and deserializing should delete audienceNetwork values of 0")
 	}
 
-	err := parsed.TrySync("audienceNetwork", "0")
-	if err == nil {
-		t.Errorf("Cookie should reject audienceNetwork values of 0.")
-	}
-	if parsed.HasLiveSync("audienceNetwork") {
-		t.Errorf("Cookie The cookie should have rejected the audienceNetwork sync.")
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			uids := test.givenCookie.GetUIDs()
+			assert.Len(t, uids, test.expectedLen)
+			for key, value := range uids {
+				assert.Equal(t, test.givenCookie.uids[key].UID, value)
+			}
+
+		})
 	}
 }
 
-func TestOptOutReset(t *testing.T) {
-	cookie := newSampleCookie()
+func TestWriteCookieUserAgent(t *testing.T) {
+	encoder := Base64Encoder{}
 
-	cookie.SetOptOut(true)
-	if cookie.AllowSyncs() {
-		t.Error("After SetOptOut(true), a cookie should not allow more user syncs.")
+	testCases := []struct {
+		name                string
+		givenUserAgent      string
+		givenCookie         *Cookie
+		givenHostCookie     config.HostCookie
+		givenSetSiteCookie  bool
+		expectedContains    string
+		expectedNotContains string
+	}{
+		{
+			name:           "same-site-none",
+			givenUserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID:     "UID",
+						Expires: time.Time{},
+					},
+				},
+				optOut: false,
+			},
+			givenHostCookie:    config.HostCookie{},
+			givenSetSiteCookie: true,
+			expectedContains:   "; Secure;",
+		},
+		{
+			name:           "older-chrome-version",
+			givenUserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3770.142 Safari/537.36",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID:     "UID",
+						Expires: time.Time{},
+					},
+				},
+				optOut: false,
+			},
+			givenHostCookie:     config.HostCookie{},
+			givenSetSiteCookie:  true,
+			expectedNotContains: "SameSite=none",
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			// Set Up
+			req := httptest.NewRequest("GET", "http://www.prebid.com", nil)
+			req.Header.Set("User-Agent", test.givenUserAgent)
+
+			// Write Cookie
+			w := httptest.NewRecorder()
+			encodedCookie, err := encoder.Encode(test.givenCookie)
+			assert.NoError(t, err)
+			WriteCookie(w, encodedCookie, &test.givenHostCookie, test.givenSetSiteCookie)
+			writtenCookie := w.Header().Get("Set-Cookie")
+
+			if test.expectedContains == "" {
+				assert.NotContains(t, writtenCookie, test.expectedNotContains)
+			} else {
+				assert.Contains(t, writtenCookie, test.expectedContains)
+			}
+		})
 	}
-	ensureConsistency(t, cookie)
 }
 
-func TestOptIn(t *testing.T) {
-	cookie := &Cookie{
-		uids:   make(map[string]uidWithExpiry),
-		optOut: true,
+func TestPrepareCookieForWrite(t *testing.T) {
+	encoder := Base64Encoder{}
+	decoder := Base64Decoder{}
+	cookieToSend := &Cookie{
+		uids: map[string]UIDEntry{
+			"1": newTempId("1234567890123456789012345678901234567890123456", 7),
+			"7": newTempId("abcdefghijklmnopqrstuvwxy", 1),
+			"2": newTempId("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 6),
+			"3": newTempId("123456789012345678901234567896123456789012345678", 5),
+			"4": newTempId("aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ", 4),
+			"5": newTempId("12345678901234567890123456789012345678901234567890", 3),
+			"6": newTempId("abcdefghij", 2),
+		},
+		optOut: false,
 	}
 
-	cookie.SetOptOut(false)
-	if !cookie.AllowSyncs() {
-		t.Error("After SetOptOut(false), a cookie should allow more user syncs.")
+	testCases := []struct {
+		name                     string
+		givenMaxCookieSize       int
+		expectedRemainingUidKeys []string
+	}{
+		{
+			name:               "no-uids-ejected",
+			givenMaxCookieSize: 2000,
+			expectedRemainingUidKeys: []string{
+				"1", "2", "3", "4", "5", "6", "7",
+			},
+		},
+		{
+			name:               "no-uids-ejected-2",
+			givenMaxCookieSize: 0,
+			expectedRemainingUidKeys: []string{
+				"1", "2", "3", "4", "5", "6", "7",
+			},
+		},
+		{
+			name:               "one-uid-ejected",
+			givenMaxCookieSize: 900,
+			expectedRemainingUidKeys: []string{
+				"1", "2", "3", "4", "5", "6",
+			},
+		},
+		{
+			name:               "four-uids-ejected",
+			givenMaxCookieSize: 500,
+			expectedRemainingUidKeys: []string{
+				"1", "2", "3",
+			},
+		},
+		{
+			name:               "all-but-one-uids-ejected",
+			givenMaxCookieSize: 300,
+			expectedRemainingUidKeys: []string{
+				"1",
+			},
+		},
+		{
+			name:                     "all-uids-ejected",
+			givenMaxCookieSize:       100,
+			expectedRemainingUidKeys: []string{},
+		},
+		{
+			name:                     "invalid-max-size",
+			givenMaxCookieSize:       -100,
+			expectedRemainingUidKeys: []string{},
+		},
 	}
-	ensureConsistency(t, cookie)
-}
 
-func TestParseCorruptedCookie(t *testing.T) {
-	raw := http.Cookie{
-		Name:  "uids",
-		Value: "bad base64 encoding",
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			encodedCookie, err := cookieToSend.PrepareCookieForWrite(&config.HostCookie{MaxCookieSizeBytes: test.givenMaxCookieSize}, encoder)
+			assert.NoError(t, err)
+			decodedCookie := decoder.Decode(encodedCookie)
+
+			for _, key := range test.expectedRemainingUidKeys {
+				_, ok := decodedCookie.uids[key]
+				assert.Equal(t, true, ok)
+			}
+			assert.Equal(t, len(decodedCookie.uids), len(test.expectedRemainingUidKeys))
+		})
 	}
-	parsed := ParseCookie(&raw)
-	ensureEmptyMap(t, parsed)
 }
 
-func TestParseCorruptedCookieJSON(t *testing.T) {
-	cookieData := base64.URLEncoding.EncodeToString([]byte("bad json"))
-	raw := http.Cookie{
-		Name:  "uids",
-		Value: cookieData,
+func TestSyncHostCookie(t *testing.T) {
+	testCases := []struct {
+		name            string
+		givenCookie     *Cookie
+		givenUID        string
+		givenHostCookie *config.HostCookie
+		expectedCookie  *Cookie
+		expectedError   error
+	}{
+		{
+			name: "simple-sync",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{},
+			},
+			givenHostCookie: &config.HostCookie{
+				Family:     "syncer",
+				CookieName: "adnxs",
+			},
+			expectedCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"syncer": {
+						UID: "some-user-id",
+					},
+				},
+			},
+		},
+		{
+			name: "uids-already-present",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"some-syncer": {
+						UID: "some-other-user-id",
+					},
+				},
+			},
+			givenHostCookie: &config.HostCookie{
+				Family:     "syncer",
+				CookieName: "adnxs",
+			},
+			expectedCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"syncer": {
+						UID: "some-user-id",
+					},
+					"some-syncer": {
+						UID: "some-other-user-id",
+					},
+				},
+			},
+		},
+		{
+			name: "host-already-synced",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"syncer": {
+						UID: "some-user-id",
+					},
+				},
+			},
+			givenHostCookie: &config.HostCookie{
+				Family:     "syncer",
+				CookieName: "adnxs",
+			},
+			expectedCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"syncer": {
+						UID: "some-user-id",
+					},
+				},
+			},
+		},
 	}
-	parsed := ParseCookie(&raw)
-	ensureEmptyMap(t, parsed)
-}
 
-func TestParseNilSyncMap(t *testing.T) {
-	raw := http.Cookie{
-		Name:  uidCookieName,
-		Value: "",
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			r := httptest.NewRequest("POST", "http://www.prebid.com", nil)
+			r.AddCookie(&http.Cookie{
+				Name:  test.givenHostCookie.CookieName,
+				Value: "some-user-id",
+			})
+
+			SyncHostCookie(r, test.givenCookie, test.givenHostCookie)
+			for key, value := range test.expectedCookie.uids {
+				assert.Equal(t, value.UID, test.givenCookie.uids[key].UID)
+			}
+		})
 	}
-	parsed := ParseCookie(&raw)
-	ensureEmptyMap(t, parsed)
-	ensureConsistency(t, parsed)
 }
 
-func TestParseOtherCookie(t *testing.T) {
-	req := httptest.NewRequest("POST", "http://www.prebid.com", nil)
-	otherCookieName := "other"
-	id := "some-user-id"
-	req.AddCookie(&http.Cookie{
-		Name:  otherCookieName,
-		Value: id,
-	})
-	parsed := ParseCookieFromRequest(req, &config.HostCookie{
-		Family:     "adnxs",
-		CookieName: otherCookieName,
-	})
-	val, _, _ := parsed.GetUID("adnxs")
-	if val != id {
-		t.Errorf("Bad cookie value. Expected %s, got %s", id, val)
+func TestBidderNameGets(t *testing.T) {
+	cookie := newSampleCookie()
+	id, exists, _ := cookie.GetUID("adnxs")
+	if !exists {
+		t.Errorf("Cookie missing expected Appnexus ID")
+	}
+	if id != "123" {
+		t.Errorf("Bad appnexus id. Expected %s, got %s", "123", id)
+	}
+
+	id, exists, _ = cookie.GetUID("rubicon")
+	if !exists {
+		t.Errorf("Cookie missing expected Rubicon ID")
+	}
+	if id != "456" {
+		t.Errorf("Bad rubicon id. Expected %s, got %s", "456", id)
 	}
 }
 
-func TestParseCookieFromRequestOptOut(t *testing.T) {
+func TestReadCookieOptOut(t *testing.T) {
 	optOutCookieName := "optOutCookieName"
 	optOutCookieValue := "optOutCookieValue"
+	decoder := Base64Decoder{}
 
-	existingCookie := *(&Cookie{
-		uids: map[string]uidWithExpiry{
+	cookie := *(&Cookie{
+		uids: map[string]UIDEntry{
 			"foo": newTempId("fooID", 1),
 			"bar": newTempId("barID", 2),
 		},
 		optOut: false,
-	}).ToHTTPCookie(24 * time.Hour)
+	})
+
+	existingCookie, _ := ToHTTPCookie(&cookie)
 
 	testCases := []struct {
 		description          string
-		givenExistingCookies []http.Cookie
+		givenExistingCookies []*http.Cookie
 		expectedEmpty        bool
 		expectedSetOptOut    bool
 	}{
 		{
 			description: "Opt Out Cookie",
-			givenExistingCookies: []http.Cookie{
+			givenExistingCookies: []*http.Cookie{
 				existingCookie,
 				{Name: optOutCookieName, Value: optOutCookieValue}},
 			expectedEmpty:     true,
@@ -179,14 +599,14 @@ func TestParseCookieFromRequestOptOut(t *testing.T) {
 		},
 		{
 			description: "No Opt Out Cookie",
-			givenExistingCookies: []http.Cookie{
+			givenExistingCookies: []*http.Cookie{
 				existingCookie},
 			expectedEmpty:     false,
 			expectedSetOptOut: false,
 		},
 		{
 			description: "Opt Out Cookie - Wrong Value",
-			givenExistingCookies: []http.Cookie{
+			givenExistingCookies: []*http.Cookie{
 				existingCookie,
 				{Name: optOutCookieName, Value: "wrong"}},
 			expectedEmpty:     false,
@@ -194,7 +614,7 @@ func TestParseCookieFromRequestOptOut(t *testing.T) {
 		},
 		{
 			description: "Opt Out Cookie - Wrong Name",
-			givenExistingCookies: []http.Cookie{
+			givenExistingCookies: []*http.Cookie{
 				existingCookie,
 				{Name: "wrong", Value: optOutCookieValue}},
 			expectedEmpty:     false,
@@ -202,7 +622,7 @@ func TestParseCookieFromRequestOptOut(t *testing.T) {
 		},
 		{
 			description: "Opt Out Cookie - No Host Cookies",
-			givenExistingCookies: []http.Cookie{
+			givenExistingCookies: []*http.Cookie{
 				{Name: optOutCookieName, Value: optOutCookieValue}},
 			expectedEmpty:     true,
 			expectedSetOptOut: true,
@@ -213,10 +633,10 @@ func TestParseCookieFromRequestOptOut(t *testing.T) {
 		req := httptest.NewRequest("POST", "http://www.prebid.com", nil)
 
 		for _, c := range test.givenExistingCookies {
-			req.AddCookie(&c)
+			req.AddCookie(c)
 		}
 
-		parsed := ParseCookieFromRequest(req, &config.HostCookie{
+		parsed := ReadCookie(req, decoder, &config.HostCookie{
 			Family: "foo",
 			OptOutCookie: config.Cookie{
 				Name:  optOutCookieName,
@@ -233,125 +653,59 @@ func TestParseCookieFromRequestOptOut(t *testing.T) {
 	}
 }
 
-func TestCookieReadWrite(t *testing.T) {
-	cookie := newSampleCookie()
-
-	received := writeThenRead(cookie, 0)
-	uid, exists, isLive := received.GetUID("adnxs")
-	if !exists || !isLive || uid != "123" {
-		t.Errorf("Received cookie should have the adnxs ID=123. Got %s", uid)
-	}
-
-	uid, exists, isLive = received.GetUID("rubicon")
-	if !exists || !isLive || uid != "456" {
-		t.Errorf("Received cookie should have the rubicon ID=456. Got %s", uid)
-	}
-
-	assert.True(t, received.HasAnyLiveSyncs(), "Has Live Syncs")
-	assert.Len(t, received.uids, 2, "Sync Count")
-}
-
-func TestNilCookie(t *testing.T) {
-	var nilCookie *Cookie
-
-	if nilCookie.HasLiveSync("anything") {
-		t.Error("nil cookies should respond with false when asked if they have a sync")
-	}
-
-	if nilCookie.HasAnyLiveSyncs() {
-		t.Error("nil cookies shouldn't have any syncs.")
-	}
-
-	if nilCookie.AllowSyncs() {
-		t.Error("nil cookies shouldn't allow syncs to take place.")
+func TestOptIn(t *testing.T) {
+	cookie := &Cookie{
+		uids:   make(map[string]UIDEntry),
+		optOut: true,
 	}
 
-	uid, hadUID, isLive := nilCookie.GetUID("anything")
-
-	if uid != "" {
-		t.Error("nil cookies should return empty strings for the UID.")
-	}
-	if hadUID {
-		t.Error("nil cookies shouldn't claim to have a UID mapping.")
-	}
-	if isLive {
-		t.Error("nil cookies shouldn't report live UID mappings.")
+	cookie.SetOptOut(false)
+	if !cookie.AllowSyncs() {
+		t.Error("After SetOptOut(false), a cookie should allow more user syncs.")
 	}
+	ensureConsistency(t, cookie)
 }
 
-func TestGetUIDs(t *testing.T) {
+func TestOptOutReset(t *testing.T) {
 	cookie := newSampleCookie()
-	uids := cookie.GetUIDs()
 
-	assert.Len(t, uids, 2, "GetUIDs should return user IDs for all bidders")
-	assert.Equal(t, "123", uids["adnxs"], "GetUIDs should return the correct user ID for each bidder")
-	assert.Equal(t, "456", uids["rubicon"], "GetUIDs should return the correct user ID for each bidder")
+	cookie.SetOptOut(true)
+	if cookie.AllowSyncs() {
+		t.Error("After SetOptOut(true), a cookie should not allow more user syncs.")
+	}
+	ensureConsistency(t, cookie)
 }
 
-func TestGetUIDsWithEmptyCookie(t *testing.T) {
-	cookie := &Cookie{}
-	uids := cookie.GetUIDs()
-
-	assert.Len(t, uids, 0, "GetUIDs shouldn't return any user syncs for an empty cookie")
+func TestOptOutCookie(t *testing.T) {
+	cookie := &Cookie{
+		uids:   make(map[string]UIDEntry),
+		optOut: true,
+	}
+	ensureConsistency(t, cookie)
 }
 
-func TestGetUIDsWithNilCookie(t *testing.T) {
-	var cookie *Cookie
-	uids := cookie.GetUIDs()
-
-	assert.Len(t, uids, 0, "GetUIDs shouldn't return any user syncs for a nil cookie")
+func newTempId(uid string, offset int) UIDEntry {
+	return UIDEntry{
+		UID:     uid,
+		Expires: time.Now().Add(time.Duration(offset) * time.Minute).UTC(),
+	}
 }
 
-func TestTrimCookiesClosestExpirationDates(t *testing.T) {
-	cookieToSend := &Cookie{
-		uids: map[string]uidWithExpiry{
-			"k1": newTempId("12345678901234567890123456789012345678901234567890", 7),
-			"k2": newTempId("abcdefghijklmnopqrstuvwxyz", 1),
-			"k3": newTempId("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 6),
-			"k4": newTempId("12345678901234567890123456789612345678901234567890", 5),
-			"k5": newTempId("aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ", 4),
-			"k6": newTempId("12345678901234567890123456789012345678901234567890", 3),
-			"k7": newTempId("abcdefghijklmnopqrstuvwxyz", 2),
+func newSampleCookie() *Cookie {
+	return &Cookie{
+		uids: map[string]UIDEntry{
+			"adnxs":   newTempId("123", 10),
+			"rubicon": newTempId("456", 10),
 		},
 		optOut: false,
 	}
-
-	type aTest struct {
-		maxCookieSize int
-		expKeys       []string
-	}
-	testCases := []aTest{
-		{maxCookieSize: 2000, expKeys: []string{"k1", "k2", "k3", "k4", "k5", "k6", "k7"}}, //1 don't trim, set
-		{maxCookieSize: 0, expKeys: []string{"k1", "k2", "k3", "k4", "k5", "k6", "k7"}},    //2 unlimited size: don't trim, set
-		{maxCookieSize: 800, expKeys: []string{"k1", "k5", "k4", "k3", "k6"}},              //3 trim to size and set
-		{maxCookieSize: 500, expKeys: []string{"k1", "k4", "k3"}},                          //4 trim to size and set
-		{maxCookieSize: 200, expKeys: []string{}},                                          //5 insufficient size, trim to zero length and set
-		{maxCookieSize: -100, expKeys: []string{}},                                         //6 invalid size, trim to zero length and set
-	}
-	for i := range testCases {
-		processedCookie := writeThenRead(cookieToSend, testCases[i].maxCookieSize)
-
-		actualKeys := make([]string, 0, 7)
-		for key := range processedCookie.uids {
-			actualKeys = append(actualKeys, key)
-		}
-
-		assert.ElementsMatch(t, testCases[i].expKeys, actualKeys, "[Test %d]", i+1)
-	}
-}
-
-func ensureEmptyMap(t *testing.T, cookie *Cookie) {
-	if !cookie.AllowSyncs() {
-		t.Error("Empty cookies should allow user syncs.")
-	}
-	if cookie.HasAnyLiveSyncs() {
-		t.Error("Empty cookies shouldn't have any user syncs. Found at least 1.")
-	}
 }
 
 func ensureConsistency(t *testing.T, cookie *Cookie) {
+	decoder := Base64Decoder{}
+
 	if cookie.AllowSyncs() {
-		err := cookie.TrySync("pulsepoint", "1")
+		err := cookie.Sync("pulsepoint", "1")
 		if err != nil {
 			t.Errorf("Cookie sync should succeed if the user has opted in.")
 		}
@@ -380,13 +734,14 @@ func ensureConsistency(t *testing.T, cookie *Cookie) {
 			t.Error("If the user opted out, the PBSCookie should have no user syncs.")
 		}
 
-		err := cookie.TrySync("adnxs", "123")
+		err := cookie.Sync("adnxs", "123")
 		if err == nil {
 			t.Error("TrySync should fail if the user has opted out of PBSCookie syncs, but it succeeded.")
 		}
 	}
-
-	copiedCookie := ParseCookie(cookie.ToHTTPCookie(90 * 24 * time.Hour))
+	httpCookie, err := ToHTTPCookie(cookie)
+	assert.NoError(t, err)
+	copiedCookie := decoder.Decode(httpCookie.Value)
 	if copiedCookie.AllowSyncs() != cookie.AllowSyncs() {
 		t.Error("The PBSCookie interface shouldn't let modifications happen if the user has opted out")
 	}
@@ -414,61 +769,17 @@ func ensureConsistency(t *testing.T, cookie *Cookie) {
 	}
 }
 
-func newTempId(uid string, offset int) uidWithExpiry {
-	return uidWithExpiry{
-		UID:     uid,
-		Expires: time.Now().Add(time.Duration(offset) * time.Minute).UTC(),
+func ToHTTPCookie(cookie *Cookie) (*http.Cookie, error) {
+	encoder := Base64Encoder{}
+	encodedCookie, err := encoder.Encode(cookie)
+	if err != nil {
+		return nil, nil
 	}
-}
 
-func newSampleCookie() *Cookie {
-	return &Cookie{
-		uids: map[string]uidWithExpiry{
-			"adnxs":   newTempId("123", 10),
-			"rubicon": newTempId("456", 10),
-		},
-		optOut: false,
-	}
-}
-
-func writeThenRead(cookie *Cookie, maxCookieSize int) *Cookie {
-	w := httptest.NewRecorder()
-	hostCookie := &config.HostCookie{Domain: "mock-domain", MaxCookieSizeBytes: maxCookieSize}
-	cookie.SetCookieOnResponse(w, false, hostCookie, 90*24*time.Hour)
-	writtenCookie := w.HeaderMap.Get("Set-Cookie")
-
-	header := http.Header{}
-	header.Add("Cookie", writtenCookie)
-	request := http.Request{Header: header}
-	return ParseCookieFromRequest(&request, hostCookie)
-}
-
-func TestSetCookieOnResponseForSameSiteNone(t *testing.T) {
-	cookie := newSampleCookie()
-	w := httptest.NewRecorder()
-	req := httptest.NewRequest("GET", "http://www.prebid.com", nil)
-	ua := "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36"
-	req.Header.Set("User-Agent", ua)
-	hostCookie := &config.HostCookie{Domain: "mock-domain", MaxCookieSizeBytes: 0}
-	cookie.SetCookieOnResponse(w, true, hostCookie, 90*24*time.Hour)
-	writtenCookie := w.HeaderMap.Get("Set-Cookie")
-	t.Log("Set-Cookie is: ", writtenCookie)
-	if !strings.Contains(writtenCookie, "; Secure;") {
-		t.Error("Set-Cookie should contain Secure")
-	}
-}
-
-func TestSetCookieOnResponseForOlderChromeVersion(t *testing.T) {
-	cookie := newSampleCookie()
-	w := httptest.NewRecorder()
-	req := httptest.NewRequest("GET", "http://www.prebid.com", nil)
-	ua := "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3770.142 Safari/537.36"
-	req.Header.Set("User-Agent", ua)
-	hostCookie := &config.HostCookie{Domain: "mock-domain", MaxCookieSizeBytes: 0}
-	cookie.SetCookieOnResponse(w, false, hostCookie, 90*24*time.Hour)
-	writtenCookie := w.HeaderMap.Get("Set-Cookie")
-	t.Log("Set-Cookie is: ", writtenCookie)
-	if strings.Contains(writtenCookie, "SameSite=none") {
-		t.Error("Set-Cookie should not contain SameSite=none")
-	}
+	return &http.Cookie{
+		Name:    uidCookieName,
+		Value:   encodedCookie,
+		Expires: time.Now().Add((90 * 24 * time.Hour)),
+		Path:    "/",
+	}, nil
 }
diff --git a/usersync/encoder_decoder_test.go b/usersync/encoder_decoder_test.go
new file mode 100644
index 00000000..5a87d4e7
--- /dev/null
+++ b/usersync/encoder_decoder_test.go
@@ -0,0 +1,149 @@
+package usersync
+
+import (
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestEncoderDecoder(t *testing.T) {
+	encoder := Base64Encoder{}
+	decoder := Base64Decoder{}
+
+	testCases := []struct {
+		name            string
+		givenRequest    *http.Request
+		givenHttpCookie *http.Cookie
+		givenCookie     *Cookie
+		givenDecoder    Decoder
+		expectedCookie  *Cookie
+	}{
+		{
+			name: "simple-cookie",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID:     "UID",
+						Expires: time.Time{},
+					},
+				},
+				optOut: false,
+			},
+			expectedCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID: "UID",
+					},
+				},
+				optOut: false,
+			},
+		},
+		{
+			name:        "empty-cookie",
+			givenCookie: &Cookie{},
+			expectedCookie: &Cookie{
+				uids:   map[string]UIDEntry{},
+				optOut: false,
+			},
+		},
+		{
+			name:        "nil-cookie",
+			givenCookie: nil,
+			expectedCookie: &Cookie{
+				uids:   map[string]UIDEntry{},
+				optOut: false,
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			encodedCookie, err := encoder.Encode(test.givenCookie)
+			assert.NoError(t, err)
+			decodedCookie := decoder.Decode(encodedCookie)
+
+			assert.Equal(t, test.expectedCookie.uids, decodedCookie.uids)
+			assert.Equal(t, test.expectedCookie.optOut, decodedCookie.optOut)
+		})
+	}
+}
+
+func TestEncoder(t *testing.T) {
+	encoder := Base64Encoder{}
+
+	testCases := []struct {
+		name                  string
+		givenCookie           *Cookie
+		expectedEncodedCookie string
+	}{
+		{
+			name: "simple-cookie",
+			givenCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID:     "UID",
+						Expires: time.Time{},
+					},
+				},
+				optOut: false,
+			},
+			expectedEncodedCookie: "eyJ0ZW1wVUlEcyI6eyJhZG54cyI6eyJ1aWQiOiJVSUQiLCJleHBpcmVzIjoiMDAwMS0wMS0wMVQwMDowMDowMFoifX19",
+		},
+		{
+			name:                  "empty-cookie",
+			givenCookie:           &Cookie{},
+			expectedEncodedCookie: "e30=",
+		},
+		{
+			name:                  "nil-cookie",
+			givenCookie:           nil,
+			expectedEncodedCookie: "bnVsbA==",
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			encodedCookie, err := encoder.Encode(test.givenCookie)
+			assert.NoError(t, err)
+
+			assert.Equal(t, test.expectedEncodedCookie, encodedCookie)
+		})
+	}
+}
+
+func TestDecoder(t *testing.T) {
+	decoder := Base64Decoder{}
+
+	testCases := []struct {
+		name               string
+		givenEncodedCookie string
+		expectedCookie     *Cookie
+	}{
+		{
+			name:               "simple-encoded-cookie",
+			givenEncodedCookie: "eyJ0ZW1wVUlEcyI6eyJhZG54cyI6eyJ1aWQiOiJVSUQiLCJleHBpcmVzIjoiMDAwMS0wMS0wMVQwMDowMDowMFoifX19",
+			expectedCookie: &Cookie{
+				uids: map[string]UIDEntry{
+					"adnxs": {
+						UID: "UID",
+					},
+				},
+				optOut: false,
+			},
+		},
+		{
+			name:               "nil-encoded-cookie",
+			givenEncodedCookie: "",
+			expectedCookie:     NewCookie(),
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			decodedCookie := decoder.Decode(test.givenEncodedCookie)
+			assert.Equal(t, test.expectedCookie, decodedCookie)
+		})
+	}
+}