diff --git a/adapters/agenticx/agenticx.go b/adapters/agenticx/agenticx.go new file mode 100644 index 00000000000..21414a30986 --- /dev/null +++ b/adapters/agenticx/agenticx.go @@ -0,0 +1,141 @@ +package agenticx + +import ( + "fmt" + "net/http" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v4/adapters" + "github.com/prebid/prebid-server/v4/config" + "github.com/prebid/prebid-server/v4/errortypes" + "github.com/prebid/prebid-server/v4/openrtb_ext" + "github.com/prebid/prebid-server/v4/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 && imp.Audio == nil { + errs = append(errs, fmt.Errorf("impID %s: no banner, video, or audio 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.ImpExtAgenticx, error) { + var bidderExt adapters.ExtImpBidder + if err := jsonutil.Unmarshal(ext, &bidderExt); err != nil { + return openrtb_ext.ImpExtAgenticx{}, err + } + var agenticxExt openrtb_ext.ImpExtAgenticx + if err := jsonutil.Unmarshal(bidderExt.Bidder, &agenticxExt); err != nil { + return openrtb_ext.ImpExtAgenticx{}, err + } + return agenticxExt, 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 + case openrtb2.MarkupAudio: + return openrtb_ext.BidTypeAudio, nil + default: + return "", &errortypes.BadServerResponse{ + Message: fmt.Sprintf("unknown bid type mtype=%d", mtype), + } + } +} diff --git a/adapters/agenticx/agenticx_test.go b/adapters/agenticx/agenticx_test.go new file mode 100644 index 00000000000..95a2887c339 --- /dev/null +++ b/adapters/agenticx/agenticx_test.go @@ -0,0 +1,165 @@ +package agenticx + +import ( + "strings" + "testing" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v4/adapters" + "github.com/prebid/prebid-server/v4/adapters/adapterstest" + "github.com/prebid/prebid-server/v4/config" + "github.com/prebid/prebid-server/v4/openrtb_ext" + "github.com/prebid/prebid-server/v4/util/jsonutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder( + openrtb_ext.BidderAgenticx, + config.Adapter{ + Endpoint: "https://ads.theagenticx.ai/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, "agenticxtest", 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}, + {"Audio", openrtb2.MarkupAudio, false, openrtb_ext.BidTypeAudio}, + {"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 or audio", []openrtb2.Imp{{ID: "1", Ext: jsonutil.RawMessage(`{"bidder":{"bidfloor": 0.5}}`)}}, "no banner, video, or audio 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/agenticx/agenticxtest/exemplary/banner_bid_floor_set.json b/adapters/agenticx/agenticxtest/exemplary/banner_bid_floor_set.json new file mode 100644 index 00000000000..ceff28cf4b5 --- /dev/null +++ b/adapters/agenticx/agenticxtest/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.theagenticx.ai/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/agenticx/agenticxtest/exemplary/imp_banner_and_video.json b/adapters/agenticx/agenticxtest/exemplary/imp_banner_and_video.json new file mode 100644 index 00000000000..44f9181dbd1 --- /dev/null +++ b/adapters/agenticx/agenticxtest/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.theagenticx.ai/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/agenticx/agenticxtest/exemplary/multiple_seatbids_bids.json b/adapters/agenticx/agenticxtest/exemplary/multiple_seatbids_bids.json new file mode 100644 index 00000000000..ecc6567a379 --- /dev/null +++ b/adapters/agenticx/agenticxtest/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.theagenticx.ai/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/agenticx/agenticxtest/exemplary/simple_audio.json b/adapters/agenticx/agenticxtest/exemplary/simple_audio.json new file mode 100644 index 00000000000..2134957575f --- /dev/null +++ b/adapters/agenticx/agenticxtest/exemplary/simple_audio.json @@ -0,0 +1,101 @@ +{ + "mockBidRequest": { + "id": "test-audio-request", + "device": { + "ip": "10.0.0.1", + "ua": "Mozilla/5.0" + }, + "site": { + "page": "https://example.com" + }, + "imp": [ + { + "id": "audio-imp-id", + "audio": { + "mimes": ["audio/mp3"], + "minduration": 5, + "maxduration": 30 + }, + "ext": { + "bidder": { + "bidfloor": 1.0 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ads.theagenticx.ai/ads/rtb/prebid/server", + "body": { + "id": "test-audio-request", + "imp": [ + { + "id": "audio-imp-id", + "audio": { + "mimes": ["audio/mp3"], + "minduration": 5, + "maxduration": 30 + }, + "bidfloor": 1.0, + "ext": { + "bidder": { + "bidfloor": 1.0 + } + } + } + ], + "site": { + "page": "https://example.com" + }, + "device": { + "ip": "10.0.0.1", + "ua": "Mozilla/5.0" + } + }, + "impIDs": ["audio-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-audio-request", + "seatbid": [ + { + "seat": "agenticx", + "bid": [ + { + "id": "audio-bid-id", + "impid": "audio-imp-id", + "price": 2.0, + "adm": "", + "crid": "audio-creative-001", + "mtype": 3 + } + ] + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "audio-bid-id", + "impid": "audio-imp-id", + "price": 2.0, + "adm": "", + "crid": "audio-creative-001", + "mtype": 3 + }, + "type": "audio" + } + ] + } + ] +} diff --git a/adapters/agenticx/agenticxtest/exemplary/simple_banner.json b/adapters/agenticx/agenticxtest/exemplary/simple_banner.json new file mode 100644 index 00000000000..43f9fec9429 --- /dev/null +++ b/adapters/agenticx/agenticxtest/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.agenticx.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.theagenticx.ai/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.agenticx.sampleapp" + }, + "device": { + "ip": "123.123.123.123", + "ua": "iPad" + } + }, + "impIDs": ["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "agenticx", + "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/agenticx/agenticxtest/exemplary/simple_video.json b/adapters/agenticx/agenticxtest/exemplary/simple_video.json new file mode 100644 index 00000000000..7c5101d7222 --- /dev/null +++ b/adapters/agenticx/agenticxtest/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.theagenticx.ai/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": "agenticx", + "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/agenticx/agenticxtest/exemplary/test_mode_set.json b/adapters/agenticx/agenticxtest/exemplary/test_mode_set.json new file mode 100644 index 00000000000..fc8aa233942 --- /dev/null +++ b/adapters/agenticx/agenticxtest/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.theagenticx.ai/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/agenticx/agenticxtest/supplemental/bad_media_type.json b/adapters/agenticx/agenticxtest/supplemental/bad_media_type.json new file mode 100644 index 00000000000..458aa0e8cd5 --- /dev/null +++ b/adapters/agenticx/agenticxtest/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.theagenticx.ai/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/agenticx/agenticxtest/supplemental/bad_response.json b/adapters/agenticx/agenticxtest/supplemental/bad_response.json new file mode 100644 index 00000000000..c48f75fcdab --- /dev/null +++ b/adapters/agenticx/agenticxtest/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.theagenticx.ai/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/agenticx/agenticxtest/supplemental/imp_no_banner_no_video.json b/adapters/agenticx/agenticxtest/supplemental/imp_no_banner_no_video.json new file mode 100644 index 00000000000..5ce66765b29 --- /dev/null +++ b/adapters/agenticx/agenticxtest/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, video, or audio object specified", + "comparison": "literal" + }, + { + "value": "no valid impressions", + "comparison": "literal" + } + ] +} diff --git a/adapters/agenticx/agenticxtest/supplemental/invalid_imp_ext.json b/adapters/agenticx/agenticxtest/supplemental/invalid_imp_ext.json new file mode 100644 index 00000000000..53d06f24dc7 --- /dev/null +++ b/adapters/agenticx/agenticxtest/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/agenticx/agenticxtest/supplemental/no_valid_impressions.json b/adapters/agenticx/agenticxtest/supplemental/no_valid_impressions.json new file mode 100644 index 00000000000..3b9d228728f --- /dev/null +++ b/adapters/agenticx/agenticxtest/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/agenticx/agenticxtest/supplemental/status_204.json b/adapters/agenticx/agenticxtest/supplemental/status_204.json new file mode 100644 index 00000000000..376d1d9f3c8 --- /dev/null +++ b/adapters/agenticx/agenticxtest/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.theagenticx.ai/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/agenticx/agenticxtest/supplemental/status_not_200.json b/adapters/agenticx/agenticxtest/supplemental/status_not_200.json new file mode 100644 index 00000000000..d7f9315ad47 --- /dev/null +++ b/adapters/agenticx/agenticxtest/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.theagenticx.ai/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/agenticx/params_test.go b/adapters/agenticx/params_test.go new file mode 100644 index 00000000000..787a0c0e9c2 --- /dev/null +++ b/adapters/agenticx/params_test.go @@ -0,0 +1,59 @@ +package agenticx + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v4/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.BidderAgenticx, 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.BidderAgenticx, json.RawMessage(tt.input))) + }) + } +} diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index 75a848b7596..74547cef12b 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/agenticx" "github.com/prebid/prebid-server/v4/adapters/adtarget" "github.com/prebid/prebid-server/v4/adapters/adtelligent" "github.com/prebid/prebid-server/v4/adapters/adtonos" @@ -294,6 +295,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.BidderAgenticx: agenticx.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 aa725e7d1bd..4b79bb6e240 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -49,6 +49,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderAdxcg, BidderAdyoulike, BidderAfront, + BidderAgenticx, BidderAidem, BidderAJA, BidderAkcelo, @@ -417,6 +418,7 @@ const ( BidderAdprime BidderName = "adprime" BidderAdquery BidderName = "adquery" BidderAdrino BidderName = "adrino" + BidderAgenticx BidderName = "agenticx" BidderAdtarget BidderName = "adtarget" BidderAdtrgtme BidderName = "adtrgtme" BidderAdTonos BidderName = "adtonos" diff --git a/openrtb_ext/imp_agenticx.go b/openrtb_ext/imp_agenticx.go new file mode 100644 index 00000000000..25e54e32475 --- /dev/null +++ b/openrtb_ext/imp_agenticx.go @@ -0,0 +1,8 @@ +package openrtb_ext + +type ImpExtAgenticx 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/agenticx.yaml b/static/bidder-info/agenticx.yaml new file mode 100644 index 00000000000..72fac9c84ea --- /dev/null +++ b/static/bidder-info/agenticx.yaml @@ -0,0 +1,22 @@ +endpoint: "https://ads.theagenticx.ai/ads/rtb/prebid/server" +endpointCompression: gzip +maintainer: + email: prebid@aidigital.com +capabilities: + app: + mediaTypes: + - banner + - video + - audio + site: + mediaTypes: + - banner + - video + - audio +userSync: + iframe: + url: "https://ads.theagenticx.ai/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.theagenticx.ai/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/agenticx.json b/static/bidder-params/agenticx.json new file mode 100644 index 00000000000..79ab12d0c1d --- /dev/null +++ b/static/bidder-params/agenticx.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Agenticx Adapter Params", + "description": "A schema which validates params accepted by the Agenticx 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 +}