Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
18b45e0
fresh
Dec 1, 2025
9806d5d
fix ortb and sync server support
Dec 21, 2025
e20d4e9
finish scalibur prebid adapter
Dec 21, 2025
952745d
Merge remote-tracking branch 'upstream/master' into scaliburA
Dec 21, 2025
a87ab09
Add Scalibur bid adapter with Banner, Video, and User Sync support
Dec 21, 2025
c4e7220
add ext debug
Dec 22, 2025
088c807
add gzip compression support
Dec 22, 2025
60a0c74
resolve multi type response
Dec 22, 2025
8ae7336
address PR review feedback and fix bidder implementation
Jan 12, 2026
09d4885
Merge branch 'master' into scaliburA
Jan 12, 2026
98d3567
address PR feedback for Scalibur adapter
Jan 14, 2026
544969c
fix(scalibur): address additional PR feedback
Jan 18, 2026
d8df9f9
Merge branch 'master' into scaliburA
Jan 18, 2026
a85f0bb
Merge branch 'master' into scaliburA
Jan 21, 2026
def8081
Merge branch 'master' into scaliburA
Jan 29, 2026
dea9555
Merge remote-tracking branch 'upstream/master' into scaliburA
Feb 1, 2026
cee6ef0
Merge branch 'master' into scaliburA
Feb 3, 2026
b450a44
merge from master
Feb 4, 2026
e1a43d2
Merge branch 'master' into scaliburA
Feb 8, 2026
794aff1
address pr comments
Feb 8, 2026
e904c76
Merge branch 'master' into scaliburA
bTimor Feb 11, 2026
4c0577f
Merge branch 'master' into scaliburA
bTimor Feb 22, 2026
3329549
Merge branch 'master' into scaliburA
bTimor Feb 25, 2026
385e42b
Merge branch 'master' into scaliburA
bTimor Mar 1, 2026
4aeb4cd
Merge branch 'master' into scaliburA
bTimor Mar 4, 2026
a02eabe
Merge branch 'master' into scaliburA
bTimor Mar 5, 2026
3d37322
merge
Mar 25, 2026
882831c
Merge branch 'scaliburA' of github.com:scalibur-io/prebid-server into…
Mar 25, 2026
80f326d
Merge branch 'master' into scaliburA
bTimor Mar 26, 2026
e1b3d3b
update to v4
Mar 26, 2026
342e19a
Merge branch 'scaliburA' of github.com:scalibur-io/prebid-server into…
Mar 26, 2026
44d7934
Merge branch 'master' into scaliburA
bTimor Apr 5, 2026
ec3eef2
Merge branch 'master' into scaliburA
bTimor Apr 13, 2026
d55fc3e
Merge branch 'master' into scaliburA
bTimor May 10, 2026
6e0c6f0
Merge branch 'master' into scaliburA
May 13, 2026
69e2c7d
fix all last pr comments and merge with master
May 13, 2026
218466e
Merge branch 'scaliburA' of github.com:scalibur-io/prebid-server into…
May 13, 2026
4e79921
remove blank lines from scalibur yaml
May 13, 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
51 changes: 51 additions & 0 deletions adapters/scalibur/params_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package scalibur

import (
"encoding/json"
"testing"

"github.com/prebid/prebid-server/v4/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-schemas. %v", err)
}

for _, validParam := range validParams {
if err := validator.Validate(openrtb_ext.BidderScalibur, json.RawMessage(validParam)); err != nil {
t.Errorf("Schema rejected scalibur params: %s", validParam)
}
}
}

func TestInvalidParams(t *testing.T) {
validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
if err != nil {
t.Fatalf("Failed to fetch the json-schemas. %v", err)
}

for _, invalidParam := range invalidParams {
if err := validator.Validate(openrtb_ext.BidderScalibur, json.RawMessage(invalidParam)); err == nil {
t.Errorf("Schema allowed unexpected params: %s", invalidParam)
}
}
}

var validParams = []string{
`{"placementId":"p123"}`,
`{"placementId":"p123", "bidfloor": 1.5}`,
`{"placementId":"p123", "bidfloor": 1.5, "bidfloorcur": "USD"}`,
}

var invalidParams = []string{
``,
`null`,
`true`,
`5`,
`[]`,
`{}`,
`{"placementId": 123}`,
`{"bidfloor": 1.5}`,
}
320 changes: 320 additions & 0 deletions adapters/scalibur/scalibur.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
package scalibur

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"text/template"

"github.com/prebid/openrtb/v20/adcom1"
"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 *template.Template
}

// Builder builds a new instance of the Scalibur adapter for the given bidder with the given config.
func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
temp, err := template.New("endpointTemplate").Parse(config.Endpoint)
if err != nil {
return nil, fmt.Errorf("unable to parse endpoint url template: %v", err)
}

return &adapter{
endpoint: temp,
}, nil
}

// MakeRequests creates the HTTP requests which should be made to fetch bids from Scalibur.
func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
var errs []error
var validImps []openrtb2.Imp

// Process each impression
for _, imp := range request.Imp {
scaliburExt, err := parseScaliburExt(imp.Ext)
if err != nil {
errs = append(errs, err)
continue
}

impCopy := imp

// Apply bid floor and currency
if scaliburExt.BidFloor != nil && *scaliburExt.BidFloor > 0 {
impCopy.BidFloor = *scaliburExt.BidFloor
if scaliburExt.BidFloorCur != "" {
impCopy.BidFloorCur = scaliburExt.BidFloorCur
}
}

if impCopy.BidFloor > 0 && impCopy.BidFloorCur != "" && impCopy.BidFloorCur != "USD" {
convertedValue, err := reqInfo.ConvertCurrency(impCopy.BidFloor, impCopy.BidFloorCur, "USD")
if err != nil {
errs = append(errs, err)
continue
}
impCopy.BidFloor = convertedValue
impCopy.BidFloorCur = "USD"
}
Comment thread
bTimor marked this conversation as resolved.

if impCopy.BidFloorCur == "" {
impCopy.BidFloorCur = "USD"
Comment thread
bTimor marked this conversation as resolved.
}

// Prepare imp.ext with placementId and params
impExtData := make(map[string]interface{})
impExtData["placementId"] = scaliburExt.PlacementID

if impCopy.BidFloor > 0 {
impExtData["bidfloor"] = impCopy.BidFloor
}
impExtData["bidfloorcur"] = impCopy.BidFloorCur

// Preserve GPID if present
var rawExt map[string]json.RawMessage
if err := jsonutil.Unmarshal(imp.Ext, &rawExt); err == nil {
if gpid, ok := rawExt["gpid"]; ok {
impExtData["gpid"] = json.RawMessage(gpid)
Comment thread
bTimor marked this conversation as resolved.
}
}

impExt, err := jsonutil.Marshal(impExtData)
if err != nil {
errs = append(errs, err)
continue
}
impCopy.Ext = impExt

// Apply video defaults (matching JS defaults)
if impCopy.Video != nil {
videoCopy := *impCopy.Video

// Note: In openrtb v20, field names are capitalized (MIMEs not Mimes)
if len(videoCopy.MIMEs) == 0 {
videoCopy.MIMEs = []string{"video/mp4"}
}
if videoCopy.MinDuration == 0 {
videoCopy.MinDuration = 1
}
if videoCopy.MaxDuration == 0 {
videoCopy.MaxDuration = 180
}
if videoCopy.MaxBitRate == 0 {
videoCopy.MaxBitRate = 30000
}
if len(videoCopy.Protocols) == 0 {
// Use adcom1.MediaCreativeSubtype for protocols in v20
videoCopy.Protocols = []adcom1.MediaCreativeSubtype{2, 3, 5, 6}
}
// Note: In openrtb v20, W and H are pointers
if videoCopy.W == nil || *videoCopy.W == 0 {
w := int64(640)
videoCopy.W = &w
}
if videoCopy.H == nil || *videoCopy.H == 0 {
h := int64(480)
videoCopy.H = &h
}
if videoCopy.Placement == 0 {
videoCopy.Placement = 1
}
if videoCopy.Linearity == 0 {
videoCopy.Linearity = 1
}

impCopy.Video = &videoCopy
}

validImps = append(validImps, impCopy)
}

// If no valid impressions, return errors
if len(validImps) == 0 {
return nil, errs
}

// Create the outgoing request
requestCopy := *request
requestCopy.Imp = validImps
requestCopy.Cur = nil

isDebug := request.Test == 1
if !isDebug && len(request.Ext) > 0 {
var extRequest openrtb_ext.ExtRequest
Comment thread
bTimor marked this conversation as resolved.
if err := jsonutil.Unmarshal(request.Ext, &extRequest); err == nil {
isDebug = extRequest.Prebid.Debug
}
}

if isDebug {
reqExt := openrtb_ext.ExtRequestScalibur{IsDebug: 1}
Comment thread
bTimor marked this conversation as resolved.
if reqExtJSON, err := jsonutil.Marshal(reqExt); err == nil {
requestCopy.Ext = reqExtJSON
}
} else {
requestCopy.Ext = nil
}

reqJSON, err := jsonutil.Marshal(requestCopy)
if err != nil {
return nil, append(errs, err)
}

var endpointBuffer bytes.Buffer
if err := a.endpoint.Execute(&endpointBuffer, nil); 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: endpointBuffer.String(),
Body: reqJSON,
Headers: headers,
ImpIDs: openrtb_ext.GetImpIDs(requestCopy.Imp),
}

return []*adapters.RequestData{requestData}, errs
}

// MakeBids unpacks the server's response into bids.
func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
if response.StatusCode == http.StatusNoContent {
return nil, nil
}

if response.StatusCode != http.StatusOK {
return nil, []error{&errortypes.BadServerResponse{
Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode),
}}
}

var bidResp openrtb2.BidResponse
if err := jsonutil.Unmarshal(response.Body, &bidResp); err != nil {
return nil, []error{err}
}

// Parse the external request to get impression details
var bidReq openrtb2.BidRequest
if err := jsonutil.Unmarshal(externalRequest.Body, &bidReq); err != nil {
return nil, []error{err}
}

// Build impression map for lookup
impMap := make(map[string]*openrtb2.Imp, len(bidReq.Imp))
for i := range bidReq.Imp {
impMap[bidReq.Imp[i].ID] = &bidReq.Imp[i]
}

bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(bidReq.Imp))

// Set currency
if bidResp.Cur != "" {
bidResponse.Currency = bidResp.Cur
} else {
bidResponse.Currency = "USD"
}

// Process each seat bid
for _, seatBid := range bidResp.SeatBid {
for _, bid := range seatBid.Bid {
// Find the corresponding imp
imp, found := impMap[bid.ImpID]
if !found {
return nil, []error{&errortypes.BadServerResponse{
Message: fmt.Sprintf("Invalid bid imp ID %s", bid.ImpID),
}}
}

// Determine bid type based on imp
bidType, err := getBidMediaType(bid, imp)
if err != nil {
return nil, []error{&errortypes.BadServerResponse{
Message: err.Error(),
}}
}

bidCopy := bid

// Handle video VAST
if bidType == openrtb_ext.BidTypeVideo {
// Try to extract custom fields vastXml and vastUrl from bid.ext
var bidExtData struct {
VastXML string `json:"vastXml"`
VastURL string `json:"vastUrl"`
}
if bid.Ext != nil {
if err := jsonutil.Unmarshal(bid.Ext, &bidExtData); err == nil {
if bidExtData.VastXML != "" {
bidCopy.AdM = bidExtData.VastXML
} else if bidExtData.VastURL != "" && bidCopy.AdM == "" {
// Wrap VAST URL in VAST wrapper
bidCopy.AdM = fmt.Sprintf(`<VAST version="3.0"><Ad><Wrapper><VASTAdTagURI><![CDATA[%s]]></VASTAdTagURI></Wrapper></Ad></VAST>`, bidExtData.VastURL)
}
}
}
}

bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
Bid: &bidCopy,
BidType: bidType,
})
}
}

if len(bidResponse.Bids) == 0 {
return nil, nil
}

return bidResponse, nil
}

// parseScaliburExt extracts Scalibur-specific parameters from the impression extension.
func parseScaliburExt(impExt json.RawMessage) (*openrtb_ext.ExtImpScalibur, error) {
var bidderExt adapters.ExtImpBidder
if err := jsonutil.Unmarshal(impExt, &bidderExt); err != nil {
return nil, &errortypes.BadInput{
Comment thread
bTimor marked this conversation as resolved.
Message: fmt.Sprintf("Failed to parse imp.ext: %s", err.Error()),
}
}

var scaliburExt openrtb_ext.ExtImpScalibur
if err := jsonutil.Unmarshal(bidderExt.Bidder, &scaliburExt); err != nil {
return nil, &errortypes.BadInput{
Message: fmt.Sprintf("Failed to parse Scalibur params: %s", err.Error()),
}
}

return &scaliburExt, nil
}

// getBidMediaType determines the media type based on the impression
func getBidMediaType(bid openrtb2.Bid, imp *openrtb2.Imp) (openrtb_ext.BidType, error) {
switch bid.MType {
case openrtb2.MarkupBanner:
Comment thread
bTimor marked this conversation as resolved.
return openrtb_ext.BidTypeBanner, nil
case openrtb2.MarkupVideo:
Comment thread
bTimor marked this conversation as resolved.
return openrtb_ext.BidTypeVideo, nil
}

// Fallback for bidders not supporting mtype (non-multi-format requests)
if imp.Banner != nil && imp.Video == nil {
return openrtb_ext.BidTypeBanner, nil
}
if imp.Video != nil && imp.Banner == nil {
return openrtb_ext.BidTypeVideo, nil
}

return "", fmt.Errorf("unsupported or ambiguous media type for bid id=%s", bid.ID)
}
Loading
Loading