diff --git a/adapters/adsmartx/adsmartx.go b/adapters/adsmartx/adsmartx.go
new file mode 100644
index 00000000000..fdad895b91f
--- /dev/null
+++ b/adapters/adsmartx/adsmartx.go
@@ -0,0 +1,139 @@
+package adsmartx
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/prebid/openrtb/v20/openrtb2"
+ "github.com/prebid/prebid-server/v3/adapters"
+ "github.com/prebid/prebid-server/v3/config"
+ "github.com/prebid/prebid-server/v3/errortypes"
+ "github.com/prebid/prebid-server/v3/openrtb_ext"
+ "github.com/prebid/prebid-server/v3/util/jsonutil"
+)
+
+type adapter struct {
+ endpoint string
+}
+
+func Builder(_ openrtb_ext.BidderName, cfg config.Adapter, _ config.Server) (adapters.Bidder, error) {
+ return &adapter{endpoint: cfg.Endpoint}, nil
+}
+
+func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+ var errs []error
+ validImps := make([]openrtb2.Imp, 0, len(request.Imp))
+ var setTestMode bool
+
+ for _, imp := range request.Imp {
+ impExt, err := parseImpExt(imp.Ext)
+ if err != nil {
+ errs = append(errs, fmt.Errorf("impID %s: %w", imp.ID, err))
+ continue
+ }
+
+ if imp.Banner == nil && imp.Video == nil {
+ errs = append(errs, fmt.Errorf("impID %s: no banner or video object specified", imp.ID))
+ continue
+ }
+
+ if imp.BidFloor == 0 && impExt.BidFloor > 0 {
+ imp.BidFloor = impExt.BidFloor
+ }
+
+ if impExt.TestMode == 1 {
+ setTestMode = true
+ }
+
+ validImps = append(validImps, imp)
+ }
+
+ if len(validImps) == 0 {
+ return nil, append(errs, fmt.Errorf("no valid impressions"))
+ }
+
+ request.Imp = validImps
+ if setTestMode {
+ request.Test = 1
+ }
+
+ reqJSON, err := jsonutil.Marshal(request)
+ if err != nil {
+ return nil, append(errs, err)
+ }
+
+ headers := http.Header{}
+ headers.Set("Content-Type", "application/json;charset=utf-8")
+ headers.Set("Accept", "application/json")
+
+ return []*adapters.RequestData{
+ {
+ Method: "POST",
+ Uri: a.endpoint,
+ Body: reqJSON,
+ Headers: headers,
+ ImpIDs: openrtb_ext.GetImpIDs(validImps),
+ },
+ }, errs
+}
+
+func parseImpExt(ext jsonutil.RawMessage) (openrtb_ext.ImpExtAdsmartx, error) {
+ var bidderExt adapters.ExtImpBidder
+ if err := jsonutil.Unmarshal(ext, &bidderExt); err != nil {
+ return openrtb_ext.ImpExtAdsmartx{}, err
+ }
+ var adsmartxExt openrtb_ext.ImpExtAdsmartx
+ if err := jsonutil.Unmarshal(bidderExt.Bidder, &adsmartxExt); err != nil {
+ return openrtb_ext.ImpExtAdsmartx{}, err
+ }
+ return adsmartxExt, nil
+}
+
+func (a *adapter) MakeBids(request *openrtb2.BidRequest, reqData *adapters.RequestData, respData *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+ if adapters.IsResponseStatusCodeNoContent(respData) {
+ return nil, nil
+ }
+ if err := adapters.CheckResponseStatusCodeForErrors(respData); err != nil {
+ return nil, []error{err}
+ }
+
+ var bidResp openrtb2.BidResponse
+ if err := jsonutil.Unmarshal(respData.Body, &bidResp); err != nil {
+ return nil, []error{err}
+ }
+
+ br := adapters.NewBidderResponseWithBidsCapacity(len(bidResp.SeatBid))
+ if bidResp.Cur != "" {
+ br.Currency = bidResp.Cur
+ }
+
+ var errs []error
+ for _, seatBid := range bidResp.SeatBid {
+ for i, bid := range seatBid.Bid {
+ bidType, err := getBidType(bid.MType)
+ if err != nil {
+ errs = append(errs, err)
+ continue
+ }
+
+ br.Bids = append(br.Bids, &adapters.TypedBid{
+ Bid: &seatBid.Bid[i],
+ BidType: bidType,
+ })
+ }
+ }
+ return br, errs
+}
+
+func getBidType(mtype openrtb2.MarkupType) (openrtb_ext.BidType, error) {
+ switch mtype {
+ case openrtb2.MarkupBanner:
+ return openrtb_ext.BidTypeBanner, nil
+ case openrtb2.MarkupVideo:
+ return openrtb_ext.BidTypeVideo, nil
+ default:
+ return "", &errortypes.BadServerResponse{
+ Message: fmt.Sprintf("unknown bid type mtype=%d", mtype),
+ }
+ }
+}
diff --git a/adapters/adsmartx/adsmartx_test.go b/adapters/adsmartx/adsmartx_test.go
new file mode 100644
index 00000000000..3896a0801dd
--- /dev/null
+++ b/adapters/adsmartx/adsmartx_test.go
@@ -0,0 +1,164 @@
+package adsmartx
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/prebid/openrtb/v20/openrtb2"
+ "github.com/prebid/prebid-server/v3/adapters"
+ "github.com/prebid/prebid-server/v3/adapters/adapterstest"
+ "github.com/prebid/prebid-server/v3/config"
+ "github.com/prebid/prebid-server/v3/openrtb_ext"
+ "github.com/prebid/prebid-server/v3/util/jsonutil"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestJsonSamples(t *testing.T) {
+ bidder, buildErr := Builder(
+ openrtb_ext.BidderAdsmartx,
+ config.Adapter{
+ Endpoint: "https://ads.adsmartx.com/ads/rtb/prebid/server",
+ },
+ config.Server{
+ ExternalUrl: "http://hosturl.com",
+ GvlID: 0,
+ DataCenter: "2",
+ },
+ )
+
+ require.NoError(t, buildErr, "Builder returned unexpected error")
+ adapterstest.RunJSONBidderTest(t, "adsmartxtest", bidder)
+}
+
+func TestParseImpExt(t *testing.T) {
+ tests := []struct {
+ name string
+ ext jsonutil.RawMessage
+ wantErr bool
+ }{
+ {"Valid ext", jsonutil.RawMessage(`{"bidder":{"bidfloor":0.5}}`), false},
+ {"Valid ext with sspId", jsonutil.RawMessage(`{"bidder":{"sspId":"ssp-123","siteId":"site-456"}}`), false},
+ {"Invalid JSON", jsonutil.RawMessage(`not-json`), true},
+ {"Not an object", jsonutil.RawMessage(`"string"`), true},
+ {"Bidder not object", jsonutil.RawMessage(`{"bidder":"not-an-object"}`), true},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ _, err := parseImpExt(tt.ext)
+ if tt.wantErr {
+ require.Error(t, err)
+ return
+ }
+ require.NoError(t, err)
+ })
+ }
+}
+
+func TestGetBidType(t *testing.T) {
+ tests := []struct {
+ name string
+ mtype openrtb2.MarkupType
+ wantErr bool
+ wantBidTy openrtb_ext.BidType
+ }{
+ {"Banner", openrtb2.MarkupBanner, false, openrtb_ext.BidTypeBanner},
+ {"Video", openrtb2.MarkupVideo, false, openrtb_ext.BidTypeVideo},
+ {"Unknown", 99, true, ""},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ bidType, err := getBidType(tt.mtype)
+ if tt.wantErr {
+ require.Error(t, err)
+ return
+ }
+ require.NoError(t, err)
+ assert.Equal(t, tt.wantBidTy, bidType)
+ })
+ }
+}
+
+func TestMakeRequestsErrors(t *testing.T) {
+ a := &adapter{endpoint: "http://test-endpoint"}
+ tests := []struct {
+ name string
+ imps []openrtb2.Imp
+ wantErr string
+ }{
+ {"Invalid ext", []openrtb2.Imp{{ID: "1", Ext: jsonutil.RawMessage(`not-json`)}}, "impID 1:"},
+ {"No valid imps", []openrtb2.Imp{}, "no valid impressions"},
+ {"No banner or video", []openrtb2.Imp{{ID: "1", Ext: jsonutil.RawMessage(`{"bidder":{"bidfloor": 0.5}}`)}}, "no banner or video object specified"},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ req := &openrtb2.BidRequest{Imp: tt.imps}
+ _, errs := a.MakeRequests(req, nil)
+ require.NotEmpty(t, errs, "expected error, got none")
+ found := false
+ for _, err := range errs {
+ if err != nil && (tt.wantErr == "" || strings.Contains(err.Error(), tt.wantErr)) {
+ found = true
+ break
+ }
+ }
+ assert.True(t, found, "expected error containing %q, got %v", tt.wantErr, errs)
+ })
+ }
+}
+
+func TestMakeBidsErrors(t *testing.T) {
+ a := &adapter{endpoint: "http://test-endpoint"}
+ validReq := &openrtb2.BidRequest{ID: "1"}
+ validReqData := &adapters.RequestData{}
+ tests := []struct {
+ name string
+ respData *adapters.ResponseData
+ wantErr string
+ }{
+ {"Non-200/204 response", &adapters.ResponseData{StatusCode: 500, Body: []byte(`{}`)}, "Unexpected status code"},
+ {"Invalid JSON", &adapters.ResponseData{StatusCode: 200, Body: []byte(`not-json`)}, ""},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ _, errs := a.MakeBids(validReq, validReqData, tt.respData)
+ require.NotEmpty(t, errs, "expected error, got none")
+ found := false
+ for _, err := range errs {
+ if err != nil && strings.Contains(err.Error(), tt.wantErr) {
+ found = true
+ break
+ }
+ }
+ assert.True(t, found, "expected error containing %q, got %v", tt.wantErr, errs)
+ })
+ }
+}
+
+func TestMakeBidsSkipsBadBidType(t *testing.T) {
+ a := &adapter{endpoint: "http://test-endpoint"}
+ validReq := &openrtb2.BidRequest{ID: "1"}
+ validReqData := &adapters.RequestData{}
+
+ respBody := `{
+ "id": "1",
+ "seatbid": [{
+ "bid": [
+ {"id": "good-bid", "impid": "1", "price": 1.0, "adm": "
ad
", "mtype": 1},
+ {"id": "bad-bid", "impid": "2", "price": 2.0, "adm": "ad2
", "mtype": 99},
+ {"id": "good-bid-2", "impid": "3", "price": 3.0, "adm": "ad3
", "mtype": 2}
+ ]
+ }],
+ "cur": "USD"
+ }`
+
+ resp := &adapters.ResponseData{StatusCode: 200, Body: []byte(respBody)}
+ bidderResp, errs := a.MakeBids(validReq, validReqData, resp)
+
+ require.NotNil(t, bidderResp, "expected bid response, got nil")
+ assert.Len(t, bidderResp.Bids, 2, "expected 2 valid bids (bad bid should be skipped)")
+ assert.Len(t, errs, 1, "expected 1 error for the bad bid type")
+ assert.Contains(t, errs[0].Error(), "unknown bid type")
+}
diff --git a/adapters/adsmartx/adsmartxtest/exemplary/banner_bid_floor_set.json b/adapters/adsmartx/adsmartxtest/exemplary/banner_bid_floor_set.json
new file mode 100644
index 00000000000..f70dd8ec0ba
--- /dev/null
+++ b/adapters/adsmartx/adsmartxtest/exemplary/banner_bid_floor_set.json
@@ -0,0 +1,36 @@
+{
+ "mockBidRequest": {
+ "id": "test-bid-floor-set",
+ "imp": [
+ {
+ "id": "1",
+ "banner": { "w": 300, "h": 250 },
+ "ext": { "bidder": { "bidfloor": 1.23, "sspId": "ssp-123" } }
+ }
+ ]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://ads.adsmartx.com/ads/rtb/prebid/server",
+ "body": {
+ "id": "test-bid-floor-set",
+ "imp": [
+ {
+ "id": "1",
+ "banner": { "w": 300, "h": 250 },
+ "bidfloor": 1.23,
+ "ext": { "bidder": { "bidfloor": 1.23, "sspId": "ssp-123" } }
+ }
+ ]
+ },
+ "impIDs": ["1"]
+ },
+ "mockResponse": {
+ "status": 204,
+ "body": {}
+ }
+ }
+ ],
+ "expectedBidResponses": []
+}
diff --git a/adapters/adsmartx/adsmartxtest/exemplary/imp_banner_and_video.json b/adapters/adsmartx/adsmartxtest/exemplary/imp_banner_and_video.json
new file mode 100644
index 00000000000..4025a048e56
--- /dev/null
+++ b/adapters/adsmartx/adsmartxtest/exemplary/imp_banner_and_video.json
@@ -0,0 +1,108 @@
+{
+ "mockBidRequest": {
+ "id": "mixed-media-request",
+ "imp": [
+ {
+ "id": "banner-imp",
+ "banner": { "w": 300, "h": 250 },
+ "ext": { "bidder": { "bidfloor": 0.5 } }
+ },
+ {
+ "id": "video-imp",
+ "video": { "mimes": ["video/mp4"], "w": 640, "h": 480 },
+ "ext": { "bidder": { "bidfloor": 2.0 } }
+ }
+ ]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://ads.adsmartx.com/ads/rtb/prebid/server",
+ "body": {
+ "id": "mixed-media-request",
+ "imp": [
+ {
+ "id": "banner-imp",
+ "banner": { "w": 300, "h": 250 },
+ "bidfloor": 0.5,
+ "ext": { "bidder": { "bidfloor": 0.5 } }
+ },
+ {
+ "id": "video-imp",
+ "video": { "mimes": ["video/mp4"], "w": 640, "h": 480 },
+ "bidfloor": 2.0,
+ "ext": { "bidder": { "bidfloor": 2.0 } }
+ }
+ ]
+ },
+ "impIDs": ["banner-imp", "video-imp"]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "mixed-media-request",
+ "seatbid": [
+ {
+ "bid": [
+ {
+ "id": "b1",
+ "impid": "banner-imp",
+ "price": 1.0,
+ "adm": "banner
",
+ "crid": "cr1",
+ "w": 300,
+ "h": 250,
+ "mtype": 1
+ },
+ {
+ "id": "b2",
+ "impid": "video-imp",
+ "price": 5.0,
+ "adm": "",
+ "crid": "cr2",
+ "w": 640,
+ "h": 480,
+ "mtype": 2
+ }
+ ]
+ }
+ ],
+ "cur": "USD"
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [
+ {
+ "currency": "USD",
+ "bids": [
+ {
+ "bid": {
+ "id": "b1",
+ "impid": "banner-imp",
+ "price": 1.0,
+ "adm": "banner
",
+ "crid": "cr1",
+ "w": 300,
+ "h": 250,
+ "mtype": 1
+ },
+ "type": "banner"
+ },
+ {
+ "bid": {
+ "id": "b2",
+ "impid": "video-imp",
+ "price": 5.0,
+ "adm": "",
+ "crid": "cr2",
+ "w": 640,
+ "h": 480,
+ "mtype": 2
+ },
+ "type": "video"
+ }
+ ]
+ }
+ ]
+}
diff --git a/adapters/adsmartx/adsmartxtest/exemplary/multiple_seatbids_bids.json b/adapters/adsmartx/adsmartxtest/exemplary/multiple_seatbids_bids.json
new file mode 100644
index 00000000000..036e992d984
--- /dev/null
+++ b/adapters/adsmartx/adsmartxtest/exemplary/multiple_seatbids_bids.json
@@ -0,0 +1,102 @@
+{
+ "mockBidRequest": {
+ "id": "multi-seat-request",
+ "imp": [
+ {
+ "id": "imp-1",
+ "banner": { "w": 300, "h": 250 },
+ "ext": { "bidder": {} }
+ }
+ ]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://ads.adsmartx.com/ads/rtb/prebid/server",
+ "body": {
+ "id": "multi-seat-request",
+ "imp": [
+ {
+ "id": "imp-1",
+ "banner": { "w": 300, "h": 250 },
+ "ext": { "bidder": {} }
+ }
+ ]
+ },
+ "impIDs": ["imp-1"]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "multi-seat-request",
+ "seatbid": [
+ {
+ "seat": "seat-a",
+ "bid": [
+ {
+ "id": "bid-a1",
+ "impid": "imp-1",
+ "price": 1.5,
+ "adm": "Ad A
",
+ "crid": "cr-a1",
+ "w": 300,
+ "h": 250,
+ "mtype": 1
+ }
+ ]
+ },
+ {
+ "seat": "seat-b",
+ "bid": [
+ {
+ "id": "bid-b1",
+ "impid": "imp-1",
+ "price": 2.0,
+ "adm": "Ad B
",
+ "crid": "cr-b1",
+ "w": 300,
+ "h": 250,
+ "mtype": 1
+ }
+ ]
+ }
+ ],
+ "cur": "USD"
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [
+ {
+ "currency": "USD",
+ "bids": [
+ {
+ "bid": {
+ "id": "bid-a1",
+ "impid": "imp-1",
+ "price": 1.5,
+ "adm": "Ad A
",
+ "crid": "cr-a1",
+ "w": 300,
+ "h": 250,
+ "mtype": 1
+ },
+ "type": "banner"
+ },
+ {
+ "bid": {
+ "id": "bid-b1",
+ "impid": "imp-1",
+ "price": 2.0,
+ "adm": "Ad B
",
+ "crid": "cr-b1",
+ "w": 300,
+ "h": 250,
+ "mtype": 1
+ },
+ "type": "banner"
+ }
+ ]
+ }
+ ]
+}
diff --git a/adapters/adsmartx/adsmartxtest/exemplary/simple_banner.json b/adapters/adsmartx/adsmartxtest/exemplary/simple_banner.json
new file mode 100644
index 00000000000..732eb4f240d
--- /dev/null
+++ b/adapters/adsmartx/adsmartxtest/exemplary/simple_banner.json
@@ -0,0 +1,119 @@
+{
+ "mockBidRequest": {
+ "id": "test-request-id",
+ "device": {
+ "ip": "123.123.123.123",
+ "ua": "iPad"
+ },
+ "app": {
+ "id": "1",
+ "bundle": "com.adsmartx.sampleapp"
+ },
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "tagid": "test-banner-slot",
+ "banner": {
+ "format": [
+ { "w": 300, "h": 250 }
+ ],
+ "w": 300,
+ "h": 250
+ },
+ "ext": {
+ "bidder": {
+ "bidfloor": 0.5,
+ "sspId": "ssp-123"
+ }
+ }
+ }
+ ]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://ads.adsmartx.com/ads/rtb/prebid/server",
+ "body": {
+ "id": "test-request-id",
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "tagid": "test-banner-slot",
+ "banner": {
+ "format": [
+ { "w": 300, "h": 250 }
+ ],
+ "w": 300,
+ "h": 250
+ },
+ "bidfloor": 0.5,
+ "ext": {
+ "bidder": {
+ "bidfloor": 0.5,
+ "sspId": "ssp-123"
+ }
+ }
+ }
+ ],
+ "app": {
+ "id": "1",
+ "bundle": "com.adsmartx.sampleapp"
+ },
+ "device": {
+ "ip": "123.123.123.123",
+ "ua": "iPad"
+ }
+ },
+ "impIDs": ["test-imp-id"]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "test-request-id",
+ "seatbid": [
+ {
+ "seat": "adsmartx",
+ "bid": [
+ {
+ "id": "test_bid_id",
+ "impid": "test-imp-id",
+ "price": 0.85,
+ "adm": "Sample Banner Ad
",
+ "crid": "banner-creative-001",
+ "cid": "banner-campaign-abc",
+ "dealid": "deal-123",
+ "w": 300,
+ "h": 250,
+ "mtype": 1
+ }
+ ]
+ }
+ ],
+ "cur": "USD"
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [
+ {
+ "currency": "USD",
+ "bids": [
+ {
+ "bid": {
+ "id": "test_bid_id",
+ "impid": "test-imp-id",
+ "price": 0.85,
+ "adm": "Sample Banner Ad
",
+ "crid": "banner-creative-001",
+ "cid": "banner-campaign-abc",
+ "dealid": "deal-123",
+ "w": 300,
+ "h": 250,
+ "mtype": 1
+ },
+ "type": "banner"
+ }
+ ]
+ }
+ ]
+}
diff --git a/adapters/adsmartx/adsmartxtest/exemplary/simple_video.json b/adapters/adsmartx/adsmartxtest/exemplary/simple_video.json
new file mode 100644
index 00000000000..38795dda174
--- /dev/null
+++ b/adapters/adsmartx/adsmartxtest/exemplary/simple_video.json
@@ -0,0 +1,109 @@
+{
+ "mockBidRequest": {
+ "id": "test-video-request",
+ "device": {
+ "ip": "10.0.0.1",
+ "ua": "Mozilla/5.0"
+ },
+ "site": {
+ "page": "https://example.com"
+ },
+ "imp": [
+ {
+ "id": "video-imp-id",
+ "video": {
+ "mimes": ["video/mp4"],
+ "protocols": [1, 2],
+ "w": 640,
+ "h": 480
+ },
+ "ext": {
+ "bidder": {
+ "bidfloor": 1.0,
+ "siteId": "site-456"
+ }
+ }
+ }
+ ]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://ads.adsmartx.com/ads/rtb/prebid/server",
+ "body": {
+ "id": "test-video-request",
+ "imp": [
+ {
+ "id": "video-imp-id",
+ "video": {
+ "mimes": ["video/mp4"],
+ "protocols": [1, 2],
+ "w": 640,
+ "h": 480
+ },
+ "bidfloor": 1.0,
+ "ext": {
+ "bidder": {
+ "bidfloor": 1.0,
+ "siteId": "site-456"
+ }
+ }
+ }
+ ],
+ "site": {
+ "page": "https://example.com"
+ },
+ "device": {
+ "ip": "10.0.0.1",
+ "ua": "Mozilla/5.0"
+ }
+ },
+ "impIDs": ["video-imp-id"]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "test-video-request",
+ "seatbid": [
+ {
+ "seat": "adsmartx",
+ "bid": [
+ {
+ "id": "video-bid-id",
+ "impid": "video-imp-id",
+ "price": 5.0,
+ "adm": "",
+ "crid": "video-creative-001",
+ "w": 640,
+ "h": 480,
+ "mtype": 2
+ }
+ ]
+ }
+ ],
+ "cur": "USD"
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [
+ {
+ "currency": "USD",
+ "bids": [
+ {
+ "bid": {
+ "id": "video-bid-id",
+ "impid": "video-imp-id",
+ "price": 5.0,
+ "adm": "",
+ "crid": "video-creative-001",
+ "w": 640,
+ "h": 480,
+ "mtype": 2
+ },
+ "type": "video"
+ }
+ ]
+ }
+ ]
+}
diff --git a/adapters/adsmartx/adsmartxtest/exemplary/test_mode_set.json b/adapters/adsmartx/adsmartxtest/exemplary/test_mode_set.json
new file mode 100644
index 00000000000..232f383a42e
--- /dev/null
+++ b/adapters/adsmartx/adsmartxtest/exemplary/test_mode_set.json
@@ -0,0 +1,82 @@
+{
+ "mockBidRequest": {
+ "id": "test-mode-request",
+ "imp": [
+ {
+ "id": "1",
+ "banner": { "w": 728, "h": 90 },
+ "ext": {
+ "bidder": {
+ "testMode": 1
+ }
+ }
+ }
+ ]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://ads.adsmartx.com/ads/rtb/prebid/server",
+ "body": {
+ "id": "test-mode-request",
+ "imp": [
+ {
+ "id": "1",
+ "banner": { "w": 728, "h": 90 },
+ "ext": {
+ "bidder": {
+ "testMode": 1
+ }
+ }
+ }
+ ],
+ "test": 1
+ },
+ "impIDs": ["1"]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "test-mode-request",
+ "seatbid": [
+ {
+ "bid": [
+ {
+ "id": "b1",
+ "impid": "1",
+ "price": 1.0,
+ "adm": "test
",
+ "crid": "cr1",
+ "w": 728,
+ "h": 90,
+ "mtype": 1
+ }
+ ]
+ }
+ ],
+ "cur": "USD"
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [
+ {
+ "currency": "USD",
+ "bids": [
+ {
+ "bid": {
+ "id": "b1",
+ "impid": "1",
+ "price": 1.0,
+ "adm": "test
",
+ "crid": "cr1",
+ "w": 728,
+ "h": 90,
+ "mtype": 1
+ },
+ "type": "banner"
+ }
+ ]
+ }
+ ]
+}
diff --git a/adapters/adsmartx/adsmartxtest/supplemental/bad_media_type.json b/adapters/adsmartx/adsmartxtest/supplemental/bad_media_type.json
new file mode 100644
index 00000000000..f5f29b09bdb
--- /dev/null
+++ b/adapters/adsmartx/adsmartxtest/supplemental/bad_media_type.json
@@ -0,0 +1,89 @@
+{
+ "mockBidRequest": {
+ "id": "bad-mtype-request",
+ "imp": [
+ {
+ "id": "1",
+ "banner": { "w": 300, "h": 250 },
+ "ext": { "bidder": {} }
+ }
+ ]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://ads.adsmartx.com/ads/rtb/prebid/server",
+ "body": {
+ "id": "bad-mtype-request",
+ "imp": [
+ {
+ "id": "1",
+ "banner": { "w": 300, "h": 250 },
+ "ext": { "bidder": {} }
+ }
+ ]
+ },
+ "impIDs": ["1"]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "bad-mtype-request",
+ "seatbid": [
+ {
+ "bid": [
+ {
+ "id": "good-bid",
+ "impid": "1",
+ "price": 1.0,
+ "adm": "good
",
+ "crid": "cr1",
+ "w": 300,
+ "h": 250,
+ "mtype": 1
+ },
+ {
+ "id": "bad-bid",
+ "impid": "1",
+ "price": 2.0,
+ "adm": "bad
",
+ "crid": "cr2",
+ "w": 300,
+ "h": 250,
+ "mtype": 99
+ }
+ ]
+ }
+ ],
+ "cur": "USD"
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [
+ {
+ "currency": "USD",
+ "bids": [
+ {
+ "bid": {
+ "id": "good-bid",
+ "impid": "1",
+ "price": 1.0,
+ "adm": "good
",
+ "crid": "cr1",
+ "w": 300,
+ "h": 250,
+ "mtype": 1
+ },
+ "type": "banner"
+ }
+ ]
+ }
+ ],
+ "expectedMakeBidsErrors": [
+ {
+ "value": "unknown bid type mtype=99",
+ "comparison": "literal"
+ }
+ ]
+}
diff --git a/adapters/adsmartx/adsmartxtest/supplemental/bad_response.json b/adapters/adsmartx/adsmartxtest/supplemental/bad_response.json
new file mode 100644
index 00000000000..d4a208f4868
--- /dev/null
+++ b/adapters/adsmartx/adsmartxtest/supplemental/bad_response.json
@@ -0,0 +1,40 @@
+{
+ "mockBidRequest": {
+ "id": "bad-response-request",
+ "imp": [
+ {
+ "id": "1",
+ "banner": { "w": 300, "h": 250 },
+ "ext": { "bidder": {} }
+ }
+ ]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://ads.adsmartx.com/ads/rtb/prebid/server",
+ "body": {
+ "id": "bad-response-request",
+ "imp": [
+ {
+ "id": "1",
+ "banner": { "w": 300, "h": 250 },
+ "ext": { "bidder": {} }
+ }
+ ]
+ },
+ "impIDs": ["1"]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": "not-json"
+ }
+ }
+ ],
+ "expectedMakeBidsErrors": [
+ {
+ "value": "expect \\{ or n, but found",
+ "comparison": "regex"
+ }
+ ]
+}
diff --git a/adapters/adsmartx/adsmartxtest/supplemental/imp_no_banner_no_video.json b/adapters/adsmartx/adsmartxtest/supplemental/imp_no_banner_no_video.json
new file mode 100644
index 00000000000..df4cd1b8bf2
--- /dev/null
+++ b/adapters/adsmartx/adsmartxtest/supplemental/imp_no_banner_no_video.json
@@ -0,0 +1,21 @@
+{
+ "mockBidRequest": {
+ "id": "no-banner-video",
+ "imp": [
+ {
+ "id": "1",
+ "ext": { "bidder": { "bidfloor": 0.5 } }
+ }
+ ]
+ },
+ "expectedMakeRequestsErrors": [
+ {
+ "value": "impID 1: no banner or video object specified",
+ "comparison": "literal"
+ },
+ {
+ "value": "no valid impressions",
+ "comparison": "literal"
+ }
+ ]
+}
diff --git a/adapters/adsmartx/adsmartxtest/supplemental/invalid_imp_ext.json b/adapters/adsmartx/adsmartxtest/supplemental/invalid_imp_ext.json
new file mode 100644
index 00000000000..53d06f24dc7
--- /dev/null
+++ b/adapters/adsmartx/adsmartxtest/supplemental/invalid_imp_ext.json
@@ -0,0 +1,22 @@
+{
+ "mockBidRequest": {
+ "id": "invalid-ext",
+ "imp": [
+ {
+ "id": "1",
+ "banner": { "w": 300, "h": 250 },
+ "ext": "not-json"
+ }
+ ]
+ },
+ "expectedMakeRequestsErrors": [
+ {
+ "value": "impID 1:",
+ "comparison": "regex"
+ },
+ {
+ "value": "no valid impressions",
+ "comparison": "literal"
+ }
+ ]
+}
diff --git a/adapters/adsmartx/adsmartxtest/supplemental/no_valid_impressions.json b/adapters/adsmartx/adsmartxtest/supplemental/no_valid_impressions.json
new file mode 100644
index 00000000000..3b9d228728f
--- /dev/null
+++ b/adapters/adsmartx/adsmartxtest/supplemental/no_valid_impressions.json
@@ -0,0 +1,12 @@
+{
+ "mockBidRequest": {
+ "id": "no-valid-imps",
+ "imp": []
+ },
+ "expectedMakeRequestsErrors": [
+ {
+ "value": "no valid impressions",
+ "comparison": "literal"
+ }
+ ]
+}
diff --git a/adapters/adsmartx/adsmartxtest/supplemental/status_204.json b/adapters/adsmartx/adsmartxtest/supplemental/status_204.json
new file mode 100644
index 00000000000..cf64b41e026
--- /dev/null
+++ b/adapters/adsmartx/adsmartxtest/supplemental/status_204.json
@@ -0,0 +1,35 @@
+{
+ "mockBidRequest": {
+ "id": "status-204",
+ "imp": [
+ {
+ "id": "1",
+ "banner": { "w": 300, "h": 250 },
+ "ext": { "bidder": {} }
+ }
+ ]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://ads.adsmartx.com/ads/rtb/prebid/server",
+ "body": {
+ "id": "status-204",
+ "imp": [
+ {
+ "id": "1",
+ "banner": { "w": 300, "h": 250 },
+ "ext": { "bidder": {} }
+ }
+ ]
+ },
+ "impIDs": ["1"]
+ },
+ "mockResponse": {
+ "status": 204,
+ "body": {}
+ }
+ }
+ ],
+ "expectedBidResponses": []
+}
diff --git a/adapters/adsmartx/adsmartxtest/supplemental/status_not_200.json b/adapters/adsmartx/adsmartxtest/supplemental/status_not_200.json
new file mode 100644
index 00000000000..41eb8d2715f
--- /dev/null
+++ b/adapters/adsmartx/adsmartxtest/supplemental/status_not_200.json
@@ -0,0 +1,40 @@
+{
+ "mockBidRequest": {
+ "id": "status-500",
+ "imp": [
+ {
+ "id": "1",
+ "banner": { "w": 300, "h": 250 },
+ "ext": { "bidder": {} }
+ }
+ ]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://ads.adsmartx.com/ads/rtb/prebid/server",
+ "body": {
+ "id": "status-500",
+ "imp": [
+ {
+ "id": "1",
+ "banner": { "w": 300, "h": 250 },
+ "ext": { "bidder": {} }
+ }
+ ]
+ },
+ "impIDs": ["1"]
+ },
+ "mockResponse": {
+ "status": 500,
+ "body": {}
+ }
+ }
+ ],
+ "expectedMakeBidsErrors": [
+ {
+ "value": "Unexpected status code: 500",
+ "comparison": "regex"
+ }
+ ]
+}
diff --git a/adapters/adsmartx/params_test.go b/adapters/adsmartx/params_test.go
new file mode 100644
index 00000000000..335c6893286
--- /dev/null
+++ b/adapters/adsmartx/params_test.go
@@ -0,0 +1,59 @@
+package adsmartx
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/prebid/prebid-server/v3/openrtb_ext"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestValidParams(t *testing.T) {
+ validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+ require.NoError(t, err, "Failed to fetch the JSON schema")
+
+ tests := []struct {
+ name string
+ input string
+ }{
+ {"Valid bidfloor only", `{"bidfloor": 0.01}`},
+ {"Valid bidfloor with testMode", `{"bidfloor": 2.5, "testMode": 1}`},
+ {"Valid sspId only", `{"sspId": "ssp-123"}`},
+ {"Valid siteId only", `{"siteId": "site-456"}`},
+ {"Valid all params", `{"bidfloor": 1.0, "testMode": 0, "sspId": "ssp-123", "siteId": "site-456"}`},
+ {"Empty object", `{}`},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assert.NoError(t, validator.Validate(openrtb_ext.BidderAdsmartx, json.RawMessage(tt.input)))
+ })
+ }
+}
+
+func TestInvalidParams(t *testing.T) {
+ validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+ require.NoError(t, err, "Failed to fetch the JSON schema")
+
+ tests := []struct {
+ name string
+ input string
+ }{
+ {"Invalid bidfloor type", `{"bidfloor": "1.2"}`},
+ {"Invalid testMode type", `{"testMode": "yes"}`},
+ {"Negative bidfloor", `{"bidfloor": -5}`},
+ {"Invalid testMode value", `{"testMode": 9999}`},
+ {"Invalid sspId type", `{"sspId": 123}`},
+ {"Invalid siteId type", `{"siteId": 456}`},
+ {"Empty sspId", `{"sspId": ""}`},
+ {"Empty siteId", `{"siteId": ""}`},
+ {"Unknown property", `{"unknownParam": "value"}`},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assert.Error(t, validator.Validate(openrtb_ext.BidderAdsmartx, json.RawMessage(tt.input)))
+ })
+ }
+}
diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go
index 630fbe348b9..2e17f010fbe 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -22,6 +22,7 @@ import (
"github.com/prebid/prebid-server/v4/adapters/adprime"
"github.com/prebid/prebid-server/v4/adapters/adquery"
"github.com/prebid/prebid-server/v4/adapters/adrino"
+ "github.com/prebid/prebid-server/v4/adapters/adsmartx"
"github.com/prebid/prebid-server/v4/adapters/adtarget"
"github.com/prebid/prebid-server/v4/adapters/adtelligent"
"github.com/prebid/prebid-server/v4/adapters/adtonos"
@@ -289,6 +290,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
openrtb_ext.BidderAdprime: adprime.Builder,
openrtb_ext.BidderAdquery: adquery.Builder,
openrtb_ext.BidderAdrino: adrino.Builder,
+ openrtb_ext.BidderAdsmartx: adsmartx.Builder,
openrtb_ext.BidderAdtarget: adtarget.Builder,
openrtb_ext.BidderAdtrgtme: adtrgtme.Builder,
openrtb_ext.BidderAdtelligent: adtelligent.Builder,
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 1b61ff724e2..ce722e5c796 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -38,6 +38,7 @@ var coreBidderNames []BidderName = []BidderName{
BidderAdprime,
BidderAdquery,
BidderAdrino,
+ BidderAdsmartx,
BidderAdtarget,
BidderAdtrgtme,
BidderAdtelligent,
@@ -412,6 +413,7 @@ const (
BidderAdprime BidderName = "adprime"
BidderAdquery BidderName = "adquery"
BidderAdrino BidderName = "adrino"
+ BidderAdsmartx BidderName = "adsmartx"
BidderAdtarget BidderName = "adtarget"
BidderAdtrgtme BidderName = "adtrgtme"
BidderAdTonos BidderName = "adtonos"
diff --git a/openrtb_ext/imp_adsmartx.go b/openrtb_ext/imp_adsmartx.go
new file mode 100644
index 00000000000..64b915a4bcf
--- /dev/null
+++ b/openrtb_ext/imp_adsmartx.go
@@ -0,0 +1,8 @@
+package openrtb_ext
+
+type ImpExtAdsmartx struct {
+ BidFloor float64 `json:"bidfloor,omitempty"`
+ TestMode int `json:"testMode,omitempty"`
+ SspID string `json:"sspId,omitempty"`
+ SspSiteID string `json:"siteId,omitempty"`
+}
diff --git a/static/bidder-info/adsmartx.yaml b/static/bidder-info/adsmartx.yaml
new file mode 100644
index 00000000000..dbd08216831
--- /dev/null
+++ b/static/bidder-info/adsmartx.yaml
@@ -0,0 +1,20 @@
+endpoint: "https://ads.adsmartx.com/ads/rtb/prebid/server"
+endpointCompression: gzip
+maintainer:
+ email: prebid@aidigital.com
+capabilities:
+ app:
+ mediaTypes:
+ - banner
+ - video
+ site:
+ mediaTypes:
+ - banner
+ - video
+userSync:
+ iframe:
+ url: "https://ads.adsmartx.com/sync?ssp_site_id=49964415&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&iframe_enabled=true&redir={{.RedirectURL}}"
+ userMacro: "[UID]"
+ redirect:
+ url: "https://ads.adsmartx.com/sync?ssp_site_id=49964415&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&iframe_enabled=false&redir={{.RedirectURL}}"
+ userMacro: "[UID]"
diff --git a/static/bidder-params/adsmartx.json b/static/bidder-params/adsmartx.json
new file mode 100644
index 00000000000..ae53cd997da
--- /dev/null
+++ b/static/bidder-params/adsmartx.json
@@ -0,0 +1,29 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "title": "Adsmartx Adapter Params",
+ "description": "A schema which validates params accepted by the Adsmartx adapter",
+ "type": "object",
+ "properties": {
+ "bidfloor": {
+ "type": "number",
+ "minimum": 0,
+ "description": "Bid Floor Price"
+ },
+ "testMode": {
+ "type": "integer",
+ "enum": [0, 1],
+ "description": "Test Mode parameter to indicate test request"
+ },
+ "sspId": {
+ "type": "string",
+ "minLength": 1,
+ "description": "SSP ID for the publisher, used for user sync and request routing"
+ },
+ "siteId": {
+ "type": "string",
+ "minLength": 1,
+ "description": "SSP Site ID for the publisher's site, used for user sync and request routing"
+ }
+ },
+ "additionalProperties": false
+}