diff --git a/adapters/adtonos/adtonos.go b/adapters/adtonos/adtonos.go
new file mode 100644
index 00000000000..dff60733955
--- /dev/null
+++ b/adapters/adtonos/adtonos.go
@@ -0,0 +1,142 @@
+package adtonos
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "text/template"
+
+ "github.com/prebid/openrtb/v20/openrtb2"
+ "github.com/prebid/prebid-server/v2/adapters"
+ "github.com/prebid/prebid-server/v2/config"
+ "github.com/prebid/prebid-server/v2/errortypes"
+ "github.com/prebid/prebid-server/v2/macros"
+ "github.com/prebid/prebid-server/v2/openrtb_ext"
+)
+
+type adapter struct {
+ endpointTemplate *template.Template
+}
+
+func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
+ template, err := template.New("endpointTemplate").Parse(config.Endpoint)
+ if err != nil {
+ return nil, fmt.Errorf("unable to parse endpoint url template: %v", err)
+ }
+
+ bidder := &adapter{
+ endpointTemplate: template,
+ }
+ return bidder, nil
+}
+
+func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+ var bidderExt adapters.ExtImpBidder
+ if err := json.Unmarshal(request.Imp[0].Ext, &bidderExt); err != nil {
+ return nil, []error{&errortypes.BadInput{
+ Message: fmt.Sprintf("Invalid imp.ext for impression index %d. Error Infomation: %s", 0, err.Error()),
+ }}
+ }
+ var impExt openrtb_ext.ImpExtAdTonos
+ if err := json.Unmarshal(bidderExt.Bidder, &impExt); err != nil {
+ return nil, []error{&errortypes.BadInput{
+ Message: fmt.Sprintf("Invalid imp.ext.bidder for impression index %d. Error Infomation: %s", 0, err.Error()),
+ }}
+ }
+
+ endpoint, err := a.buildEndpointURL(&impExt)
+ if err != nil {
+ return nil, []error{err}
+ }
+
+ requestJson, err := json.Marshal(request)
+ if err != nil {
+ return nil, []error{err}
+ }
+
+ headers := http.Header{}
+ headers.Add("Content-Type", "application/json;charset=utf-8")
+ headers.Add("Accept", "application/json")
+
+ requestData := &adapters.RequestData{
+ Method: "POST",
+ Uri: endpoint,
+ Body: requestJson,
+ Headers: headers,
+ ImpIDs: openrtb_ext.GetImpIDs(request.Imp),
+ }
+
+ return []*adapters.RequestData{requestData}, nil
+}
+
+func (a *adapter) buildEndpointURL(params *openrtb_ext.ImpExtAdTonos) (string, error) {
+ endpointParams := macros.EndpointTemplateParams{PublisherID: params.SupplierID}
+ return macros.ResolveMacros(a.endpointTemplate, endpointParams)
+}
+
+func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+ if adapters.IsResponseStatusCodeNoContent(responseData) {
+ return nil, nil
+ }
+
+ if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil {
+ return nil, []error{err}
+ }
+
+ var response openrtb2.BidResponse
+ if err := json.Unmarshal(responseData.Body, &response); err != nil {
+ return nil, []error{err}
+ }
+
+ bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
+ bidResponse.Currency = response.Cur
+ var errors []error
+ for _, seatBid := range response.SeatBid {
+ for i := range seatBid.Bid {
+ bidType, err := getMediaTypeForBid(seatBid.Bid[i], request.Imp)
+ if err != nil {
+ errors = append(errors, err)
+ continue
+ }
+ b := &adapters.TypedBid{
+ Bid: &seatBid.Bid[i],
+ BidType: bidType,
+ }
+ bidResponse.Bids = append(bidResponse.Bids, b)
+ }
+ }
+ return bidResponse, errors
+}
+
+func getMediaTypeForBid(bid openrtb2.Bid, requestImps []openrtb2.Imp) (openrtb_ext.BidType, error) {
+ if bid.MType != 0 {
+ // If present, use explicit markup type annotation from the bidder:
+ switch bid.MType {
+ case openrtb2.MarkupAudio:
+ return openrtb_ext.BidTypeAudio, nil
+ case openrtb2.MarkupVideo:
+ return openrtb_ext.BidTypeVideo, nil
+ case openrtb2.MarkupBanner:
+ return openrtb_ext.BidTypeBanner, nil
+ case openrtb2.MarkupNative:
+ return openrtb_ext.BidTypeNative, nil
+ }
+ }
+ // As a fallback, guess markup type based on requested type - AdTonos is an audio company so we prioritize that.
+ for _, requestImp := range requestImps {
+ if requestImp.ID == bid.ImpID {
+ if requestImp.Audio != nil {
+ return openrtb_ext.BidTypeAudio, nil
+ } else if requestImp.Video != nil {
+ return openrtb_ext.BidTypeVideo, nil
+ } else {
+ return "", &errortypes.BadInput{
+ Message: fmt.Sprintf("Unsupported bidtype for bid: \"%s\"", bid.ImpID),
+ }
+ }
+ }
+ }
+ return "", &errortypes.BadInput{
+ Message: fmt.Sprintf("Failed to find impression: \"%s\"", bid.ImpID),
+ }
+}
diff --git a/adapters/adtonos/adtonos_test.go b/adapters/adtonos/adtonos_test.go
new file mode 100644
index 00000000000..612e71783be
--- /dev/null
+++ b/adapters/adtonos/adtonos_test.go
@@ -0,0 +1,30 @@
+package adtonos
+
+import (
+ "testing"
+
+ "github.com/prebid/prebid-server/v2/adapters/adapterstest"
+ "github.com/prebid/prebid-server/v2/config"
+ "github.com/prebid/prebid-server/v2/openrtb_ext"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestJsonSamples(t *testing.T) {
+ bidder, buildErr := Builder(openrtb_ext.BidderAdTonos, config.Adapter{
+ Endpoint: "http://exchange.example.com/bid/{{.PublisherID}}"},
+ config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})
+
+ if buildErr != nil {
+ t.Fatalf("Builder returned unexpected error %v", buildErr)
+ }
+
+ adapterstest.RunJSONBidderTest(t, "adtonostest", bidder)
+}
+
+func TestEndpointTemplateMalformed(t *testing.T) {
+ _, buildErr := Builder(openrtb_ext.BidderAdTonos, config.Adapter{
+ Endpoint: "{{Malformed}}"},
+ config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})
+
+ assert.Error(t, buildErr)
+}
diff --git a/adapters/adtonos/adtonostest/exemplary/simple-audio-with-mtype.json b/adapters/adtonos/adtonostest/exemplary/simple-audio-with-mtype.json
new file mode 100644
index 00000000000..c9103e37ee4
--- /dev/null
+++ b/adapters/adtonos/adtonostest/exemplary/simple-audio-with-mtype.json
@@ -0,0 +1,115 @@
+{
+ "mockBidRequest": {
+ "id": "some-request-id",
+ "tmax": 1000,
+ "imp": [
+ {
+ "id": "some-impression-id",
+ "bidfloor": 4.2,
+ "ext": {
+ "bidder": {
+ "supplierId": "777XYZ123"
+ }
+ },
+ "audio": {
+ "mimes": [
+ "audio/mpeg"
+ ]
+ }
+ }
+ ],
+ "test": 1,
+ "site": {
+ "publisher": {
+ "id": "1"
+ },
+ "page": "http://www.example.com",
+ "domain": "www.example.com"
+ },
+ "device": {}
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "http://exchange.example.com/bid/777XYZ123",
+ "headers": {
+ "Content-Type": [
+ "application/json;charset=utf-8"
+ ],
+ "Accept": [
+ "application/json"
+ ]
+ },
+ "body": {
+ "id": "some-request-id",
+ "tmax": 1000,
+ "imp": [
+ {
+ "id": "some-impression-id",
+ "bidfloor": 4.2,
+ "ext": {
+ "bidder": {
+ "supplierId": "777XYZ123"
+ }
+ },
+ "audio": {
+ "mimes": [
+ "audio/mpeg"
+ ]
+ }
+ }
+ ],
+ "site": {
+ "publisher": {
+ "id": "1"
+ },
+ "page": "http://www.example.com",
+ "domain": "www.example.com"
+ },
+ "device": {},
+ "test": 1
+ },
+ "impIDs":["some-impression-id"]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "some-request-id",
+ "cur": "USD",
+ "seatbid": [
+ {
+ "bid": [
+ {
+ "id": "1",
+ "impid": "some-impression-id",
+ "crid": "some-creative-id",
+ "adm": "TAG",
+ "price": 6.5,
+ "mtype": 3
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [
+ {
+ "currency": "USD",
+ "bids": [
+ {
+ "bid": {
+ "id": "1",
+ "impid": "some-impression-id",
+ "crid": "some-creative-id",
+ "adm": "TAG",
+ "price": 6.5,
+ "mtype": 3
+ },
+ "type": "audio"
+ }
+ ]
+ }
+ ]
+}
diff --git a/adapters/adtonos/adtonostest/exemplary/simple-audio.json b/adapters/adtonos/adtonostest/exemplary/simple-audio.json
new file mode 100644
index 00000000000..61cac002660
--- /dev/null
+++ b/adapters/adtonos/adtonostest/exemplary/simple-audio.json
@@ -0,0 +1,113 @@
+{
+ "mockBidRequest": {
+ "id": "some-request-id",
+ "tmax": 1000,
+ "imp": [
+ {
+ "id": "some-impression-id",
+ "bidfloor": 4.2,
+ "ext": {
+ "bidder": {
+ "supplierId": "777XYZ123"
+ }
+ },
+ "audio": {
+ "mimes": [
+ "audio/mpeg"
+ ]
+ }
+ }
+ ],
+ "test": 1,
+ "site": {
+ "publisher": {
+ "id": "1"
+ },
+ "page": "http://www.example.com",
+ "domain": "www.example.com"
+ },
+ "device": {}
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "http://exchange.example.com/bid/777XYZ123",
+ "headers": {
+ "Content-Type": [
+ "application/json;charset=utf-8"
+ ],
+ "Accept": [
+ "application/json"
+ ]
+ },
+ "body": {
+ "id": "some-request-id",
+ "tmax": 1000,
+ "imp": [
+ {
+ "id": "some-impression-id",
+ "bidfloor": 4.2,
+ "ext": {
+ "bidder": {
+ "supplierId": "777XYZ123"
+ }
+ },
+ "audio": {
+ "mimes": [
+ "audio/mpeg"
+ ]
+ }
+ }
+ ],
+ "site": {
+ "publisher": {
+ "id": "1"
+ },
+ "page": "http://www.example.com",
+ "domain": "www.example.com"
+ },
+ "device": {},
+ "test": 1
+ },
+ "impIDs":["some-impression-id"]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "some-request-id",
+ "cur": "USD",
+ "seatbid": [
+ {
+ "bid": [
+ {
+ "id": "1",
+ "impid": "some-impression-id",
+ "crid": "some-creative-id",
+ "adm": "TAG",
+ "price": 6.5
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [
+ {
+ "currency": "USD",
+ "bids": [
+ {
+ "bid": {
+ "id": "1",
+ "impid": "some-impression-id",
+ "crid": "some-creative-id",
+ "adm": "TAG",
+ "price": 6.5
+ },
+ "type": "audio"
+ }
+ ]
+ }
+ ]
+}
diff --git a/adapters/adtonos/adtonostest/exemplary/simple-video.json b/adapters/adtonos/adtonostest/exemplary/simple-video.json
new file mode 100644
index 00000000000..f00089b4008
--- /dev/null
+++ b/adapters/adtonos/adtonostest/exemplary/simple-video.json
@@ -0,0 +1,113 @@
+{
+ "mockBidRequest": {
+ "id": "video-request-id",
+ "tmax": 1000,
+ "imp": [
+ {
+ "id": "some-impression-id",
+ "bidfloor": 4.2,
+ "ext": {
+ "bidder": {
+ "supplierId": "777XYZ123"
+ }
+ },
+ "video": {
+ "mimes": [
+ "video/mp4"
+ ]
+ }
+ }
+ ],
+ "test": 1,
+ "site": {
+ "publisher": {
+ "id": "1"
+ },
+ "page": "http://www.example.com",
+ "domain": "www.example.com"
+ },
+ "device": {}
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "http://exchange.example.com/bid/777XYZ123",
+ "headers": {
+ "Content-Type": [
+ "application/json;charset=utf-8"
+ ],
+ "Accept": [
+ "application/json"
+ ]
+ },
+ "body": {
+ "id": "video-request-id",
+ "tmax": 1000,
+ "imp": [
+ {
+ "id": "some-impression-id",
+ "bidfloor": 4.2,
+ "ext": {
+ "bidder": {
+ "supplierId": "777XYZ123"
+ }
+ },
+ "video": {
+ "mimes": [
+ "video/mp4"
+ ]
+ }
+ }
+ ],
+ "site": {
+ "publisher": {
+ "id": "1"
+ },
+ "page": "http://www.example.com",
+ "domain": "www.example.com"
+ },
+ "device": {},
+ "test": 1
+ },
+ "impIDs":["some-impression-id"]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "video-request-id",
+ "cur": "USD",
+ "seatbid": [
+ {
+ "bid": [
+ {
+ "id": "1",
+ "impid": "some-impression-id",
+ "crid": "some-creative-id",
+ "adm": "TAG",
+ "price": 6.5
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [
+ {
+ "currency": "USD",
+ "bids": [
+ {
+ "bid": {
+ "id": "1",
+ "impid": "some-impression-id",
+ "crid": "some-creative-id",
+ "adm": "TAG",
+ "price": 6.5
+ },
+ "type": "video"
+ }
+ ]
+ }
+ ]
+}
diff --git a/adapters/adtonos/adtonostest/supplemental/wrong-impression-mapping.json b/adapters/adtonos/adtonostest/supplemental/wrong-impression-mapping.json
new file mode 100644
index 00000000000..6fa20e3d3c3
--- /dev/null
+++ b/adapters/adtonos/adtonostest/supplemental/wrong-impression-mapping.json
@@ -0,0 +1,103 @@
+{
+ "mockBidRequest": {
+ "id": "some-request-id",
+ "tmax": 1000,
+ "imp": [
+ {
+ "id": "correct-impression-id",
+ "bidfloor": 4.2,
+ "ext": {
+ "bidder": {
+ "supplierId": "777XYZ123"
+ }
+ },
+ "audio": {
+ "mimes": [
+ "audio/mpeg"
+ ]
+ }
+ }
+ ],
+ "test": 1,
+ "site": {
+ "publisher": {
+ "id": "1"
+ },
+ "page": "http://www.example.com",
+ "domain": "www.example.com"
+ },
+ "device": {}
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "http://exchange.example.com/bid/777XYZ123",
+ "headers": {
+ "Content-Type": [
+ "application/json;charset=utf-8"
+ ],
+ "Accept": [
+ "application/json"
+ ]
+ },
+ "body": {
+ "id": "some-request-id",
+ "tmax": 1000,
+ "imp": [
+ {
+ "id": "correct-impression-id",
+ "bidfloor": 4.2,
+ "ext": {
+ "bidder": {
+ "supplierId": "777XYZ123"
+ }
+ },
+ "audio": {
+ "mimes": [
+ "audio/mpeg"
+ ]
+ }
+ }
+ ],
+ "site": {
+ "publisher": {
+ "id": "1"
+ },
+ "page": "http://www.example.com",
+ "domain": "www.example.com"
+ },
+ "device": {},
+ "test": 1
+ },
+ "impIDs":["correct-impression-id"]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "some-request-id",
+ "cur": "USD",
+ "seatbid": [
+ {
+ "bid": [
+ {
+ "id": "1",
+ "impid": "unexpected-impression-id",
+ "crid": "some-creative-id",
+ "adm": "TAG",
+ "price": 6.5
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [{"currency":"USD","bids":[]}],
+ "expectedMakeBidsErrors": [
+ {
+ "value": "Failed to find impression: \"unexpected-impression-id\"",
+ "comparison": "literal"
+ }
+ ]
+}
diff --git a/adapters/adtonos/params_test.go b/adapters/adtonos/params_test.go
new file mode 100644
index 00000000000..98fa85f7b0d
--- /dev/null
+++ b/adapters/adtonos/params_test.go
@@ -0,0 +1,43 @@
+package adtonos
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/prebid/prebid-server/v2/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.BidderAdTonos, 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.BidderAdTonos, json.RawMessage(p)); err == nil {
+ t.Errorf("Schema allowed invalid params: %s", p)
+ }
+ }
+}
+
+var validParams = []string{
+ `{"supplierId": ""}`,
+ `{"supplierId": "7YZxxxdJMSXWv7SwY"}`,
+}
+
+var invalidParams = []string{
+ `{"supplierId": 42}`,
+}
diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go
index 74fb044ec18..2f282b08cd3 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -26,6 +26,7 @@ import (
"github.com/prebid/prebid-server/v2/adapters/adsinteractive"
"github.com/prebid/prebid-server/v2/adapters/adtarget"
"github.com/prebid/prebid-server/v2/adapters/adtelligent"
+ "github.com/prebid/prebid-server/v2/adapters/adtonos"
"github.com/prebid/prebid-server/v2/adapters/adtrgtme"
"github.com/prebid/prebid-server/v2/adapters/advangelists"
"github.com/prebid/prebid-server/v2/adapters/adview"
@@ -246,6 +247,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
openrtb_ext.BidderAdtarget: adtarget.Builder,
openrtb_ext.BidderAdtrgtme: adtrgtme.Builder,
openrtb_ext.BidderAdtelligent: adtelligent.Builder,
+ openrtb_ext.BidderAdTonos: adtonos.Builder,
openrtb_ext.BidderAdvangelists: advangelists.Builder,
openrtb_ext.BidderAdView: adview.Builder,
openrtb_ext.BidderAdxcg: adxcg.Builder,
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 15113334222..2b6cbcb2cb8 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -43,6 +43,7 @@ var coreBidderNames []BidderName = []BidderName{
BidderAdtarget,
BidderAdtrgtme,
BidderAdtelligent,
+ BidderAdTonos,
BidderAdvangelists,
BidderAdView,
BidderAdxcg,
@@ -361,6 +362,7 @@ const (
BidderAdsinteractive BidderName = "adsinteractive"
BidderAdtarget BidderName = "adtarget"
BidderAdtrgtme BidderName = "adtrgtme"
+ BidderAdTonos BidderName = "adtonos"
BidderAdtelligent BidderName = "adtelligent"
BidderAdvangelists BidderName = "advangelists"
BidderAdView BidderName = "adview"
diff --git a/openrtb_ext/imp_adtonos.go b/openrtb_ext/imp_adtonos.go
new file mode 100644
index 00000000000..f59ee35b329
--- /dev/null
+++ b/openrtb_ext/imp_adtonos.go
@@ -0,0 +1,5 @@
+package openrtb_ext
+
+type ImpExtAdTonos struct {
+ SupplierID string `json:"supplierId"`
+}
diff --git a/static/bidder-info/adtonos.yaml b/static/bidder-info/adtonos.yaml
new file mode 100644
index 00000000000..37a81372710
--- /dev/null
+++ b/static/bidder-info/adtonos.yaml
@@ -0,0 +1,24 @@
+endpoint: https://exchange.adtonos.com/bid/{{.PublisherID}}
+maintainer:
+ email: support@adtonos.com
+gvlVendorID: 682
+geoscope:
+ - global
+modifyingVastXmlAllowed: true
+capabilities:
+ app:
+ mediaTypes:
+ - audio
+ # NOTE: This is purely an audio ad exchange - video ads are synthesized from audio ads for compatibility
+ # with mobile games that only understand video mimetypes. The visual layer is just a placeholder.
+ - video
+ site:
+ mediaTypes:
+ - audio
+ dooh:
+ mediaTypes:
+ - audio
+userSync:
+ redirect:
+ url: https://play.adtonos.com/redir?to={{.RedirectURL}}
+ userMacro: '@UUID@'
diff --git a/static/bidder-params/adtonos.json b/static/bidder-params/adtonos.json
new file mode 100644
index 00000000000..e43d23e2b4b
--- /dev/null
+++ b/static/bidder-params/adtonos.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "title": "AdTonos Adapter Params",
+ "description": "A schema which validates params accepted by the AdTonos adapter",
+
+ "type": "object",
+ "properties": {
+ "supplierId": {
+ "type": "string",
+ "description": "ID of the supplier account in AdTonos platform"
+ }
+ },
+ "required": ["supplierId"]
+}
\ No newline at end of file