Repository: prebid/prebid-server
Pull request #1374 (2020-06-30 03:32:00)

Add support for multiple root schain nodes

bsardo: Related issue: #1091 

Specification: https://github.com/InteractiveAdvertisingBureau/openrtb/blob/master/supplychainobject.md

Patch:
diff --git a/adapters/adform/adform.go b/adapters/adform/adform.go
index 3aeea62e..69f1c12f 100644
--- a/adapters/adform/adform.go
+++ b/adapters/adform/adform.go
@@ -42,6 +42,7 @@ type adformRequest struct {
 	consent       string
 	digitrust     *adformDigitrust
 	currency      string
+	eids          string
 }
 
 type adformDigitrust struct {
@@ -279,6 +280,9 @@ func (r *adformRequest) buildAdformUrl(a *AdformAdapter) string {
 
 	parameters.Add("gdpr", r.gdprApplies)
 	parameters.Add("gdpr_consent", r.consent)
+	if r.eids != "" {
+		parameters.Add("eids", r.eids)
+	}
 
 	URL := *a.URL
 	URL.RawQuery = parameters.Encode()
@@ -465,6 +469,7 @@ func openRtbToAdformRequest(request *openrtb.BidRequest) (*adformRequest, []erro
 		}
 	}
 
+	eids := ""
 	consent := ""
 	var digitrustData *openrtb_ext.ExtUserDigiTrust
 	if request.User != nil {
@@ -472,6 +477,7 @@ func openRtbToAdformRequest(request *openrtb.BidRequest) (*adformRequest, []erro
 		if err := json.Unmarshal(request.User.Ext, &extUser); err == nil {
 			consent = extUser.Consent
 			digitrustData = extUser.DigiTrust
+			eids = encodeEids(extUser.Eids)
 		}
 	}
 
@@ -513,9 +519,34 @@ func openRtbToAdformRequest(request *openrtb.BidRequest) (*adformRequest, []erro
 		consent:       consent,
 		digitrust:     digitrust,
 		currency:      requestCurrency,
+		eids:          eids,
 	}, errors
 }
 
+func encodeEids(eids []openrtb_ext.ExtUserEid) string {
+	if eids == nil {
+		return ""
+	}
+
+	eidsMap := make(map[string]map[string][]int)
+	for _, eid := range eids {
+		_, ok := eidsMap[eid.Source]
+		if !ok {
+			eidsMap[eid.Source] = make(map[string][]int)
+		}
+		for _, uid := range eid.Uids {
+			eidsMap[eid.Source][uid.ID] = append(eidsMap[eid.Source][uid.ID], uid.Atype)
+		}
+	}
+
+	encodedEids := ""
+	if eidsString, err := json.Marshal(eidsMap); err == nil {
+		encodedEids = base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(eidsString)
+	}
+
+	return encodedEids
+}
+
 func getIPSafely(device *openrtb.Device) string {
 	if device == nil {
 		return ""
diff --git a/adapters/logicad/logicad.go b/adapters/logicad/logicad.go
new file mode 100644
index 00000000..e757705a
--- /dev/null
+++ b/adapters/logicad/logicad.go
@@ -0,0 +1,155 @@
+package logicad
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+
+	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+type LogicadAdapter struct {
+	endpoint string
+}
+
+func (adapter *LogicadAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	if len(request.Imp) == 0 {
+		return nil, []error{&errortypes.BadInput{Message: "No impression in the bid request"}}
+	}
+
+	pub2impressions, imps, errs := getImpressionsInfo(request.Imp)
+	if len(pub2impressions) == 0 || len(imps) == 0 {
+		return nil, errs
+	}
+
+	result := make([]*adapters.RequestData, 0, len(pub2impressions))
+	for k, imps := range pub2impressions {
+		bidRequest, err := adapter.buildAdapterRequest(request, &k, imps)
+		if err != nil {
+			errs = append(errs, err)
+		} else {
+			result = append(result, bidRequest)
+		}
+	}
+	return result, errs
+}
+
+func getImpressionsInfo(imps []openrtb.Imp) (map[openrtb_ext.ExtImpLogicad][]openrtb.Imp, []openrtb.Imp, []error) {
+	errors := make([]error, 0, len(imps))
+	resImps := make([]openrtb.Imp, 0, len(imps))
+	res := make(map[openrtb_ext.ExtImpLogicad][]openrtb.Imp)
+
+	for _, imp := range imps {
+		impExt, err := getImpressionExt(&imp)
+		if err != nil {
+			errors = append(errors, err)
+			continue
+		}
+		if err := validateImpression(&impExt); err != nil {
+			errors = append(errors, err)
+			continue
+		}
+
+		if res[impExt] == nil {
+			res[impExt] = make([]openrtb.Imp, 0)
+		}
+		res[impExt] = append(res[impExt], imp)
+		resImps = append(resImps, imp)
+	}
+	return res, resImps, errors
+}
+
+func validateImpression(impExt *openrtb_ext.ExtImpLogicad) error {
+	if impExt.Tid == "" {
+		return &errortypes.BadInput{Message: "No tid value provided"}
+	}
+	return nil
+}
+
+func getImpressionExt(imp *openrtb.Imp) (openrtb_ext.ExtImpLogicad, error) {
+	var bidderExt adapters.ExtImpBidder
+	var logicadExt openrtb_ext.ExtImpLogicad
+
+	if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
+		return logicadExt, &errortypes.BadInput{
+			Message: err.Error(),
+		}
+	}
+	if err := json.Unmarshal(bidderExt.Bidder, &logicadExt); err != nil {
+		return logicadExt, &errortypes.BadInput{
+			Message: err.Error(),
+		}
+	}
+	return logicadExt, nil
+}
+
+func (adapter *LogicadAdapter) buildAdapterRequest(prebidBidRequest *openrtb.BidRequest, params *openrtb_ext.ExtImpLogicad, imps []openrtb.Imp) (*adapters.RequestData, error) {
+	newBidRequest := createBidRequest(prebidBidRequest, params, imps)
+	reqJSON, err := json.Marshal(newBidRequest)
+	if err != nil {
+		return nil, err
+	}
+
+	headers := http.Header{}
+	headers.Add("Content-Type", "application/json;charset=utf-8")
+	headers.Add("Accept", "application/json")
+
+	return &adapters.RequestData{
+		Method:  "POST",
+		Uri:     adapter.endpoint,
+		Body:    reqJSON,
+		Headers: headers}, nil
+}
+
+func createBidRequest(prebidBidRequest *openrtb.BidRequest, params *openrtb_ext.ExtImpLogicad, imps []openrtb.Imp) *openrtb.BidRequest {
+	bidRequest := *prebidBidRequest
+	bidRequest.Imp = imps
+	for idx := range bidRequest.Imp {
+		imp := &bidRequest.Imp[idx]
+		imp.TagID = params.Tid
+		imp.Ext = nil
+	}
+	return &bidRequest
+}
+
+//MakeBids translates Logicad bid response to prebid-server specific format
+func (adapter *LogicadAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	if response.StatusCode == http.StatusNoContent {
+		return nil, nil
+	}
+	if response.StatusCode != http.StatusOK {
+		msg := fmt.Sprintf("Unexpected http status code: %d", response.StatusCode)
+		return nil, []error{&errortypes.BadServerResponse{Message: msg}}
+
+	}
+	var bidResp openrtb.BidResponse
+	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
+		msg := fmt.Sprintf("Bad server response: %d", err)
+		return nil, []error{&errortypes.BadServerResponse{Message: msg}}
+	}
+	if len(bidResp.SeatBid) != 1 {
+		msg := fmt.Sprintf("Invalid SeatBids count: %d", len(bidResp.SeatBid))
+		return nil, []error{&errortypes.BadServerResponse{Message: msg}}
+	}
+
+	seatBid := bidResp.SeatBid[0]
+	bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(seatBid.Bid))
+
+	for i := 0; i < len(seatBid.Bid); i++ {
+		bid := seatBid.Bid[i]
+		bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
+			Bid:     &bid,
+			BidType: openrtb_ext.BidTypeBanner,
+		})
+	}
+	return bidResponse, nil
+}
+
+func NewLogicadBidder(endpoint string) adapters.Bidder {
+	return &LogicadAdapter{
+		endpoint: endpoint,
+	}
+}
diff --git a/adapters/logicad/usersync.go b/adapters/logicad/usersync.go
new file mode 100644
index 00000000..d26a197b
--- /dev/null
+++ b/adapters/logicad/usersync.go
@@ -0,0 +1,12 @@
+package logicad
+
+import (
+	"text/template"
+
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/usersync"
+)
+
+func NewLogicadSyncer(temp *template.Template) usersync.Usersyncer {
+	return adapters.NewSyncer("logicad", 0, temp, adapters.SyncTypeRedirect)
+}
diff --git a/config/config.go b/config/config.go
index cc1d4a0a..5d538f38 100755
--- a/config/config.go
+++ b/config/config.go
@@ -597,6 +597,7 @@ func (cfg *Configuration) setDerivedDefaults() {
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderIx, "https://ssum.casalemedia.com/usermatchredir?s=184932&cb="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dix%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderLifestreet, "https://ads.lfstmedia.com/idsync/137062?synced=1&ttl=1s&rurl="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dlifestreet%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%24visitor_cookie%24%24")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderLockerDome, "https://lockerdome.com/usync/prebidserver?pid="+cfg.Adapters["lockerdome"].PlatformID+"&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dlockerdome%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7B%7Buid%7D%7D")
+	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderLogicad, "https://cr-p31.ladsp.jp/cookiesender/31?r=true&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&ru="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dlogicad%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderLunaMedia, "https://api.lunamedia.io/xp/user-sync?redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dlunamedia%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderMarsmedia, "https://dmp.rtbsrv.com/dmp/profiles/cm?p_id=179&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dmarsmedia%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUUID%7D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderMgid, "https://cm.mgid.com/m?cdsp=363893&adu="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dmgid%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Bmuidn%7D")
@@ -763,12 +764,8 @@ func SetupViper(v *viper.Viper, filename string) {
 	// Disabling adapters by default that require some specific config params.
 	// If you're using one of these, make sure you check out the documentation (https://github.com/prebid/prebid-server/tree/master/docs/bidders)
 	// for them and specify all the parameters they need for them to work correctly.
-	v.SetDefault("adapters.audiencenetwork.disabled", true)
-	v.SetDefault("adapters.rubicon.disabled", true)
 	v.SetDefault("adapters.33across.endpoint", "http://ssc.33across.com/api/v1/hb")
 	v.SetDefault("adapters.33across.partner_id", "")
-	v.SetDefault("adapters.dmx.endpoint", "https://dmx.districtm.io/b/v2")
-	v.SetDefault("adapters.adtelligent.endpoint", "http://hb.adtelligent.com/auction")
 	v.SetDefault("adapters.adform.endpoint", "http://adx.adform.net/adx")
 	v.SetDefault("adapters.adgeneration.endpoint", "https://d.socdm.com/adsv/v1")
 	v.SetDefault("adapters.adhese.endpoint", "https://ads-{{.AccountID}}.adhese.com/json")
@@ -786,6 +783,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.applogy.endpoint", "http://rtb.applogy.com/v1/prebid")
 	v.SetDefault("adapters.appnexus.endpoint", "http://ib.adnxs.com/openrtb2") // Docs: https://wiki.appnexus.com/display/supply/Incoming+Bid+Request+from+SSPs
 	v.SetDefault("adapters.appnexus.platform_id", "5")
+	v.SetDefault("adapters.audiencenetwork.disabled", true)
 	v.SetDefault("adapters.avocet.disabled", true)
 	v.SetDefault("adapters.beachfront.endpoint", "https://display.bfmio.com/prebid_display")
 	v.SetDefault("adapters.beachfront.extra_info", "{\"video_endpoint\":\"https://reachms.bfmio.com/bid.json?exchange_id\"}")
@@ -795,6 +793,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.conversant.endpoint", "http://api.hb.ad.cpe.dotomi.com/cvx/server/hb/ortb/25")
 	v.SetDefault("adapters.cpmstar.endpoint", "https://server.cpmstar.com/openrtbbidrq.aspx")
 	v.SetDefault("adapters.datablocks.endpoint", "http://{{.Host}}/openrtb2?sid={{.SourceId}}")
+	v.SetDefault("adapters.dmx.endpoint", "https://dmx.districtm.io/b/v2")
 	v.SetDefault("adapters.emx_digital.endpoint", "https://hb.emxdgt.com")
 	v.SetDefault("adapters.engagebdr.endpoint", "http://dsp.bnmla.com/hb")
 	v.SetDefault("adapters.eplanning.endpoint", "http://rtb.e-planning.net/pbs/1")
@@ -805,9 +804,10 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.improvedigital.endpoint", "http://ad.360yield.com/pbs")
 	v.SetDefault("adapters.ix.endpoint", "http://appnexus-us-east.lb.indexww.com/transbidder?p=184932")
 	v.SetDefault("adapters.kidoz.endpoint", "http://prebid-adapter.kidoz.net/openrtb2/auction?src=prebid-server")
-	v.SetDefault("adapters.kubient.endpoint", "http://kbntx.ch/prebid")
+	v.SetDefault("adapters.kubient.endpoint", "https://kssp.kbntx.ch/prebid")
 	v.SetDefault("adapters.lifestreet.endpoint", "https://prebid.s2s.lfstmedia.com/adrequest")
 	v.SetDefault("adapters.lockerdome.endpoint", "https://lockerdome.com/ladbid/prebidserver/openrtb2")
+	v.SetDefault("adapters.logicad.endpoint", "https://pbs.ladsp.com/adrequest/prebidserver")
 	v.SetDefault("adapters.lunamedia.endpoint", "http://api.lunamedia.io/xp/get?pubid={{.PublisherID}}")
 	v.SetDefault("adapters.marsmedia.endpoint", "https://bid306.rtbsrv.com/bidder/?bid=f3xtet")
 	v.SetDefault("adapters.mgid.endpoint", "https://prebid.mgid.com/prebid/")
@@ -821,6 +821,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.pulsepoint.endpoint", "http://bid.contextweb.com/header/s/ortb/prebid-s2s")
 	v.SetDefault("adapters.rhythmone.endpoint", "http://tag.1rx.io/rmp")
 	v.SetDefault("adapters.rtbhouse.endpoint", "http://prebidserver-s2s-ams.creativecdn.com/bidder/prebidserver/bids")
+	v.SetDefault("adapters.rubicon.disabled", true)
 	v.SetDefault("adapters.rubicon.endpoint", "http://exapi-us-east.rubiconproject.com/a/api/exchange.json")
 	v.SetDefault("adapters.sharethrough.endpoint", "http://btlr.sharethrough.com/FGMrCMMc/v1")
 	v.SetDefault("adapters.smartadserver.endpoint", "https://ssb.smartadserver.com")
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index 20acc2ae..3fd21321 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -290,6 +290,10 @@ func (deps *endpointDeps) validateRequest(req *openrtb.BidRequest) []error {
 		if err := validateBidAdjustmentFactors(bidExt.Prebid.BidAdjustmentFactors, aliases); err != nil {
 			return []error{err}
 		}
+
+		if err := validateSChains(bidExt); err != nil {
+			return []error{err}
+		}
 	}
 
 	if (req.Site == nil && req.App == nil) || (req.Site != nil && req.App != nil) {
@@ -362,6 +366,11 @@ func validateBidAdjustmentFactors(adjustmentFactors map[string]float64, aliases
 	return nil
 }
 
+func validateSChains(req *openrtb_ext.ExtRequest) error {
+	_, err := exchange.BidderToPrebidSChains(req)
+	return err
+}
+
 func (deps *endpointDeps) validateImp(imp *openrtb.Imp, aliases map[string]string, index int) []error {
 	if imp.ID == "" {
 		return []error{fmt.Errorf("request.imp[%d] missing required field: \"id\"", index)}
diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go
index 1f62d232..53607ac5 100755
--- a/exchange/adapter_map.go
+++ b/exchange/adapter_map.go
@@ -48,6 +48,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/kubient"
 	"github.com/prebid/prebid-server/adapters/lifestreet"
 	"github.com/prebid/prebid-server/adapters/lockerdome"
+	"github.com/prebid/prebid-server/adapters/logicad"
 	"github.com/prebid/prebid-server/adapters/lunamedia"
 	"github.com/prebid/prebid-server/adapters/marsmedia"
 	"github.com/prebid/prebid-server/adapters/mgid"
@@ -133,6 +134,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter
 		openrtb_ext.BidderKubient:         kubient.NewKubientBidder(cfg.Adapters[string(openrtb_ext.BidderKubient)].Endpoint),
 		openrtb_ext.BidderLockerDome:      lockerdome.NewLockerDomeBidder(cfg.Adapters[string(openrtb_ext.BidderLockerDome)].Endpoint),
 		openrtb_ext.BidderLunaMedia:       lunamedia.NewLunaMediaBidder(cfg.Adapters[string(openrtb_ext.BidderLunaMedia)].Endpoint),
+		openrtb_ext.BidderLogicad:         logicad.NewLogicadBidder(cfg.Adapters[string(openrtb_ext.BidderLogicad)].Endpoint),
 		openrtb_ext.BidderMarsmedia:       marsmedia.NewMarsmediaBidder(cfg.Adapters[string(openrtb_ext.BidderMarsmedia)].Endpoint),
 		openrtb_ext.BidderMgid:            mgid.NewMgidBidder(cfg.Adapters[string(openrtb_ext.BidderMgid)].Endpoint),
 		openrtb_ext.BidderMobileFuse:      mobilefuse.NewMobileFuseBidder(cfg.Adapters[string(openrtb_ext.BidderMobileFuse)].Endpoint),
diff --git a/exchange/utils.go b/exchange/utils.go
index 96c00ec0..4de985ec 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -28,6 +28,29 @@ type cleanMetrics struct {
 	gdprTcfVersion int
 }
 
+func BidderToPrebidSChains(req *openrtb_ext.ExtRequest) (map[string]*openrtb_ext.ExtRequestPrebidSChainSChain, error) {
+	bidderToSChains := make(map[string]*openrtb_ext.ExtRequestPrebidSChainSChain)
+
+	if len(req.Prebid.SChains) == 0 {
+		return bidderToSChains, nil
+	}
+
+	for _, schainWrapper := range req.Prebid.SChains {
+		if schainWrapper != nil && len(schainWrapper.Bidders) > 0 {
+			for _, bidder := range schainWrapper.Bidders {
+				if _, present := bidderToSChains[bidder]; present {
+					return nil, fmt.Errorf("request.ext.prebid.schains contains multiple schains for bidder %s; "+
+						"it must contain no more than one per bidder.", bidder)
+				} else {
+					bidderToSChains[bidder] = &schainWrapper.SChain
+				}
+			}
+		}
+	}
+
+	return bidderToSChains, nil
+}
+
 // cleanOpenRTBRequests splits the input request into requests which are sanitized for each bidder. Intended behavior is:
 //
 //   1. BidRequest.Imp[].Ext will only contain the "prebid" field and a "bidder" field which has the params for the intended Bidder.
@@ -103,12 +126,35 @@ func cleanOpenRTBRequests(ctx context.Context,
 	return
 }
 
-func splitBidRequest(req *openrtb.BidRequest, impsByBidder map[string][]openrtb.Imp, aliases map[string]string, usersyncs IdFetcher, blabels map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels, labels pbsmetrics.Labels) (map[openrtb_ext.BidderName]*openrtb.BidRequest, []error) {
+func splitBidRequest(req *openrtb.BidRequest,
+	impsByBidder map[string][]openrtb.Imp,
+	aliases map[string]string,
+	usersyncs IdFetcher,
+	blabels map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels,
+	labels pbsmetrics.Labels) (map[openrtb_ext.BidderName]*openrtb.BidRequest, []error) {
+
 	requestsByBidder := make(map[openrtb_ext.BidderName]*openrtb.BidRequest, len(impsByBidder))
 	explicitBuyerUIDs, err := extractBuyerUIDs(req.User)
 	if err != nil {
 		return nil, []error{err}
 	}
+
+	var requestExt openrtb_ext.ExtRequest
+	var sChainsByBidder map[string]*openrtb_ext.ExtRequestPrebidSChainSChain
+	if len(req.Ext) > 0 {
+		err := json.Unmarshal(req.Ext, &requestExt)
+		if err != nil {
+			return nil, []error{err}
+		}
+
+		sChainsByBidder, err = BidderToPrebidSChains(&requestExt)
+		if err != nil {
+			return nil, []error{err}
+		}
+	} else {
+		sChainsByBidder = make(map[string]*openrtb_ext.ExtRequestPrebidSChainSChain)
+	}
+
 	for bidder, imps := range impsByBidder {
 		reqCopy := *req
 		coreBidder := resolveBidder(bidder, aliases)
@@ -128,11 +174,57 @@ func splitBidRequest(req *openrtb.BidRequest, impsByBidder map[string][]openrtb.
 			blabels[coreBidder].CookieFlag = pbsmetrics.CookieFlagYes
 		}
 		reqCopy.Imp = imps
+
+		prepareSource(&reqCopy, bidder, sChainsByBidder)
+		prepareExt(&reqCopy, &requestExt)
+
 		requestsByBidder[openrtb_ext.BidderName(bidder)] = &reqCopy
 	}
 	return requestsByBidder, nil
 }
 
+func prepareExt(req *openrtb.BidRequest, unpackedExt *openrtb_ext.ExtRequest) {
+	if len(req.Ext) == 0 {
+		return
+	}
+	extCopy := *unpackedExt
+	extCopy.Prebid.SChains = nil
+	reqExt, err := json.Marshal(extCopy)
+	if err == nil {
+		req.Ext = reqExt
+	}
+}
+
+func prepareSource(req *openrtb.BidRequest, bidder string, sChainsByBidder map[string]*openrtb_ext.ExtRequestPrebidSChainSChain) {
+	const sChainWildCard = "*"
+	var selectedSChain *openrtb_ext.ExtRequestPrebidSChainSChain
+
+	wildCardSChain := sChainsByBidder[sChainWildCard]
+	bidderSChain := sChainsByBidder[bidder]
+
+	// source should not be modified
+	if bidderSChain == nil && wildCardSChain == nil {
+		return
+	}
+
+	if bidderSChain != nil {
+		selectedSChain = bidderSChain
+	} else {
+		selectedSChain = wildCardSChain
+	}
+
+	// set source
+	var source openrtb.Source
+	schain := openrtb_ext.ExtRequestPrebidSChain{
+		SChain: *selectedSChain,
+	}
+	sourceExt, err := json.Marshal(schain)
+	if err == nil {
+		source.Ext = sourceExt
+		req.Source = &source
+	}
+}
+
 // extractBuyerUIDs parses the values from user.ext.prebid.buyeruids, and then deletes those values from the ext.
 // This prevents a Bidder from using these values to figure out who else is involved in the Auction.
 func extractBuyerUIDs(user *openrtb.User) (map[string]string, error) {
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 49d7b09d..62fb9750 100755
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -64,6 +64,7 @@ const (
 	BidderKubient          BidderName = "kubient"
 	BidderLifestreet       BidderName = "lifestreet"
 	BidderLockerDome       BidderName = "lockerdome"
+	BidderLogicad          BidderName = "logicad"
 	BidderLunaMedia        BidderName = "lunamedia"
 	BidderMarsmedia        BidderName = "marsmedia"
 	BidderMgid             BidderName = "mgid"
@@ -145,6 +146,7 @@ var BidderMap = map[string]BidderName{
 	"kubient":           BidderKubient,
 	"lifestreet":        BidderLifestreet,
 	"lockerdome":        BidderLockerDome,
+	"logicad":           BidderLogicad,
 	"lunamedia":         BidderLunaMedia,
 	"marsmedia":         BidderMarsmedia,
 	"mgid":              BidderMgid,
diff --git a/openrtb_ext/imp_logicad.go b/openrtb_ext/imp_logicad.go
new file mode 100644
index 00000000..e4e3c3b0
--- /dev/null
+++ b/openrtb_ext/imp_logicad.go
@@ -0,0 +1,5 @@
+package openrtb_ext
+
+type ExtImpLogicad struct {
+	Tid string `json:"tid"`
+}
diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go
index 25b5c881..86388f60 100644
--- a/openrtb_ext/request.go
+++ b/openrtb_ext/request.go
@@ -12,12 +12,43 @@ type ExtRequest struct {
 
 // ExtRequestPrebid defines the contract for bidrequest.ext.prebid
 type ExtRequestPrebid struct {
-	Aliases              map[string]string      `json:"aliases,omitempty"`
-	BidAdjustmentFactors map[string]float64     `json:"bidadjustmentfactors,omitempty"`
-	Cache                *ExtRequestPrebidCache `json:"cache,omitempty"`
-	StoredRequest        *ExtStoredRequest      `json:"storedrequest,omitempty"`
-	Targeting            *ExtRequestTargeting   `json:"targeting,omitempty"`
-	SupportDeals         bool                   `json:"supportdeals,omitempty"`
+	Aliases              map[string]string         `json:"aliases,omitempty"`
+	BidAdjustmentFactors map[string]float64        `json:"bidadjustmentfactors,omitempty"`
+	Cache                *ExtRequestPrebidCache    `json:"cache,omitempty"`
+	SChains              []*ExtRequestPrebidSChain `json:"schains,omitempty"`
+	StoredRequest        *ExtStoredRequest         `json:"storedrequest,omitempty"`
+	Targeting            *ExtRequestTargeting      `json:"targeting,omitempty"`
+	SupportDeals         bool                      `json:"supportdeals,omitempty"`
+}
+
+// ExtRequestPrebid defines the contract for bidrequest.ext.prebid.schains
+type ExtRequestPrebidSChain struct {
+	Bidders []string                     `json:"bidders,omitempty"`
+	SChain  ExtRequestPrebidSChainSChain `json:"schain"`
+}
+
+// ExtRequestPrebidSChainSChain defines the contract for bidrequest.ext.prebid.schains[i].schain
+type ExtRequestPrebidSChainSChain struct {
+	Complete int                                 `json:"complete"`
+	Nodes    []*ExtRequestPrebidSChainSChainNode `json:"nodes"`
+	Ver      string                              `json:"ver"`
+	Ext      json.RawMessage                     `json:"ext,omitempty"`
+}
+
+// ExtRequestPrebidSChainSChainNode defines the contract for bidrequest.ext.prebid.schains[i].schain[i].nodes
+type ExtRequestPrebidSChainSChainNode struct {
+	ASI    string          `json:"asi"`
+	SID    string          `json:"sid"`
+	RID    string          `json:"rid,omitempty"`
+	Name   string          `json:"name,omitempty"`
+	Domain string          `json:"domain,omitempty"`
+	HP     int             `json:"hp"`
+	Ext    json.RawMessage `json:"ext,omitempty"`
+}
+
+// SourceExt defines the contract for bidrequest.source.ext
+type SourceExt struct {
+	SChain ExtRequestPrebidSChainSChain `json:"schain"`
 }
 
 // ExtRequestPrebidCache defines the contract for bidrequest.ext.prebid.cache
diff --git a/openrtb_ext/user.go b/openrtb_ext/user.go
index 520d73a6..b83f8233 100644
--- a/openrtb_ext/user.go
+++ b/openrtb_ext/user.go
@@ -43,6 +43,7 @@ type ExtUserEid struct {
 
 // ExtUserEidUid defines the contract for bidrequest.user.ext.eids[i].uids[j]
 type ExtUserEidUid struct {
-	ID  string          `json:"id"`
-	Ext json.RawMessage `json:"ext,omitempty"`
+	ID    string          `json:"id"`
+	Atype int             `json:"atype,omitempty"`
+	Ext   json.RawMessage `json:"ext,omitempty"`
 }
diff --git a/static/bidder-info/logicad.yaml b/static/bidder-info/logicad.yaml
new file mode 100644
index 00000000..c087516c
--- /dev/null
+++ b/static/bidder-info/logicad.yaml
@@ -0,0 +1,10 @@
+maintainer:
+  email: "prebid@so-netmedia.jp"
+capabilities:
+  site:
+    mediaTypes:
+      - banner
+  app:
+    mediaTypes:
+      - banner
+
diff --git a/static/bidder-params/logicad.json b/static/bidder-params/logicad.json
new file mode 100644
index 00000000..2a892f91
--- /dev/null
+++ b/static/bidder-params/logicad.json
@@ -0,0 +1,13 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "Logicad Adapter Params",
+  "description": "A schema which validates params accepted by the Logicad adapter",
+  "type": "object",
+  "properties": {
+    "tid": {
+      "type": "string",
+      "description": "Logicad for Publishers placement ID"
+    }
+  },
+  "required": ["tid"]
+}
diff --git a/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go
index f1f643af..89540ea2 100755
--- a/usersync/usersyncers/syncer.go
+++ b/usersync/usersyncers/syncer.go
@@ -39,6 +39,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/ix"
 	"github.com/prebid/prebid-server/adapters/lifestreet"
 	"github.com/prebid/prebid-server/adapters/lockerdome"
+	"github.com/prebid/prebid-server/adapters/logicad"
 	"github.com/prebid/prebid-server/adapters/lunamedia"
 	"github.com/prebid/prebid-server/adapters/marsmedia"
 	"github.com/prebid/prebid-server/adapters/mgid"
@@ -115,6 +116,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderIx, ix.NewIxSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderLifestreet, lifestreet.NewLifestreetSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderLockerDome, lockerdome.NewLockerDomeSyncer)
+	insertIntoMap(cfg, syncers, openrtb_ext.BidderLogicad, logicad.NewLogicadSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderLunaMedia, lunamedia.NewLunaMediaSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderMarsmedia, marsmedia.NewMarsmediaSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderMgid, mgid.NewMgidSyncer)


Test patch:
diff --git a/adapters/adform/adform_test.go b/adapters/adform/adform_test.go
index 63646f5f..2fca7d17 100644
--- a/adapters/adform/adform_test.go
+++ b/adapters/adform/adform_test.go
@@ -480,7 +480,33 @@ func getUserExt() []byte {
 		KeyV: 1,
 		Pref: 0,
 	}
+
+	eids := []openrtb_ext.ExtUserEid{
+		{
+			Source: "test.com",
+			Uids: []openrtb_ext.ExtUserEidUid{
+				{
+					ID:    "some_user_id",
+					Atype: 1,
+				},
+				{
+					ID: "other_user_id",
+				},
+			},
+		},
+		{
+			Source: "test2.org",
+			Uids: []openrtb_ext.ExtUserEidUid{
+				{
+					ID:    "other_user_id",
+					Atype: 2,
+				},
+			},
+		},
+	}
+
 	userExt := openrtb_ext.ExtUser{
+		Eids:      eids,
 		Consent:   "abc",
 		DigiTrust: &digitrust,
 	}
@@ -519,13 +545,16 @@ func assertAdformServerRequest(testData aBidInfo, r *http.Request, isOpenRtb boo
 	}
 
 	var midsWithCurrency = ""
+	var queryString = ""
 	if isOpenRtb {
 		midsWithCurrency = "bWlkPTMyMzQ0JnJjdXI9RVVSJm1rdj1jb2xvcjpyZWQsYWdlOjMwLTQwJm1rdz1yZWQsYmx1ZQ&bWlkPTMyMzQ1JnJjdXI9RVVS&bWlkPTMyMzQ2JnJjdXI9RVVS"
+		queryString = "CC=1&adid=6D92078A-8246-4BA4-AE5B-76104861E7DC&eids=eyJ0ZXN0LmNvbSI6eyJvdGhlcl91c2VyX2lkIjpbMF0sInNvbWVfdXNlcl9pZCI6WzFdfSwidGVzdDIub3JnIjp7Im90aGVyX3VzZXJfaWQiOlsyXX19&fd=1&gdpr=1&gdpr_consent=abc&ip=111.111.111.111&pt=gross&rp=4&stid=transaction-id&" + midsWithCurrency
 	} else {
 		midsWithCurrency = "bWlkPTMyMzQ0JnJjdXI9VVNEJm1rdj1jb2xvcjpyZWQsYWdlOjMwLTQwJm1rdz1yZWQsYmx1ZQ&bWlkPTMyMzQ1JnJjdXI9VVNE&bWlkPTMyMzQ2JnJjdXI9VVNE" // no way to pass currency in legacy adapter
+		queryString = "CC=1&adid=6D92078A-8246-4BA4-AE5B-76104861E7DC&fd=1&gdpr=1&gdpr_consent=abc&ip=111.111.111.111&pt=gross&rp=4&stid=transaction-id&" + midsWithCurrency
 	}
 
-	if ok, err := equal("CC=1&adid=6D92078A-8246-4BA4-AE5B-76104861E7DC&fd=1&gdpr=1&gdpr_consent=abc&ip=111.111.111.111&pt=gross&rp=4&stid=transaction-id&"+midsWithCurrency, r.URL.RawQuery, "Query string"); !ok {
+	if ok, err := equal(queryString, r.URL.RawQuery, "Query string"); !ok {
 		return err
 	}
 	if ok, err := equal("application/json;charset=utf-8", r.Header.Get("Content-Type"), "Content type"); !ok {
diff --git a/adapters/logicad/logicad_test.go b/adapters/logicad/logicad_test.go
new file mode 100644
index 00000000..adf20e4e
--- /dev/null
+++ b/adapters/logicad/logicad_test.go
@@ -0,0 +1,10 @@
+package logicad
+
+import (
+	"github.com/prebid/prebid-server/adapters/adapterstest"
+	"testing"
+)
+
+func TestJsonSamples(t *testing.T) {
+	adapterstest.RunJSONBidderTest(t, "logicadtest", NewLogicadBidder("https://localhost/adrequest/prebidserver"))
+}
diff --git a/adapters/logicad/logicadtest/exemplary/banner.json b/adapters/logicad/logicadtest/exemplary/banner.json
new file mode 100644
index 00000000..f782cc2b
--- /dev/null
+++ b/adapters/logicad/logicadtest/exemplary/banner.json
@@ -0,0 +1,92 @@
+{
+  "mockBidRequest": {
+    "id": "testid",
+    "imp": [
+      {
+        "id": "testimpid",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 320,
+              "h": 50
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "tid": "testtid"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://localhost/adrequest/prebidserver",
+        "body": {
+          "id": "testid",
+          "imp": [
+            {
+              "id": "testimpid",
+              "tagid": "testtid",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  },
+                  {
+                    "w": 320,
+                    "h": 50
+                  }
+                ]
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "crid": "123",
+                  "adid": "456",
+                  "price": 0.12,
+                  "id": "testid",
+                  "impid": "testimpid",
+                  "cid": "789"
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "crid": "123",
+            "adid": "456",
+            "price": 0.12,
+            "id": "testid",
+            "impid": "testimpid",
+            "cid": "789"
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/logicad/logicadtest/params/race/banner.json b/adapters/logicad/logicadtest/params/race/banner.json
new file mode 100644
index 00000000..7cb3de5a
--- /dev/null
+++ b/adapters/logicad/logicadtest/params/race/banner.json
@@ -0,0 +1,3 @@
+{
+  "tid": "testtid"
+}
\ No newline at end of file
diff --git a/adapters/logicad/logicadtest/supplemental/checkImp.json b/adapters/logicad/logicadtest/supplemental/checkImp.json
new file mode 100644
index 00000000..62c6e3e8
--- /dev/null
+++ b/adapters/logicad/logicadtest/supplemental/checkImp.json
@@ -0,0 +1,15 @@
+{
+  "mockBidRequest": {
+    "id": "testid",
+    "site": {
+      "id": "test",
+      "domain": "test.com"
+    }
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "No impression in the bid request",
+      "comparison": "literal"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/logicad/logicadtest/supplemental/ext.json b/adapters/logicad/logicadtest/supplemental/ext.json
new file mode 100644
index 00000000..ad358920
--- /dev/null
+++ b/adapters/logicad/logicadtest/supplemental/ext.json
@@ -0,0 +1,31 @@
+{
+  "mockBidRequest": {
+    "id": "testid",
+    "imp": [
+      {
+        "id": "testimpid",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 320,
+              "h": 50
+            }
+          ]
+        },
+        "ext": {
+          "tid": "testtid"
+        }
+      }
+    ]
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "unexpected end of JSON input",
+      "comparison": "literal"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/logicad/logicadtest/supplemental/missingtid.json b/adapters/logicad/logicadtest/supplemental/missingtid.json
new file mode 100644
index 00000000..5ed84cef
--- /dev/null
+++ b/adapters/logicad/logicadtest/supplemental/missingtid.json
@@ -0,0 +1,33 @@
+{
+  "mockBidRequest": {
+    "id": "testid",
+    "imp": [
+      {
+        "id": "testimpid",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 320,
+              "h": 50
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "tid": ""
+          }
+        }
+      }
+    ]
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "No tid value provided",
+      "comparison": "literal"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/logicad/logicadtest/supplemental/multiImpSameTid.json b/adapters/logicad/logicadtest/supplemental/multiImpSameTid.json
new file mode 100644
index 00000000..848733cd
--- /dev/null
+++ b/adapters/logicad/logicadtest/supplemental/multiImpSameTid.json
@@ -0,0 +1,112 @@
+{
+  "mockBidRequest": {
+    "id": "testid",
+    "imp": [
+      {
+        "id": "testimpid",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "tid": "testtid"
+          }
+        }
+      },
+      {
+        "id": "testimpid",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "tid": "testtid"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://localhost/adrequest/prebidserver",
+        "body": {
+          "id": "testid",
+          "imp": [
+            {
+              "id": "testimpid",
+              "tagid": "testtid",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ]
+              }
+            },
+            {
+              "id": "testimpid",
+              "tagid": "testtid",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ]
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "crid": "123",
+                  "adid": "456",
+                  "price": 0.12,
+                  "id": "testid",
+                  "impid": "testimpid",
+                  "cid": "789"
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "crid": "123",
+            "adid": "456",
+            "price": 0.12,
+            "id": "testid",
+            "impid": "testimpid",
+            "cid": "789"
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/logicad/logicadtest/supplemental/responseCode.json b/adapters/logicad/logicadtest/supplemental/responseCode.json
new file mode 100644
index 00000000..471993ad
--- /dev/null
+++ b/adapters/logicad/logicadtest/supplemental/responseCode.json
@@ -0,0 +1,72 @@
+{
+  "mockBidRequest": {
+    "id": "testid",
+    "site": {
+      "id": "test"
+    },
+    "imp": [
+      {
+        "id": "testimpid",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 320,
+              "h": 50
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "tid": "testtid"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://localhost/adrequest/prebidserver",
+        "body": {
+          "id": "testid",
+          "imp": [
+            {
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  },
+                  {
+                    "w": 320,
+                    "h": 50
+                  }
+                ]
+              },
+              "id": "testimpid",
+              "tagid": "testtid"
+            }
+          ],
+          "site": {
+            "id": "test"
+          }
+        }
+      },
+      "mockResponse": {
+        "body": {
+          "seatbid": []
+        }
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Unexpected http status code: 0",
+      "comparison": "literal"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/logicad/logicadtest/supplemental/responseNoBid.json b/adapters/logicad/logicadtest/supplemental/responseNoBid.json
new file mode 100644
index 00000000..6ddab2ab
--- /dev/null
+++ b/adapters/logicad/logicadtest/supplemental/responseNoBid.json
@@ -0,0 +1,66 @@
+{
+  "mockBidRequest": {
+    "id": "testid",
+    "imp": [
+      {
+        "id": "testimpid",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 320,
+              "h": 50
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "tid": "testtid"
+          }
+        }
+      }
+    ],
+    "site": {
+      "id": "test"
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://localhost/adrequest/prebidserver",
+        "body": {
+          "id": "testid",
+          "imp": [
+            {
+              "id": "testimpid",
+              "tagid": "testtid",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  },
+                  {
+                    "w": 320,
+                    "h": 50
+                  }
+                ]
+              }
+            }
+          ],
+          "site": {
+            "id": "test"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 204,
+        "body": {}
+      }
+    }
+  ],
+  "expectedBidResponses": []
+}
diff --git a/adapters/logicad/logicadtest/supplemental/responsebid.json b/adapters/logicad/logicadtest/supplemental/responsebid.json
new file mode 100644
index 00000000..59d1c2ac
--- /dev/null
+++ b/adapters/logicad/logicadtest/supplemental/responsebid.json
@@ -0,0 +1,73 @@
+{
+  "mockBidRequest": {
+    "id": "testid",
+    "site": {
+      "id": "test"
+    },
+    "imp": [
+      {
+        "id": "testimpid",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 320,
+              "h": 50
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "tid": "testtid"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://localhost/adrequest/prebidserver",
+        "body": {
+          "id": "testid",
+          "imp": [
+            {
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  },
+                  {
+                    "w": 320,
+                    "h": 50
+                  }
+                ]
+              },
+              "id": "testimpid",
+              "tagid": "testtid"
+            }
+          ],
+          "site": {
+            "id": "test"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "seatbid": []
+        }
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Invalid SeatBids count: 0",
+      "comparison": "literal"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/logicad/logicadtest/supplemental/site.json b/adapters/logicad/logicadtest/supplemental/site.json
new file mode 100644
index 00000000..c747413f
--- /dev/null
+++ b/adapters/logicad/logicadtest/supplemental/site.json
@@ -0,0 +1,98 @@
+{
+  "mockBidRequest": {
+    "id": "testid",
+    "site": {
+      "id": "test"
+    },
+    "imp": [
+      {
+        "id": "testimpid",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 320,
+              "h": 50
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "tid": "testtid"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://localhost/adrequest/prebidserver",
+        "body": {
+          "id": "testid",
+          "imp": [
+            {
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  },
+                  {
+                    "w": 320,
+                    "h": 50
+                  }
+                ]
+              },
+              "id": "testimpid",
+              "tagid": "testtid"
+            }
+          ],
+          "site": {
+            "id": "test"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "crid": "123",
+                  "adid": "456",
+                  "price": 0.12,
+                  "id": "testid",
+                  "impid": "testimpid",
+                  "cid": "789"
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "crid": "123",
+            "adid": "456",
+            "price": 0.12,
+            "id": "testid",
+            "impid": "testimpid",
+            "cid": "789"
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/logicad/params_test.go b/adapters/logicad/params_test.go
new file mode 100644
index 00000000..eb344528
--- /dev/null
+++ b/adapters/logicad/params_test.go
@@ -0,0 +1,45 @@
+package logicad
+
+import (
+	"encoding/json"
+	"github.com/prebid/prebid-server/openrtb_ext"
+	"testing"
+)
+
+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.BidderLogicad, json.RawMessage(validParam)); err != nil {
+			t.Errorf("Schema rejected LunaMedia params: %s", validParam)
+		}
+	}
+}
+
+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.BidderLogicad, json.RawMessage(invalidParam)); err == nil {
+			t.Errorf("Schema allowed unexpected params: %s", invalidParam)
+		}
+	}
+}
+
+var validParams = []string{
+	`{"tid": "testtid"}`,
+}
+
+var invalidParams = []string{
+	`nil`,
+	``,
+	`[]`,
+	`true`,
+	`{"tid": 42}`,
+}
diff --git a/adapters/logicad/usersync_test.go b/adapters/logicad/usersync_test.go
new file mode 100644
index 00000000..89d6207d
--- /dev/null
+++ b/adapters/logicad/usersync_test.go
@@ -0,0 +1,31 @@
+package logicad
+
+import (
+	"testing"
+	"text/template"
+
+	"github.com/prebid/prebid-server/privacy"
+	"github.com/prebid/prebid-server/privacy/gdpr"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestLogicadSyncer(t *testing.T) {
+	syncURL := "https://localhost/cookiesender?r=true&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&ru=localhost%2Fsetuid%3Fbidder%3Dlogicad%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID"
+	syncURLTemplate := template.Must(
+		template.New("sync-template").Parse(syncURL),
+	)
+
+	syncer := NewLogicadSyncer(syncURLTemplate)
+	syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{
+		GDPR: gdpr.Policy{
+			Signal:  "1",
+			Consent: "A",
+		},
+	})
+
+	assert.NoError(t, err)
+	assert.Equal(t, "https://localhost/cookiesender?r=true&gdpr=1&gdpr_consent=A&ru=localhost%2Fsetuid%3Fbidder%3Dlogicad%26gdpr%3D1%26gdpr_consent%3DA%26uid%3D%24UID", syncInfo.URL)
+	assert.Equal(t, "redirect", syncInfo.Type)
+	assert.EqualValues(t, 0, syncer.GDPRVendorID())
+	assert.Equal(t, false, syncInfo.SupportCORS)
+}
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index 97f0038a..c697c206 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -1039,6 +1039,53 @@ func TestCCPAInvalid(t *testing.T) {
 	assert.Empty(t, req.Regs.Ext, "Invalid Consent Removed From Request")
 }
 
+func TestSChainInvalid(t *testing.T) {
+	deps := &endpointDeps{
+		&nobidExchange{},
+		newParamsValidator(t),
+		&mockStoredReqFetcher{},
+		empty_fetcher.EmptyFetcher{},
+		empty_fetcher.EmptyFetcher{},
+		&config.Configuration{},
+		pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{}),
+		analyticsConf.NewPBSAnalytics(&config.Analytics{}),
+		map[string]string{},
+		false,
+		[]byte{},
+		openrtb_ext.BidderMap,
+		nil,
+		nil,
+		hardcodedResponseIPValidator{response: true},
+	}
+
+	ui := uint64(1)
+	req := openrtb.BidRequest{
+		ID: "someID",
+		Imp: []openrtb.Imp{
+			{
+				ID: "imp-ID",
+				Banner: &openrtb.Banner{
+					W: &ui,
+					H: &ui,
+				},
+				Ext: json.RawMessage(`{"appnexus": {"placementId": 5667}}`),
+			},
+		},
+		Site: &openrtb.Site{
+			ID: "myID",
+		},
+		Regs: &openrtb.Regs{
+			Ext: json.RawMessage(`{"us_privacy":"abcd"}`),
+		},
+		Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller1.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller2.com","sid":"00002","rid":"BidRequest2","hp":1}],"ver":"1.0"}}]}}`),
+	}
+
+	errL := deps.validateRequest(&req)
+
+	expectedError := fmt.Errorf("request.ext.prebid.schains contains multiple schains for bidder appnexus; it must contain no more than one per bidder.")
+	assert.ElementsMatch(t, errL, []error{expectedError})
+}
+
 func TestSanitizeRequest(t *testing.T) {
 	testCases := []struct {
 		description  string
diff --git a/exchange/utils_test.go b/exchange/utils_test.go
index e50d0f77..6d66e816 100644
--- a/exchange/utils_test.go
+++ b/exchange/utils_test.go
@@ -135,6 +135,92 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) {
 	}
 }
 
+func TestCleanOpenRTBRequestsSChain(t *testing.T) {
+	testCases := []struct {
+		description  string
+		inSourceExt  json.RawMessage
+		inExt        json.RawMessage
+		outSourceExt json.RawMessage
+		outExt       json.RawMessage
+		hasError     bool
+	}{
+		{
+			description:  "Empty root ext and source ext",
+			inSourceExt:  json.RawMessage(``),
+			inExt:        json.RawMessage(``),
+			outSourceExt: json.RawMessage(``),
+			outExt:       json.RawMessage(``),
+			hasError:     false,
+		},
+		{
+			description:  "No schains in root ext and empty source ext",
+			inSourceExt:  json.RawMessage(``),
+			inExt:        json.RawMessage(`{"prebid":{"schains":[]}}`),
+			outSourceExt: json.RawMessage(``),
+			outExt:       json.RawMessage(`{"prebid":{}}`),
+			hasError:     false,
+		},
+		{
+			description:  "Use source schain -- no bidder schain or wildcard schain in ext.prebid.schains",
+			inSourceExt:  json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"example.com","sid":"example1","rid":"ExampleReq1","hp":1}],"ver":"1.0"}}`),
+			inExt:        json.RawMessage(`{"prebid":{"schains":[{"bidders":["bidder1"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}]}}`),
+			outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"example.com","sid":"example1","rid":"ExampleReq1","hp":1}],"ver":"1.0"}}`),
+			outExt:       json.RawMessage(`{"prebid":{}}`),
+			hasError:     false,
+		},
+		{
+			description:  "Use schain for bidder in ext.prebid.schains",
+			inSourceExt:  json.RawMessage(``),
+			inExt:        json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}]}}`),
+			outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}`),
+			outExt:       json.RawMessage(`{"prebid":{}}`),
+			hasError:     false,
+		},
+		{
+			description:  "Use wildcard schain in ext.prebid.schains",
+			inSourceExt:  json.RawMessage(``),
+			inExt:        json.RawMessage(`{"prebid":{"schains":[{"bidders":["*"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}]}}`),
+			outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}`),
+			outExt:       json.RawMessage(`{"prebid":{}}`),
+			hasError:     false,
+		},
+		{
+			description:  "Use schain for bidder in ext.prebid.schains instead of wildcard",
+			inSourceExt:  json.RawMessage(``),
+			inExt:        json.RawMessage(`{"prebid":{"aliases":{"appnexus":"alias1"},"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["*"],"schain":{"complete":1,"nodes":[{"asi":"wildcard.com","sid":"wildcard1","rid":"WildcardReq1","hp":1}],"ver":"1.0"}} ]}}`),
+			outSourceExt: json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}`),
+			outExt:       json.RawMessage(`{"prebid":{"aliases":{"appnexus":"alias1"}}}`),
+			hasError:     false,
+		},
+		{
+			description:  "Use source schain -- multiple (two) bidder schains in ext.prebid.schains",
+			inSourceExt:  json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"example.com","sid":"example1","rid":"ExampleReq1","hp":1}],"ver":"1.0"}}`),
+			inExt:        json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller1.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller2.com","sid":"00002","rid":"BidRequest2","hp":1}],"ver":"1.0"}}]}}`),
+			outSourceExt: nil,
+			outExt:       nil,
+			hasError:     true,
+		},
+	}
+
+	for _, test := range testCases {
+		req := newBidRequest(t)
+		req.Source.Ext = test.inSourceExt
+		req.Ext = test.inExt
+
+		results, _, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, config.Privacy{})
+		result := results["appnexus"]
+
+		if test.hasError == true {
+			assert.NotNil(t, errs)
+			assert.Nil(t, result)
+		} else {
+			assert.Nil(t, errs)
+			assert.Equal(t, test.outSourceExt, result.Source.Ext, test.description+":Source.Ext")
+			assert.Equal(t, test.outExt, result.Ext, test.description+":Ext")
+		}
+	}
+}
+
 func TestCleanOpenRTBRequestsLMT(t *testing.T) {
 	var (
 		enabled  int8 = 1
@@ -302,5 +388,99 @@ func TestRandomizeList(t *testing.T) {
 	if len(adapters) != 1 {
 		t.Errorf("RandomizeList, expected a list of 1, found %d", len(adapters))
 	}
+}
+
+func TestBidderToPrebidChains(t *testing.T) {
+	input := openrtb_ext.ExtRequest{
+		Prebid: openrtb_ext.ExtRequestPrebid{
+			SChains: []*openrtb_ext.ExtRequestPrebidSChain{
+				{
+					Bidders: []string{"Bidder1", "Bidder2"},
+					SChain: openrtb_ext.ExtRequestPrebidSChainSChain{
+						Complete: 1,
+						Nodes: []*openrtb_ext.ExtRequestPrebidSChainSChainNode{
+							{
+								ASI:    "asi1",
+								SID:    "sid1",
+								Name:   "name1",
+								RID:    "rid1",
+								Domain: "domain1",
+								HP:     1,
+							},
+							{
+								ASI:    "asi2",
+								SID:    "sid2",
+								Name:   "name2",
+								RID:    "rid2",
+								Domain: "domain2",
+								HP:     2,
+							},
+						},
+						Ver: "version1",
+					},
+				},
+				{
+					Bidders: []string{"Bidder3", "Bidder4"},
+					SChain:  openrtb_ext.ExtRequestPrebidSChainSChain{},
+				},
+			},
+		},
+	}
+
+	output, err := BidderToPrebidSChains(&input)
+
+	assert.Nil(t, err)
+	assert.Equal(t, len(output), 4)
+	assert.Same(t, output["Bidder1"], &input.Prebid.SChains[0].SChain)
+	assert.Same(t, output["Bidder2"], &input.Prebid.SChains[0].SChain)
+	assert.Same(t, output["Bidder3"], &input.Prebid.SChains[1].SChain)
+	assert.Same(t, output["Bidder4"], &input.Prebid.SChains[1].SChain)
+}
+
+func TestBidderToPrebidChainsDiscardMultipleChainsForBidder(t *testing.T) {
+	input := openrtb_ext.ExtRequest{
+		Prebid: openrtb_ext.ExtRequestPrebid{
+			SChains: []*openrtb_ext.ExtRequestPrebidSChain{
+				{
+					Bidders: []string{"Bidder1"},
+					SChain:  openrtb_ext.ExtRequestPrebidSChainSChain{},
+				},
+				{
+					Bidders: []string{"Bidder1", "Bidder2"},
+					SChain:  openrtb_ext.ExtRequestPrebidSChainSChain{},
+				},
+			},
+		},
+	}
+
+	output, err := BidderToPrebidSChains(&input)
+
+	assert.NotNil(t, err)
+	assert.Nil(t, output)
+}
+
+func TestBidderToPrebidChainsNilSChains(t *testing.T) {
+	input := openrtb_ext.ExtRequest{
+		Prebid: openrtb_ext.ExtRequestPrebid{
+			SChains: nil,
+		},
+	}
+
+	output, err := BidderToPrebidSChains(&input)
+
+	assert.Nil(t, err)
+	assert.Equal(t, len(output), 0)
+}
+
+func TestBidderToPrebidChainsZeroLengthSChains(t *testing.T) {
+	input := openrtb_ext.ExtRequest{
+		Prebid: openrtb_ext.ExtRequestPrebid{
+			SChains: []*openrtb_ext.ExtRequestPrebidSChain{},
+		},
+	}
+
+	output, err := BidderToPrebidSChains(&input)
 
+	assert.Nil(t, err)
+	assert.Equal(t, len(output), 0)
 }
diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go
index b23541ea..32ab2e73 100755
--- a/usersync/usersyncers/syncer_test.go
+++ b/usersync/usersyncers/syncer_test.go
@@ -48,6 +48,7 @@ func TestNewSyncerMap(t *testing.T) {
 			string(openrtb_ext.BidderIx):               syncConfig,
 			string(openrtb_ext.BidderLifestreet):       syncConfig,
 			string(openrtb_ext.BidderLockerDome):       syncConfig,
+			string(openrtb_ext.BidderLogicad):          syncConfig,
 			string(openrtb_ext.BidderLunaMedia):        syncConfig,
 			string(openrtb_ext.BidderMarsmedia):        syncConfig,
 			string(openrtb_ext.BidderMgid):             syncConfig,