diff --git a/adapters/relevantdigital/params_test.go b/adapters/relevantdigital/params_test.go
new file mode 100644
index 00000000000..14c8beeff36
--- /dev/null
+++ b/adapters/relevantdigital/params_test.go
@@ -0,0 +1,42 @@
+package relevantdigital
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/prebid/prebid-server/openrtb_ext"
+)
+
+func TestValidParams(t *testing.T) {
+ validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+ if err != nil {
+ t.Fatalf("Failed to fetch the json schema. %v", err)
+ }
+
+ for _, p := range validParams {
+ if err := validator.Validate(openrtb_ext.BidderRelevantDigital, json.RawMessage(p)); err != nil {
+ t.Errorf("Schema rejected valid params: %s", p)
+ }
+ }
+}
+
+func TestInvalidParams(t *testing.T) {
+ validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+ if err != nil {
+ t.Fatalf("Failed to fetch the json schema. %v", err)
+ }
+
+ for _, p := range invalidParams {
+ if err := validator.Validate(openrtb_ext.BidderRelevantDigital, json.RawMessage(p)); err == nil {
+ t.Errorf("Schema allowed invalid params: %s", p)
+ }
+ }
+}
+
+var validParams = []string{
+ `{"accountId": "5fcf49f83a64ba6602b5be7e", "placementId" : "63b68275b4f35962c8eec9b1_5fcf49f83a64ba6602b5be9a", "pbsHost" : "some-host" }`,
+}
+
+var invalidParams = []string{
+ `{"accountId": 123, "placementId" : 123, "pbsHost" : ""}`,
+}
diff --git a/adapters/relevantdigital/relevantdigital.go b/adapters/relevantdigital/relevantdigital.go
new file mode 100644
index 00000000000..10c92c4ff35
--- /dev/null
+++ b/adapters/relevantdigital/relevantdigital.go
@@ -0,0 +1,265 @@
+package relevantdigital
+
+import (
+ "encoding/json"
+ "fmt"
+ "math"
+ "net/http"
+ "strings"
+ "text/template"
+
+ "github.com/buger/jsonparser"
+ "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
+ name string
+}
+
+const RELEVANT_DOMAIN = ".relevant-digital.com"
+const DEFAULT_TIMEOUT = 1000
+const DEFAULT_BUFFER_MS = 250
+const STORED_REQUEST_EXT = "{\"prebid\":{\"debug\":%t,\"storedrequest\":{\"id\":\"%s\"}},\"relevant\":{\"count\":%d,\"adapterType\":\"server\"}}"
+const STORED_IMP_EXT = "{\"prebid\":{\"storedrequest\":{\"id\":\"%s\"}}}"
+
+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)
+ }
+ return &adapter{
+ endpoint: template,
+ name: bidderName.String(),
+ }, nil
+}
+
+func patchBidRequestExt(prebidBidRequest *openrtb2.BidRequest, id string) (error) {
+ count, cerr := jsonparser.GetInt(prebidBidRequest.Ext, "relevant", "count")
+ if cerr != nil {
+ count = 0
+ }
+
+ if count >= 5 {
+ return &errortypes.FailedToRequestBids{
+ Message: "too many requests",
+ }
+ } else {
+ count = count + 1
+ }
+
+ debug, derr := jsonparser.GetBoolean(prebidBidRequest.Ext, "prebid", "debug")
+ if derr != nil {
+ debug = false
+ }
+
+ prebidBidRequest.Ext = []byte(fmt.Sprintf(STORED_REQUEST_EXT, debug, id, count))
+ return nil
+}
+
+func patchBidImpExt(imp *openrtb2.Imp, id string) {
+ imp.Ext = []byte(fmt.Sprintf(STORED_IMP_EXT, id))
+ if imp.Banner != nil {
+ imp.Banner.Ext = nil
+ }
+ if imp.Video != nil {
+ imp.Video.Ext = nil
+ }
+ if imp.Native != nil {
+ imp.Native.Ext = nil
+ }
+ if imp.Audio != nil {
+ imp.Audio.Ext = nil
+ }
+}
+
+func setTMax(prebidBidRequest *openrtb2.BidRequest, pbsBufferMs int) {
+ timeout := float64(prebidBidRequest.TMax)
+ if timeout <= 0 {
+ timeout = DEFAULT_TIMEOUT
+ }
+ buffer := float64(pbsBufferMs)
+ prebidBidRequest.TMax = int64(math.Min(math.Max(timeout-buffer, buffer), timeout))
+}
+
+func cloneBidRequest(prebidBidRequest *openrtb2.BidRequest) (*openrtb2.BidRequest, error) {
+ jsonRes, err := json.Marshal(prebidBidRequest)
+ if err != nil {
+ return nil, err
+ }
+ var copy openrtb2.BidRequest
+ err = json.Unmarshal(jsonRes, ©)
+ return ©, err
+}
+
+func (a *adapter) createBidRequest(prebidBidRequest *openrtb2.BidRequest, params []*openrtb_ext.ExtRelevantDigital) (*openrtb2.BidRequest, error) {
+ bidRequestCopy, err := cloneBidRequest(prebidBidRequest)
+ if err != nil {
+ return nil, err
+ }
+
+ err = patchBidRequestExt(bidRequestCopy, params[0].AccountId)
+ if err != nil {
+ return nil, &errortypes.BadInput{
+ Message: fmt.Sprintf("failed to create bidRequest, error: %s", err),
+ }
+ }
+
+ setTMax(bidRequestCopy, params[0].PbsBufferMs)
+
+ for idx := range bidRequestCopy.Imp {
+ patchBidImpExt(&bidRequestCopy.Imp[idx], params[idx].PlacementId)
+ }
+ return bidRequestCopy, err
+}
+
+func (a *adapter) getImpressionExt(imp *openrtb2.Imp) (*openrtb_ext.ExtRelevantDigital, error) {
+ var bidderExt adapters.ExtImpBidder
+ if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
+ return nil, &errortypes.BadInput{
+ Message: "imp.ext not provided",
+ }
+ }
+ relevantExt := openrtb_ext.ExtRelevantDigital{PbsBufferMs: DEFAULT_BUFFER_MS}
+ if err := json.Unmarshal(bidderExt.Bidder, &relevantExt); err != nil {
+ return nil, &errortypes.BadInput{
+ Message: "ext.bidder not provided",
+ }
+ }
+ return &relevantExt, nil
+}
+
+func (a *adapter) buildEndpointURL(params *openrtb_ext.ExtRelevantDigital) (string, error) {
+ params.Host = strings.ReplaceAll(params.Host, "http://", "")
+ params.Host = strings.ReplaceAll(params.Host, "https://", "")
+ params.Host = strings.ReplaceAll(params.Host, RELEVANT_DOMAIN, "")
+
+ endpointParams := macros.EndpointTemplateParams{Host: params.Host}
+ return macros.ResolveMacros(a.endpoint, endpointParams)
+}
+
+func (adapter *adapter) buildAdapterRequest(prebidBidRequest *openrtb2.BidRequest, params []*openrtb_ext.ExtRelevantDigital) (*adapters.RequestData, error) {
+ newBidRequest, err := adapter.createBidRequest(prebidBidRequest, params)
+
+ if err != nil {
+ return nil, err
+ }
+
+ reqJSON, err := json.Marshal(newBidRequest)
+ if err != nil {
+ return nil, err
+ }
+
+ url, err := adapter.buildEndpointURL(params[0])
+ if err != nil {
+ return nil, err
+ }
+
+ return &adapters.RequestData{
+ Method: "POST",
+ Uri: url,
+ Body: reqJSON,
+ Headers: getHeaders(prebidBidRequest),
+ }, nil
+}
+
+func (adapter *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+ impParams, errs := adapter.getImpressionsInfo(request.Imp)
+ if len(errs) > 0 {
+ return nil, errs
+ }
+
+ bidRequest, err := adapter.buildAdapterRequest(request, impParams)
+ if err != nil {
+ errs = []error{err}
+ }
+
+ if bidRequest != nil {
+ return []*adapters.RequestData{bidRequest}, errs
+ }
+ return nil, errs
+}
+
+func (adapter *adapter) getImpressionsInfo(imps []openrtb2.Imp) (resImps []*openrtb_ext.ExtRelevantDigital, errors []error) {
+ for _, imp := range imps {
+ impExt, err := adapter.getImpressionExt(&imp)
+ if err != nil {
+ errors = append(errors, err)
+ continue
+ }
+ resImps = append(resImps, impExt)
+ }
+ return
+}
+
+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 getMediaTypeForBid(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.MarkupAudio:
+ return openrtb_ext.BidTypeAudio, nil
+ case openrtb2.MarkupNative:
+ return openrtb_ext.BidTypeNative, nil
+ default:
+ return "", fmt.Errorf("unable to fetch mediaType in multi-format: %s", bid.ImpID)
+ }
+}
+
+func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+ if responseData.StatusCode != http.StatusOK {
+ return nil, []error{&errortypes.BadInput{
+ Message: fmt.Sprintf("Unexpected status code: %d. Bad request from publisher. Run with request.debug = 1 for more info.", responseData.StatusCode),
+ }}
+ }
+
+ var response openrtb2.BidResponse
+ if err := json.Unmarshal(responseData.Body, &response); err != nil {
+ return nil, []error{err}
+ }
+
+ bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(response.SeatBid))
+ bidResponse.Currency = response.Cur
+ var errs []error
+ for _, seatBid := range response.SeatBid {
+ for i, bid := range seatBid.Bid {
+ bidType, err := getMediaTypeForBid(bid)
+ if err != nil {
+ errs = append(errs, err)
+ } else {
+ b := &adapters.TypedBid{
+ Bid: &seatBid.Bid[i],
+ BidType: bidType,
+ }
+ bidResponse.Bids = append(bidResponse.Bids, b)
+ }
+ }
+ }
+ return bidResponse, errs
+}
diff --git a/adapters/relevantdigital/relevantdigital_test.go b/adapters/relevantdigital/relevantdigital_test.go
new file mode 100644
index 00000000000..848f3057ecf
--- /dev/null
+++ b/adapters/relevantdigital/relevantdigital_test.go
@@ -0,0 +1,28 @@
+package relevantdigital
+
+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.BidderRelevantDigital, config.Adapter{
+ Endpoint: "https://{{.Host}}.relevant-digital.com/openrtb2/auction"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})
+
+ if buildErr != nil {
+ t.Fatalf("Builder returned unexpected error %v", buildErr)
+ }
+
+ adapterstest.RunJSONBidderTest(t, "relevantdigitaltest", bidder)
+}
+
+func TestEndpointTemplateMalformed(t *testing.T) {
+ _, buildErr := Builder(openrtb_ext.BidderAceex, config.Adapter{
+ Endpoint: "{{Malformed}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})
+
+ assert.Error(t, buildErr)
+}
diff --git a/adapters/relevantdigital/relevantdigitaltest/exemplary/simple-banner.json b/adapters/relevantdigital/relevantdigitaltest/exemplary/simple-banner.json
new file mode 100644
index 00000000000..3d23dcf61ef
--- /dev/null
+++ b/adapters/relevantdigital/relevantdigitaltest/exemplary/simple-banner.json
@@ -0,0 +1,198 @@
+{
+ "mockBidRequest": {
+ "id": "3621f78b-abdf-4562-8eca-1c5e893387d0",
+ "imp": [
+ {
+ "id": "div-1",
+ "banner": {
+ "format": [
+ {
+ "w": 300,
+ "h": 250
+ },
+ {
+ "w": 300,
+ "h": 600
+ },
+ {
+ "w": 320,
+ "h": 320
+ }
+ ]
+ },
+ "ext": {
+ "bidder": {
+ "accountId": "620523ae7f4bbe1691bbb815",
+ "pbsHost": "fakeHost",
+ "placementId": "620525862d7518bfd4bbb81e_620523b5d1dbed6b0fbbb817"
+ }
+ }
+ }
+ ],
+ "site": {
+ "domain": "somedomain.com",
+ "page": "https://somedomain.com",
+ "publisher": {
+ "id": "1001",
+ "domain": "somepub.com"
+ },
+ "ext": {
+ "amp": 0
+ }
+ }
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://fakeHost.relevant-digital.com/openrtb2/auction",
+ "body": {
+ "id": "3621f78b-abdf-4562-8eca-1c5e893387d0",
+ "imp": [
+ {
+ "id": "div-1",
+ "banner": {
+ "format": [
+ {
+ "w": 300,
+ "h": 250
+ },
+ {
+ "w": 300,
+ "h": 600
+ },
+ {
+ "w": 320,
+ "h": 320
+ }
+ ]
+ },
+ "ext": {
+ "prebid": {
+ "storedrequest": {
+ "id": "620525862d7518bfd4bbb81e_620523b5d1dbed6b0fbbb817"
+ }
+ }
+ }
+ }
+ ],
+ "site": {
+ "domain": "somedomain.com",
+ "page": "https://somedomain.com",
+ "publisher": {
+ "id": "1001",
+ "domain": "somepub.com"
+ },
+ "ext": {
+ "amp": 0
+ }
+ },
+ "ext": {
+ "prebid": {
+ "debug" : false,
+ "storedrequest": {
+ "id": "620523ae7f4bbe1691bbb815"
+ }
+ },
+ "relevant": {
+ "adapterType": "server",
+ "count": 1
+ }
+ },
+ "tmax": 750
+ }
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "3621f78b-abdf-4562-8eca-1c5e893387d0",
+ "seatbid": [
+ {
+ "seat": "relevantdigital",
+ "bid": [
+ {
+ "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
+ "impid": "div-1",
+ "price": 0.500000,
+ "adm": "",
+ "crid": "crid_10",
+ "w": 728,
+ "h": 90,
+ "mtype": 1,
+ "ext": {
+ "bidtype": 0,
+ "dspid": 6,
+ "origbidcpm": 11.13998874085194,
+ "origbidcur": "USD",
+ "prebid": {
+ "bidid": "09abd496-5706-4311-a356-d559629a1d17",
+ "events": {
+ "win": "https://fakeHost.relevant-digital.com/event?t=win\u0026b=09abd496-5706-4311-a356-d559629a1d17\u0026a=1001\u0026bidder=providerA\u0026ts=1694939785078",
+ "imp": "https://fakeHost.relevant-digital.com/event?t=imp\u0026b=09abd496-5706-4311-a356-d559629a1d17\u0026a=1001\u0026bidder=providerA\u0026ts=1694939785078"
+ },
+ "meta": {
+ "adaptercode": "relevantdigital"
+ },
+ "targeting": {
+ "hb_bidder": "relevantdigital",
+ "hb_cache_host": "somedomain.com",
+ "hb_cache_path": "/analytics_cache/read",
+ "hb_pb": "11.10",
+ "hb_size": "300x250"
+ },
+ "type": "banner"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "cur": "USD"
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [
+ {
+ "currency": "USD",
+ "bids": [
+ {
+ "bid": {
+ "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
+ "impid": "div-1",
+ "price": 0.5,
+ "adm": "",
+ "crid": "crid_10",
+ "w": 728,
+ "h": 90,
+ "mtype": 1,
+ "ext": {
+ "bidtype": 0,
+ "dspid": 6,
+ "origbidcpm": 11.13998874085194,
+ "origbidcur": "USD",
+ "prebid": {
+ "bidid": "09abd496-5706-4311-a356-d559629a1d17",
+ "events": {
+ "win": "https://fakeHost.relevant-digital.com/event?t=win\u0026b=09abd496-5706-4311-a356-d559629a1d17\u0026a=1001\u0026bidder=providerA\u0026ts=1694939785078",
+ "imp": "https://fakeHost.relevant-digital.com/event?t=imp\u0026b=09abd496-5706-4311-a356-d559629a1d17\u0026a=1001\u0026bidder=providerA\u0026ts=1694939785078"
+ },
+ "meta": {
+ "adaptercode": "relevantdigital"
+ },
+ "targeting": {
+ "hb_bidder": "relevantdigital",
+ "hb_cache_host": "somedomain.com",
+ "hb_cache_path": "/analytics_cache/read",
+ "hb_pb": "11.10",
+ "hb_size": "300x250"
+ },
+ "type": "banner"
+ }
+ }
+ },
+ "type": "banner"
+ }
+ ]
+ }
+ ]
+}
diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go
index 95cb1626085..7be6d86513c 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -133,6 +133,7 @@ import (
"github.com/prebid/prebid-server/adapters/pubnative"
"github.com/prebid/prebid-server/adapters/pulsepoint"
"github.com/prebid/prebid-server/adapters/pwbid"
+ "github.com/prebid/prebid-server/adapters/relevantdigital"
"github.com/prebid/prebid-server/adapters/revcontent"
"github.com/prebid/prebid-server/adapters/richaudience"
"github.com/prebid/prebid-server/adapters/rise"
@@ -334,6 +335,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
openrtb_ext.BidderPulsepoint: pulsepoint.Builder,
openrtb_ext.BidderPWBid: pwbid.Builder,
openrtb_ext.BidderQuantumdex: apacdex.Builder,
+ openrtb_ext.BidderRelevantDigital: relevantdigital.Builder,
openrtb_ext.BidderRevcontent: revcontent.Builder,
openrtb_ext.BidderRichaudience: richaudience.Builder,
openrtb_ext.BidderRise: rise.Builder,
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 475d56a7ba8..95ee3fff38a 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -163,6 +163,7 @@ var coreBidderNames []BidderName = []BidderName{
BidderPulsepoint,
BidderPWBid,
BidderQuantumdex,
+ BidderRelevantDigital,
BidderRevcontent,
BidderRichaudience,
BidderRise,
@@ -452,6 +453,7 @@ const (
BidderPulsepoint BidderName = "pulsepoint"
BidderPWBid BidderName = "pwbid"
BidderQuantumdex BidderName = "quantumdex"
+ BidderRelevantDigital BidderName = "relevantdigital"
BidderRevcontent BidderName = "revcontent"
BidderRichaudience BidderName = "richaudience"
BidderRise BidderName = "rise"
diff --git a/openrtb_ext/imp_relevantdigital.go b/openrtb_ext/imp_relevantdigital.go
new file mode 100644
index 00000000000..676df24dca8
--- /dev/null
+++ b/openrtb_ext/imp_relevantdigital.go
@@ -0,0 +1,8 @@
+package openrtb_ext
+
+type ExtRelevantDigital struct {
+ AccountId string `json:"accountId"`
+ PlacementId string `json:"placementId"`
+ Host string `json:"pbsHost"`
+ PbsBufferMs int `json:"pbsBufferMs"`
+}
diff --git a/static/bidder-info/relevantdigital.yaml b/static/bidder-info/relevantdigital.yaml
new file mode 100644
index 00000000000..edf6d6705bc
--- /dev/null
+++ b/static/bidder-info/relevantdigital.yaml
@@ -0,0 +1,16 @@
+endpoint: "https://{{.Host}}.relevant-digital.com/openrtb2/auction"
+maintainer:
+ email: "support@relevant-digital.com"
+gvlVendorID: 1100
+extra_info: ""
+capabilities:
+ app:
+ mediaTypes:
+ - banner
+ - video
+ - native
+ site:
+ mediaTypes:
+ - banner
+ - video
+ - native
diff --git a/static/bidder-params/relevantdigital.json b/static/bidder-params/relevantdigital.json
new file mode 100644
index 00000000000..5f88d85fe58
--- /dev/null
+++ b/static/bidder-params/relevantdigital.json
@@ -0,0 +1,29 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "title": "Relevant Digital Adapter Params",
+ "description": "A schema which validates params accepted by the Relevant Digital adapter",
+ "type": "object",
+ "properties": {
+ "accountId": {
+ "type": "string",
+ "description": "An ID which identifies the Relevant Digital account ID"
+ },
+ "placementId": {
+ "type": "string",
+ "description": "An ID which identifies the Relevant Digital placement ID"
+ },
+ "pbsHost": {
+ "type": "string",
+ "description": "Prebid Server Host supplied by Relevant Digital"
+ },
+ "pbsBufferMs": {
+ "type": "number",
+ "description": "TMax buffer, default is 250"
+ }
+ },
+ "required": [
+ "accountId",
+ "placementId",
+ "pbsHost"
+ ]
+}