From f993886fbc1b463eb4039b98839e4048cf8f1ee9 Mon Sep 17 00:00:00 2001
From: trchandraprakash <47793448+trchandraprakash@users.noreply.github.com>
Date: Fri, 24 Apr 2020 12:28:39 -0700
Subject: [PATCH] NinthDecimal Adapter (#1249)

Co-authored-by: Chandra Prakash <chandra.prakash@advangelists.com>
---
 adapters/ninthdecimal/ninthdecimal.go         | 236 ++++++++++++++++++
 adapters/ninthdecimal/ninthdecimal_test.go    |  10 +
 .../ninthdecimaltest/exemplary/banner.json    |  95 +++++++
 .../ninthdecimaltest/exemplary/video.json     |  83 ++++++
 .../ninthdecimaltest/params/race/banner.json  |   4 +
 .../ninthdecimaltest/params/race/video.json   |   4 +
 .../supplemental/checkImp.json                |  14 ++
 .../ninthdecimaltest/supplemental/compat.json |  80 ++++++
 .../ninthdecimaltest/supplemental/ext.json    |  33 +++
 .../supplemental/missingpub.json              |  35 +++
 .../supplemental/responseCode.json            |  78 ++++++
 .../supplemental/responsebid.json             |  79 ++++++
 .../ninthdecimaltest/supplemental/site.json   | 103 ++++++++
 .../ninthdecimaltest/supplemental/size.json   |  28 +++
 adapters/ninthdecimal/params_test.go          |  45 ++++
 adapters/ninthdecimal/usersync.go             |  12 +
 adapters/ninthdecimal/usersync_test.go        |  31 +++
 config/config.go                              |   2 +
 exchange/adapter_map.go                       |   2 +
 openrtb_ext/bidders.go                        |   2 +
 openrtb_ext/imp_ninthdecimal.go               |   6 +
 static/bidder-info/ninthdecimal.yaml          |  13 +
 static/bidder-params/ninthdecimal.json        |  18 ++
 usersync/usersyncers/syncer.go                |   2 +
 usersync/usersyncers/syncer_test.go           |   1 +
 25 files changed, 1016 insertions(+)
 create mode 100755 adapters/ninthdecimal/ninthdecimal.go
 create mode 100755 adapters/ninthdecimal/ninthdecimal_test.go
 create mode 100644 adapters/ninthdecimal/ninthdecimaltest/exemplary/banner.json
 create mode 100644 adapters/ninthdecimal/ninthdecimaltest/exemplary/video.json
 create mode 100644 adapters/ninthdecimal/ninthdecimaltest/params/race/banner.json
 create mode 100644 adapters/ninthdecimal/ninthdecimaltest/params/race/video.json
 create mode 100644 adapters/ninthdecimal/ninthdecimaltest/supplemental/checkImp.json
 create mode 100644 adapters/ninthdecimal/ninthdecimaltest/supplemental/compat.json
 create mode 100644 adapters/ninthdecimal/ninthdecimaltest/supplemental/ext.json
 create mode 100644 adapters/ninthdecimal/ninthdecimaltest/supplemental/missingpub.json
 create mode 100644 adapters/ninthdecimal/ninthdecimaltest/supplemental/responseCode.json
 create mode 100644 adapters/ninthdecimal/ninthdecimaltest/supplemental/responsebid.json
 create mode 100644 adapters/ninthdecimal/ninthdecimaltest/supplemental/site.json
 create mode 100644 adapters/ninthdecimal/ninthdecimaltest/supplemental/size.json
 create mode 100755 adapters/ninthdecimal/params_test.go
 create mode 100755 adapters/ninthdecimal/usersync.go
 create mode 100755 adapters/ninthdecimal/usersync_test.go
 mode change 100644 => 100755 config/config.go
 mode change 100644 => 100755 exchange/adapter_map.go
 mode change 100644 => 100755 openrtb_ext/bidders.go
 create mode 100755 openrtb_ext/imp_ninthdecimal.go
 create mode 100755 static/bidder-info/ninthdecimal.yaml
 create mode 100755 static/bidder-params/ninthdecimal.json
 mode change 100644 => 100755 usersync/usersyncers/syncer.go
 mode change 100644 => 100755 usersync/usersyncers/syncer_test.go

diff --git a/adapters/ninthdecimal/ninthdecimal.go b/adapters/ninthdecimal/ninthdecimal.go
new file mode 100755
index 00000000000..6d7cbdb6a9d
--- /dev/null
+++ b/adapters/ninthdecimal/ninthdecimal.go
@@ -0,0 +1,236 @@
+package ninthdecimal
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"text/template"
+
+	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/macros"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+type NinthDecimalAdapter struct {
+	EndpointTemplate template.Template
+}
+
+//MakeRequests prepares request information for prebid-server core
+func (adapter *NinthDecimalAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	errs := make([]error, 0, len(request.Imp))
+	if len(request.Imp) == 0 {
+		errs = append(errs, &errortypes.BadInput{Message: "No impression in the bid request"})
+		return nil, errs
+	}
+	pub2impressions, imps, err := getImpressionsInfo(request.Imp)
+	if len(imps) == 0 {
+		return nil, err
+	}
+	errs = append(errs, err...)
+
+	if len(pub2impressions) == 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)
+			return nil, errs
+		} else {
+			result = append(result, bidRequest)
+		}
+	}
+	return result, errs
+}
+
+// getImpressionsInfo checks each impression for validity and returns impressions copy with corresponding exts
+func getImpressionsInfo(imps []openrtb.Imp) (map[openrtb_ext.ExtImpNinthDecimal][]openrtb.Imp, []openrtb.Imp, []error) {
+	errors := make([]error, 0, len(imps))
+	resImps := make([]openrtb.Imp, 0, len(imps))
+	res := make(map[openrtb_ext.ExtImpNinthDecimal][]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
+		}
+		//dispatchImpressions
+		//Group impressions by NinthDecimal-specific parameters `pubid
+		if err := compatImpression(&imp); 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.ExtImpNinthDecimal) error {
+	if impExt.PublisherID == "" {
+		return &errortypes.BadInput{Message: "No pubid value provided"}
+	}
+	return nil
+}
+
+//Alter impression info to comply with NinthDecimal platform requirements
+func compatImpression(imp *openrtb.Imp) error {
+	imp.Ext = nil //do not forward ext to NinthDecimal platform
+	if imp.Banner != nil {
+		return compatBannerImpression(imp)
+	}
+	return nil
+}
+
+func compatBannerImpression(imp *openrtb.Imp) error {
+	// Create a copy of the banner, since imp is a shallow copy of the original.
+
+	bannerCopy := *imp.Banner
+	banner := &bannerCopy
+	//As banner.w/h are required fields for NinthDecimal platform - take the first format entry
+	if banner.W == nil || banner.H == nil {
+		if len(banner.Format) == 0 {
+			return &errortypes.BadInput{Message: "Expected at least one banner.format entry or explicit w/h"}
+		}
+		format := banner.Format[0]
+		banner.Format = banner.Format[1:]
+		banner.W = &format.W
+		banner.H = &format.H
+		imp.Banner = banner
+	}
+	return nil
+}
+
+func getImpressionExt(imp *openrtb.Imp) (*openrtb_ext.ExtImpNinthDecimal, error) {
+	var bidderExt adapters.ExtImpBidder
+	if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
+		return nil, &errortypes.BadInput{
+			Message: err.Error(),
+		}
+	}
+	var NinthDecimalExt openrtb_ext.ExtImpNinthDecimal
+	if err := json.Unmarshal(bidderExt.Bidder, &NinthDecimalExt); err != nil {
+		return nil, &errortypes.BadInput{
+			Message: err.Error(),
+		}
+	}
+	return &NinthDecimalExt, nil
+}
+
+func (adapter *NinthDecimalAdapter) buildAdapterRequest(prebidBidRequest *openrtb.BidRequest, params *openrtb_ext.ExtImpNinthDecimal, 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")
+	headers.Add("x-openrtb-version", "2.5")
+
+	url, err := adapter.buildEndpointURL(params)
+	if err != nil {
+		return nil, err
+	}
+
+	return &adapters.RequestData{
+		Method:  "POST",
+		Uri:     url,
+		Body:    reqJSON,
+		Headers: headers}, nil
+}
+
+func createBidRequest(prebidBidRequest *openrtb.BidRequest, params *openrtb_ext.ExtImpNinthDecimal, imps []openrtb.Imp) *openrtb.BidRequest {
+	bidRequest := *prebidBidRequest
+	bidRequest.Imp = imps
+	for idx := range bidRequest.Imp {
+		imp := &bidRequest.Imp[idx]
+		imp.TagID = params.Placement
+	}
+	if bidRequest.Site != nil {
+		// Need to copy Site as Request is a shallow copy
+		siteCopy := *bidRequest.Site
+		bidRequest.Site = &siteCopy
+		bidRequest.Site.Publisher = nil
+		bidRequest.Site.Domain = ""
+	}
+	if bidRequest.App != nil {
+		// Need to copy App as Request is a shallow copy
+		appCopy := *bidRequest.App
+		bidRequest.App = &appCopy
+		bidRequest.App.Publisher = nil
+	}
+	return &bidRequest
+}
+
+// Builds enpoint url based on adapter-specific pub settings from imp.ext
+func (adapter *NinthDecimalAdapter) buildEndpointURL(params *openrtb_ext.ExtImpNinthDecimal) (string, error) {
+	endpointParams := macros.EndpointTemplateParams{PublisherID: params.PublisherID}
+	return macros.ResolveMacros(adapter.EndpointTemplate, endpointParams)
+}
+
+//MakeBids translates NinthDecimal bid response to prebid-server specific format
+func (adapter *NinthDecimalAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	var msg = ""
+	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 {
+		var 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(bidResp.SeatBid[0].Bid))
+
+	for i := 0; i < len(seatBid.Bid); i++ {
+		bid := seatBid.Bid[i]
+		bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
+			Bid:     &bid,
+			BidType: getMediaTypeForImpID(bid.ImpID, internalRequest.Imp),
+		})
+	}
+	return bidResponse, nil
+}
+
+// getMediaTypeForImp figures out which media type this bid is for
+func getMediaTypeForImpID(impID string, imps []openrtb.Imp) openrtb_ext.BidType {
+	for _, imp := range imps {
+		if imp.ID == impID && imp.Video != nil {
+			return openrtb_ext.BidTypeVideo
+		}
+	}
+	return openrtb_ext.BidTypeBanner
+}
+
+// NewNinthDecimalAdapter to be called in prebid-server core to create NinthDecimal adapter instance
+func NewNinthDecimalBidder(endpointTemplate string) adapters.Bidder {
+	template, err := template.New("endpointTemplate").Parse(endpointTemplate)
+	if err != nil {
+		return nil
+	}
+	return &NinthDecimalAdapter{EndpointTemplate: *template}
+}
diff --git a/adapters/ninthdecimal/ninthdecimal_test.go b/adapters/ninthdecimal/ninthdecimal_test.go
new file mode 100755
index 00000000000..206455a64be
--- /dev/null
+++ b/adapters/ninthdecimal/ninthdecimal_test.go
@@ -0,0 +1,10 @@
+package ninthdecimal
+
+import (
+	"github.com/prebid/prebid-server/adapters/adapterstest"
+	"testing"
+)
+
+func TestJsonSamples(t *testing.T) {
+	adapterstest.RunJSONBidderTest(t, "ninthdecimaltest", NewNinthDecimalBidder("http://rtb.ninthdecimal.com/xp/get?pubid={{.PublisherID}}"))
+}
diff --git a/adapters/ninthdecimal/ninthdecimaltest/exemplary/banner.json b/adapters/ninthdecimal/ninthdecimaltest/exemplary/banner.json
new file mode 100644
index 00000000000..d2184fa06b6
--- /dev/null
+++ b/adapters/ninthdecimal/ninthdecimaltest/exemplary/banner.json
@@ -0,0 +1,95 @@
+{
+  "mockBidRequest": {
+    "id": "testid",
+    "imp": [
+      {
+        "id": "testimpid",
+        "banner": {
+          "format": [
+            {
+              "w": 320,
+              "h": 250
+            },
+            {
+              "w": 320,
+              "h": 300
+            }
+          ],
+          "w": 320,
+          "h": 250
+        },
+        "ext": {
+          "bidder": {
+            "pubid": "19f1b372c7548ec1fe734d2c9f8dc688",
+            "placement": "dummyplacement"
+          }
+        }
+      }
+    ]
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://rtb.ninthdecimal.com/xp/get?pubid=19f1b372c7548ec1fe734d2c9f8dc688",
+        "body":{
+          "id": "testid",
+          "imp": [{
+            "id": "testimpid",
+            "tagid": "dummyplacement",
+            "banner": {
+              "format": [{
+                "w": 320,
+                "h": 250
+              }, {
+                "w": 320,
+                "h": 300
+              }],
+              "w": 320,
+              "h": 250
+            }
+            
+          }]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "crid": "24080",
+                   "adid": "2068416",
+                  "price": 0.01,
+                  "id": "testid",
+                  "impid": "testimpid",
+                  "cid": "8048"
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "crid": "24080",
+           "adid": "2068416",
+            "price": 0.01,
+            "id": "testid",
+            "impid": "testimpid",
+            "cid": "8048"
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/ninthdecimal/ninthdecimaltest/exemplary/video.json b/adapters/ninthdecimal/ninthdecimaltest/exemplary/video.json
new file mode 100644
index 00000000000..4ad093e0648
--- /dev/null
+++ b/adapters/ninthdecimal/ninthdecimaltest/exemplary/video.json
@@ -0,0 +1,83 @@
+{
+  "mockBidRequest": {
+    "id": "testid",
+    "imp": [
+      {
+        "id": "testimpid",
+        "video": {
+            "mimes": [
+              "video/mp4"
+            ],
+            "w": 640,
+            "h": 480
+          },
+        "ext": {
+          "bidder": {
+            "pubid": "19f1b372c7548ec1fe734d2c9f8dc688",
+            "placement": "dummyplacement"
+          }
+        }
+      }
+    ]
+  },
+  
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://rtb.ninthdecimal.com/xp/get?pubid=19f1b372c7548ec1fe734d2c9f8dc688",
+        "body":{
+          "id": "testid",
+          "imp": [{
+            "id": "testimpid",
+            "tagid": "dummyplacement",
+            "video": {
+                "mimes": [
+                  "video/mp4"
+                ],
+                "w": 640,
+                "h": 480
+              }
+          }]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "crid": "24080",
+                   "adid": "2068416",
+                  "price": 0.01,
+                  "id": "testid",
+                  "impid": "testimpid",
+                  "cid": "8048"
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "crid": "24080",
+           "adid": "2068416",
+            "price": 0.01,
+            "id": "testid",
+            "impid": "testimpid",
+            "cid": "8048"
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/ninthdecimal/ninthdecimaltest/params/race/banner.json b/adapters/ninthdecimal/ninthdecimaltest/params/race/banner.json
new file mode 100644
index 00000000000..2eed8f2ec4e
--- /dev/null
+++ b/adapters/ninthdecimal/ninthdecimaltest/params/race/banner.json
@@ -0,0 +1,4 @@
+{
+  "pubid": "19f1b372c7548ec1fe734d2c9f8dc688",
+  "placement": "dummyplacement"
+}
\ No newline at end of file
diff --git a/adapters/ninthdecimal/ninthdecimaltest/params/race/video.json b/adapters/ninthdecimal/ninthdecimaltest/params/race/video.json
new file mode 100644
index 00000000000..2eed8f2ec4e
--- /dev/null
+++ b/adapters/ninthdecimal/ninthdecimaltest/params/race/video.json
@@ -0,0 +1,4 @@
+{
+  "pubid": "19f1b372c7548ec1fe734d2c9f8dc688",
+  "placement": "dummyplacement"
+}
\ No newline at end of file
diff --git a/adapters/ninthdecimal/ninthdecimaltest/supplemental/checkImp.json b/adapters/ninthdecimal/ninthdecimaltest/supplemental/checkImp.json
new file mode 100644
index 00000000000..ca48812b4df
--- /dev/null
+++ b/adapters/ninthdecimal/ninthdecimaltest/supplemental/checkImp.json
@@ -0,0 +1,14 @@
+{
+    "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/ninthdecimal/ninthdecimaltest/supplemental/compat.json b/adapters/ninthdecimal/ninthdecimaltest/supplemental/compat.json
new file mode 100644
index 00000000000..ff33b59cff9
--- /dev/null
+++ b/adapters/ninthdecimal/ninthdecimaltest/supplemental/compat.json
@@ -0,0 +1,80 @@
+{
+  "mockBidRequest": {
+    "id": "testid",
+    "imp": [
+      {
+        "id": "testimpid",
+        "banner": {
+          "format": [{
+            "w": 320,
+            "h": 250
+          }]
+        },
+        "ext": {
+          "bidder": {
+            "pubid": "19f1b372c7548ec1fe734d2c9f8dc688",
+            "placement": "dummyplacement"
+          }
+        }
+      }
+    ]
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://rtb.ninthdecimal.com/xp/get?pubid=19f1b372c7548ec1fe734d2c9f8dc688",
+        "body":{
+          "id": "testid",
+          "imp": [{
+            "banner": {
+              "h": 250,
+              "w": 320
+              },
+              "id": "testimpid",
+              "tagid": "dummyplacement"
+            
+          }]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "crid": "24080",
+                   "adid": "2068416",
+                  "price": 0.01,
+                  "id": "testid",
+                  "impid": "testimpid",
+                  "cid": "8048"
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "crid": "24080",
+           "adid": "2068416",
+            "price": 0.01,
+            "id": "testid",
+            "impid": "testimpid",
+            "cid": "8048"
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/ninthdecimal/ninthdecimaltest/supplemental/ext.json b/adapters/ninthdecimal/ninthdecimaltest/supplemental/ext.json
new file mode 100644
index 00000000000..3cfb878bd47
--- /dev/null
+++ b/adapters/ninthdecimal/ninthdecimaltest/supplemental/ext.json
@@ -0,0 +1,33 @@
+{
+  "mockBidRequest": {
+    "id": "testid",
+    "imp": [
+      {
+        "id": "testimpid",
+        "banner": {
+          "format": [
+            {
+              "w": 320,
+              "h": 250
+            },
+            {
+              "w": 320,
+              "h": 300
+            }
+          ],
+          "w": 320,
+          "h": 250
+        },
+        "ext": {
+            "pubid": "19f1b372c7548ec1fe734d2c9f8dc688",
+            "placement": "dummyplacement"
+        }
+      }
+    ]
+  },
+"expectedMakeRequestsErrors": [
+    {
+        "value": "unexpected end of JSON input",
+        "comparison": "literal"
+    }]
+}
\ No newline at end of file
diff --git a/adapters/ninthdecimal/ninthdecimaltest/supplemental/missingpub.json b/adapters/ninthdecimal/ninthdecimaltest/supplemental/missingpub.json
new file mode 100644
index 00000000000..b088917afa3
--- /dev/null
+++ b/adapters/ninthdecimal/ninthdecimaltest/supplemental/missingpub.json
@@ -0,0 +1,35 @@
+{
+    "mockBidRequest": {
+      "id": "testid",
+      "imp": [
+        {
+          "id": "testimpid",
+          "banner": {
+            "format": [
+              {
+                "w": 320,
+                "h": 250
+              },
+              {
+                "w": 320,
+                "h": 300
+              }
+            ],
+            "w": 320,
+            "h": 250
+          },
+          "ext": {
+            "bidder": {
+              "pubid": "",
+              "placement": "dummyplacement"
+            }
+          }
+        }
+      ]
+    },
+    "expectedMakeRequestsErrors": [
+      {
+          "value": "No pubid value provided",
+          "comparison": "literal"
+      }]
+  }
\ No newline at end of file
diff --git a/adapters/ninthdecimal/ninthdecimaltest/supplemental/responseCode.json b/adapters/ninthdecimal/ninthdecimaltest/supplemental/responseCode.json
new file mode 100644
index 00000000000..a68db2b823e
--- /dev/null
+++ b/adapters/ninthdecimal/ninthdecimaltest/supplemental/responseCode.json
@@ -0,0 +1,78 @@
+{
+  "mockBidRequest": {
+      "id": "testid",
+      "site": {
+          "id": "test"
+      },
+      "imp": [
+          {
+              "id": "testimpid",
+              "banner": {
+                  "format": [
+                      {
+                          "w": 320,
+                          "h": 250
+                      },
+                      {
+                          "w": 320,
+                          "h": 300
+                      }
+                  ],
+                  "w": 320,
+                  "h": 250
+              },
+              "ext": {
+                  "bidder": {
+                      "pubid": "yu",
+                      "placement": "dummyplacement"
+                  }
+              }
+          }
+      ]
+  },
+  "httpCalls": [
+      {
+          "expectedRequest": {
+              "uri": "http://rtb.ninthdecimal.com/xp/get?pubid=yu",
+              "body": {
+                  "id": "testid",
+                  "imp": [
+                      {
+                          "banner": {
+                              "format": [
+                                  {
+                                      "h": 250,
+                                      "w": 320
+                                  },
+                                  {
+                                      "h": 300,
+                                      "w": 320
+                                  }
+                              ],
+                              "h": 250,
+                              "w": 320
+                          },
+                          "id": "testimpid",
+                          "tagid": "dummyplacement"
+                      }
+                  ],
+                  "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/ninthdecimal/ninthdecimaltest/supplemental/responsebid.json b/adapters/ninthdecimal/ninthdecimaltest/supplemental/responsebid.json
new file mode 100644
index 00000000000..c03aa1b2d89
--- /dev/null
+++ b/adapters/ninthdecimal/ninthdecimaltest/supplemental/responsebid.json
@@ -0,0 +1,79 @@
+{
+  "mockBidRequest": {
+      "id": "testid",
+      "site": {
+          "id": "test"
+      },
+      "imp": [
+          {
+              "id": "testimpid",
+              "banner": {
+                  "format": [
+                      {
+                          "w": 320,
+                          "h": 250
+                      },
+                      {
+                          "w": 320,
+                          "h": 300
+                      }
+                  ],
+                  "w": 320,
+                  "h": 250
+              },
+              "ext": {
+                  "bidder": {
+                      "pubid": "yu",
+                      "placement": "dummyplacement"
+                  }
+              }
+          }
+      ]
+  },
+  "httpCalls": [
+      {
+          "expectedRequest": {
+              "uri": "http://rtb.ninthdecimal.com/xp/get?pubid=yu",
+              "body": {
+                  "id": "testid",
+                  "imp": [
+                      {
+                          "banner": {
+                              "format": [
+                                  {
+                                      "h": 250,
+                                      "w": 320
+                                  },
+                                  {
+                                      "h": 300,
+                                      "w": 320
+                                  }
+                              ],
+                              "h": 250,
+                              "w": 320
+                          },
+                          "id": "testimpid",
+                          "tagid": "dummyplacement"
+                      }
+                  ],
+                  "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/ninthdecimal/ninthdecimaltest/supplemental/site.json b/adapters/ninthdecimal/ninthdecimaltest/supplemental/site.json
new file mode 100644
index 00000000000..a3e160e4bd8
--- /dev/null
+++ b/adapters/ninthdecimal/ninthdecimaltest/supplemental/site.json
@@ -0,0 +1,103 @@
+{
+  "mockBidRequest": {
+      "id": "testid",
+      "site": {
+          "id": "test"
+      },
+      "imp": [
+          {
+              "id": "testimpid",
+              "banner": {
+                  "format": [
+                      {
+                          "w": 320,
+                          "h": 250
+                      },
+                      {
+                          "w": 320,
+                          "h": 300
+                      }
+                  ],
+                  "w": 320,
+                  "h": 250
+              },
+              "ext": {
+                  "bidder": {
+                      "pubid": "19f1b372c7548ec1fe734d2c9f8dc688",
+                      "placement": "dummyplacement"
+                  }
+              }
+          }
+      ]
+  },
+  "httpCalls": [
+      {
+          "expectedRequest": {
+              "uri": "http://rtb.ninthdecimal.com/xp/get?pubid=19f1b372c7548ec1fe734d2c9f8dc688",
+              "body": {
+                  "id": "testid",
+                  "imp": [
+                      {
+                          "banner": {
+                              "format": [
+                                  {
+                                      "h": 250,
+                                      "w": 320
+                                  },
+                                  {
+                                      "h": 300,
+                                      "w": 320
+                                  }
+                              ],
+                              "h": 250,
+                              "w": 320
+                          },
+                          "id": "testimpid",
+                          "tagid": "dummyplacement"
+                      }
+                  ],
+                  "site": {
+                      "id": "test"
+                  }
+              }
+          },
+          "mockResponse": {
+              "status": 200,
+              "body": {
+                  "seatbid": [
+                      {
+                          "bid": [
+                              {
+                                  "crid": "24080",
+                                  "adid": "2068416",
+                                  "price": 0.01,
+                                  "id": "testid",
+                                  "impid": "testimpid",
+                                  "cid": "8048"
+                              }
+                          ]
+                      }
+                  ]
+              }
+          }
+      }
+  ],
+  "expectedBidResponses": [
+      {
+          "currency": "USD",
+          "bids": [
+              {
+                  "bid": {
+                      "crid": "24080",
+                      "adid": "2068416",
+                      "price": 0.01,
+                      "id": "testid",
+                      "impid": "testimpid",
+                      "cid": "8048"
+                  },
+                  "type": "banner"
+              }
+          ]
+      }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/ninthdecimal/ninthdecimaltest/supplemental/size.json b/adapters/ninthdecimal/ninthdecimaltest/supplemental/size.json
new file mode 100644
index 00000000000..77228559eee
--- /dev/null
+++ b/adapters/ninthdecimal/ninthdecimaltest/supplemental/size.json
@@ -0,0 +1,28 @@
+{
+  "mockBidRequest": {
+    "id": "testid",
+    "site": {
+      "id": "test",
+      "domain": "test.com"
+    },
+    "imp": [
+      {
+        "id": "testimpid",
+        "banner": {
+          
+        },
+        "ext": {
+          "bidder": {
+            "pubid": "19f1b372c7548ec1fe734d2c9f8dc688",
+            "placement": "dummyplacement"
+          }
+        }
+      }
+    ]
+  },
+  "expectedMakeRequestsErrors": [
+    {
+        "value": "Expected at least one banner.format entry or explicit w/h",
+        "comparison": "literal"
+    }]
+}
\ No newline at end of file
diff --git a/adapters/ninthdecimal/params_test.go b/adapters/ninthdecimal/params_test.go
new file mode 100755
index 00000000000..8d3ef3d706f
--- /dev/null
+++ b/adapters/ninthdecimal/params_test.go
@@ -0,0 +1,45 @@
+package ninthdecimal
+
+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.BidderNinthDecimal, json.RawMessage(validParam)); err != nil {
+			t.Errorf("Schema rejected NinthDecimal 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.BidderNinthDecimal, json.RawMessage(invalidParam)); err == nil {
+			t.Errorf("Schema allowed unexpected params: %s", invalidParam)
+		}
+	}
+}
+
+var validParams = []string{
+	`{"pubid": "19f1b372c7548ec1fe734d2c9f8dc688"}`,
+}
+
+var invalidParams = []string{
+	`{"publisher": "19f1b372c7548ec1fe734d2c9f8dc688"}`,
+	`nil`,
+	``,
+	`[]`,
+	`true`,
+}
diff --git a/adapters/ninthdecimal/usersync.go b/adapters/ninthdecimal/usersync.go
new file mode 100755
index 00000000000..7a8d029cfa9
--- /dev/null
+++ b/adapters/ninthdecimal/usersync.go
@@ -0,0 +1,12 @@
+package ninthdecimal
+
+import (
+	"text/template"
+
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/usersync"
+)
+
+func NewNinthDecimalSyncer(temp *template.Template) usersync.Usersyncer {
+	return adapters.NewSyncer("ninthdecimal", 0, temp, adapters.SyncTypeIframe)
+}
diff --git a/adapters/ninthdecimal/usersync_test.go b/adapters/ninthdecimal/usersync_test.go
new file mode 100755
index 00000000000..ee121434f29
--- /dev/null
+++ b/adapters/ninthdecimal/usersync_test.go
@@ -0,0 +1,31 @@
+package ninthdecimal
+
+import (
+	"testing"
+	"text/template"
+
+	"github.com/prebid/prebid-server/privacy"
+	"github.com/prebid/prebid-server/privacy/gdpr"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNinthDecimalSyncer(t *testing.T) {
+	syncURL := "https://rtb.ninthdecimal.com/xp/user-sync?acctid={aid}&&redirect=localhost/setuid?bidder=ninthdecimal&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&uid=$UID"
+	syncURLTemplate := template.Must(
+		template.New("sync-template").Parse(syncURL),
+	)
+
+	syncer := NewNinthDecimalSyncer(syncURLTemplate)
+	syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{
+		GDPR: gdpr.Policy{
+			Signal:  "1",
+			Consent: "A",
+		},
+	})
+
+	assert.NoError(t, err)
+	assert.Equal(t, "https://rtb.ninthdecimal.com/xp/user-sync?acctid={aid}&&redirect=localhost/setuid?bidder=ninthdecimal&gdpr=1&gdpr_consent=A&uid=$UID", syncInfo.URL)
+	assert.Equal(t, "iframe", syncInfo.Type)
+	assert.EqualValues(t, 0, syncer.GDPRVendorID())
+	assert.Equal(t, false, syncInfo.SupportCORS)
+}
diff --git a/config/config.go b/config/config.go
old mode 100644
new mode 100755
index 2ea91bfa24b..02745bbb8d2
--- a/config/config.go
+++ b/config/config.go
@@ -523,6 +523,7 @@ func (cfg *Configuration) setDerivedDefaults() {
 	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")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderNanoInteractive, "https://ad.audiencemanager.de/hbs/cookie_sync?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirectUri="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dnanointeractive%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID")
+	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderNinthDecimal, "https://rtb.ninthdecimal.com/xp/user-sync?acctid={aid}&&redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dninthdecimal%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderOpenx, "https://rtb.openx.net/sync/prebid?r="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dopenx%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUID%7D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderPubmatic, "https://ads.pubmatic.com/AdServer/js/user_sync.html?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&predirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dpubmatic%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderPulsepoint, "https://bh.contextweb.com/rtset?pid=561205&ev=1&rurl="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dpulsepoint%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%25%25VGUID%25%25")
@@ -720,6 +721,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.marsmedia.endpoint", "https://bid306.rtbsrv.com/bidder/?bid=f3xtet")
 	v.SetDefault("adapters.mgid.endpoint", "https://prebid.mgid.com/prebid/")
 	v.SetDefault("adapters.nanointeractive.endpoint", "https://ad.audiencemanager.de/hbs")
+	v.SetDefault("adapters.ninthdecimal.endpoint", "http://rtb.ninthdecimal.com/xp/get?pubid={{.PublisherID}}")
 	v.SetDefault("adapters.openx.endpoint", "http://rtb.openx.net/prebid")
 	v.SetDefault("adapters.pubmatic.endpoint", "https://hbopenbid.pubmatic.com/translator?source=prebid-server")
 	v.SetDefault("adapters.pubnative.endpoint", "http://dsp.pubnative.net/bid/v1/request")
diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go
old mode 100644
new mode 100755
index 698824c3fe2..0b0c42d88b6
--- a/exchange/adapter_map.go
+++ b/exchange/adapter_map.go
@@ -41,6 +41,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/marsmedia"
 	"github.com/prebid/prebid-server/adapters/mgid"
 	"github.com/prebid/prebid-server/adapters/nanointeractive"
+	"github.com/prebid/prebid-server/adapters/ninthdecimal"
 	"github.com/prebid/prebid-server/adapters/openx"
 	"github.com/prebid/prebid-server/adapters/pubmatic"
 	"github.com/prebid/prebid-server/adapters/pubnative"
@@ -111,6 +112,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter
 		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.BidderNanoInteractive: nanointeractive.NewNanoIneractiveBidder(cfg.Adapters[string(openrtb_ext.BidderNanoInteractive)].Endpoint),
+		openrtb_ext.BidderNinthDecimal:    ninthdecimal.NewNinthDecimalBidder(cfg.Adapters[string(openrtb_ext.BidderNinthDecimal)].Endpoint),
 		openrtb_ext.BidderOpenx:           openx.NewOpenxBidder(cfg.Adapters[string(openrtb_ext.BidderOpenx)].Endpoint),
 		openrtb_ext.BidderPubmatic:        pubmatic.NewPubmaticBidder(client, cfg.Adapters[string(openrtb_ext.BidderPubmatic)].Endpoint),
 		openrtb_ext.BidderPubnative:       pubnative.NewPubnativeBidder(cfg.Adapters[string(openrtb_ext.BidderPubnative)].Endpoint),
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
old mode 100644
new mode 100755
index 5b0046ce096..f8973469e40
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -59,6 +59,7 @@ const (
 	BidderMarsmedia        BidderName = "marsmedia"
 	BidderMgid             BidderName = "mgid"
 	BidderNanoInteractive  BidderName = "nanointeractive"
+	BidderNinthDecimal     BidderName = "ninthdecimal"
 	BidderOpenx            BidderName = "openx"
 	BidderPubmatic         BidderName = "pubmatic"
 	BidderPubnative        BidderName = "pubnative"
@@ -125,6 +126,7 @@ var BidderMap = map[string]BidderName{
 	"marsmedia":         BidderMarsmedia,
 	"mgid":              BidderMgid,
 	"nanointeractive":   BidderNanoInteractive,
+	"ninthdecimal":      BidderNinthDecimal,
 	"openx":             BidderOpenx,
 	"pubmatic":          BidderPubmatic,
 	"pubnative":         BidderPubnative,
diff --git a/openrtb_ext/imp_ninthdecimal.go b/openrtb_ext/imp_ninthdecimal.go
new file mode 100755
index 00000000000..8fb794dbdf2
--- /dev/null
+++ b/openrtb_ext/imp_ninthdecimal.go
@@ -0,0 +1,6 @@
+package openrtb_ext
+
+type ExtImpNinthDecimal struct {
+	PublisherID string `json:"pubid"`
+	Placement   string `json:"placement,omitempty"`
+}
diff --git a/static/bidder-info/ninthdecimal.yaml b/static/bidder-info/ninthdecimal.yaml
new file mode 100755
index 00000000000..eda7d222a5f
--- /dev/null
+++ b/static/bidder-info/ninthdecimal.yaml
@@ -0,0 +1,13 @@
+maintainer:
+  email: "abudig@ninthdecimal.com"
+capabilities:
+  site:
+    mediaTypes:
+    - banner
+    - video
+    
+  app:
+    mediaTypes:
+    - banner
+    - video
+   
diff --git a/static/bidder-params/ninthdecimal.json b/static/bidder-params/ninthdecimal.json
new file mode 100755
index 00000000000..f230361d77e
--- /dev/null
+++ b/static/bidder-params/ninthdecimal.json
@@ -0,0 +1,18 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "NinthDecimal Adapter Params",
+  "description": "A schema which validates params accepted by the NinthDecimal adapter",
+  "type": "object",
+  "properties": {
+    "pubid": {
+      "type": "string",
+      "description": "An id used to identify NinthDecimal publisher.",
+      "minLength": 8
+    },
+    "placement": {
+      "type": "string",
+      "description": "A placement created on adserver."
+    }
+  },
+  "required": ["pubid"]
+}
diff --git a/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go
old mode 100644
new mode 100755
index 995f573adca..3c0931f4dca
--- a/usersync/usersyncers/syncer.go
+++ b/usersync/usersyncers/syncer.go
@@ -35,6 +35,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/marsmedia"
 	"github.com/prebid/prebid-server/adapters/mgid"
 	"github.com/prebid/prebid-server/adapters/nanointeractive"
+	"github.com/prebid/prebid-server/adapters/ninthdecimal"
 	"github.com/prebid/prebid-server/adapters/openx"
 	"github.com/prebid/prebid-server/adapters/pubmatic"
 	"github.com/prebid/prebid-server/adapters/pulsepoint"
@@ -100,6 +101,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderMarsmedia, marsmedia.NewMarsmediaSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderMgid, mgid.NewMgidSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderNanoInteractive, nanointeractive.NewNanoInteractiveSyncer)
+	insertIntoMap(cfg, syncers, openrtb_ext.BidderNinthDecimal, ninthdecimal.NewNinthDecimalSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderOpenx, openx.NewOpenxSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderPubmatic, pubmatic.NewPubmaticSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderPulsepoint, pulsepoint.NewPulsepointSyncer)
diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go
old mode 100644
new mode 100755
index c84b3d9b6b9..3e1de8ada74
--- a/usersync/usersyncers/syncer_test.go
+++ b/usersync/usersyncers/syncer_test.go
@@ -44,6 +44,7 @@ func TestNewSyncerMap(t *testing.T) {
 			string(openrtb_ext.BidderMarsmedia):        syncConfig,
 			string(openrtb_ext.BidderMgid):             syncConfig,
 			string(openrtb_ext.BidderNanoInteractive):  syncConfig,
+			string(openrtb_ext.BidderNinthDecimal):     syncConfig,
 			string(openrtb_ext.BidderOpenx):            syncConfig,
 			string(openrtb_ext.BidderPubmatic):         syncConfig,
 			string(openrtb_ext.BidderPulsepoint):       syncConfig,