Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
805e7ab
RM-1477 : Added a new adapter for adsmartx
pritishmd-talentica Feb 25, 2026
5cfb1d2
RM-1477 : Copilot review handled
pritishmd-talentica Feb 25, 2026
cfad8a6
Merge pull request #3 from smart-exchange-ai-digital/RM-1477-Prebid-A…
pritishmd-talentica Mar 6, 2026
80c5088
Merge remote-tracking branch 'upstream/master' into RM-1477-Prebid-Ad…
pritishmd-talentica Mar 17, 2026
403fe1c
Merge pull request #4 from smart-exchange-ai-digital/RM-1477-Prebid-A…
pritishmd-talentica Mar 17, 2026
cc36c00
RM-1477 : Fixed version mismatch
pritishmd-talentica May 4, 2026
de04df7
Merge branch 'prebid:master' into master
sushant-dey-talentica Jul 2, 2026
17def18
RM-1891 : Rebranded adsmartx adapter to agenticx
pritishmd-talentica Jul 9, 2026
9085157
Merge branch 'RM-1477-Prebid-Adapter-For-Adsmartx' of https://github.…
pritishmd-talentica Jul 10, 2026
93fd1af
Merge pull request #5 from smart-exchange-ai-digital/RM-1477-Prebid-A…
sushant-dey-talentica Jul 13, 2026
d3477f9
RM-1891:Handled copilot review comments
pritishmd-talentica Jul 15, 2026
94a65d2
RM-1891 : Handled package rename issues
pritishmd-talentica Jul 15, 2026
001ca1f
Merge branch 'master' of https://github.com/smart-exchange-ai-digital…
pritishmd-talentica Jul 15, 2026
034f41a
RM-1891: Fixed test case for audio ads
pritishmd-talentica Jul 15, 2026
cf1c599
Merge pull request #6 from smart-exchange-ai-digital/RM-1891-Rebrandi…
pritishmd-talentica Jul 15, 2026
37f5979
RM-1891 : Code review changes
pritishmd-talentica Aug 7, 2026
8ce2c8e
Merge pull request #7 from smart-exchange-ai-digital/RM-1891-Rebrandi…
sushant-dey-talentica Aug 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions adapters/agenticx/agenticx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
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 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testMode is an imp-level parameter, but setting it causes request.Test = 1 — a request-level flag that affects every impression in the outgoing request. If only one imp out of several has testMode: 1, all imps are sent as a test request.

Is this intentional? If so, please add a comment explaining the design decision. If not, consider an alternative approach (e.g. reject the request entirely, or apply test mode only if all imps agree).

setTestMode = true
}

validImps = append(validImps, imp)
}

if len(validImps) == 0 {
return nil, append(errs, fmt.Errorf("no valid impressions"))
}

// Copy to avoid mutating the shared *request seen by other bidders in this auction.
reqCopy := *request
reqCopy.Imp = validImps
// Intentional: any imp requesting test mode marks the whole outgoing request as test.
if setTestMode {
reqCopy.Test = 1
}

reqJSON, err := jsonutil.Marshal(reqCopy)
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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider this as a suggestion. The current implementation follows an anti-pattern, assumes that if there is a multi-format request, the media type defaults to openrtb_ext.BidTypeAudio. Prebid server expects the media type to be explicitly set in the adapter response. Therefore, we strongly recommend implementing a pattern where the adapter server sets the MType field in the response to accurately determine the media type for the impression.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider this as a suggestion. The current implementation follows an anti-pattern, assumes that if there is a multi-format request, the media type defaults to openrtb_ext.BidTypeBanner. Prebid server expects the media type to be explicitly set in the adapter response. Therefore, we strongly recommend implementing a pattern where the adapter server sets the MType field in the response to accurately determine the media type for the impression.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider this as a suggestion. The current implementation follows an anti-pattern, assumes that if there is a multi-format request, the media type defaults to openrtb_ext.BidTypeVideo. Prebid server expects the media type to be explicitly set in the adapter response. Therefore, we strongly recommend implementing a pattern where the adapter server sets the MType field in the response to accurately determine the media type for the impression.

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),
}
}
}
165 changes: 165 additions & 0 deletions adapters/agenticx/agenticx_test.go
Original file line number Diff line number Diff line change
@@ -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": "<div>ad</div>", "mtype": 1},
{"id": "bad-bid", "impid": "2", "price": 2.0, "adm": "<div>ad2</div>", "mtype": 99},
{"id": "good-bid-2", "impid": "3", "price": 3.0, "adm": "<div>ad3</div>", "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")
}
36 changes: 36 additions & 0 deletions adapters/agenticx/agenticxtest/exemplary/banner_bid_floor_set.json
Original file line number Diff line number Diff line change
@@ -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": []
}
Loading
Loading