diff --git a/adapters/bidmatic/bidmatic.go b/adapters/bidmatic/bidmatic.go new file mode 100644 index 00000000000..950107ea0e0 --- /dev/null +++ b/adapters/bidmatic/bidmatic.go @@ -0,0 +1,206 @@ +package bidmatic + +import ( + "encoding/json" + "fmt" + "net/http" + + "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/openrtb_ext" +) + +type adapter struct { + endpoint string +} + +type bidmaticImpExt struct { + Bidmatic openrtb_ext.ExtImpBidmatic `json:"bidmatic"` +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + totalImps := len(request.Imp) + errors := make([]error, 0, totalImps) + imp2source := make(map[int][]int) + + for i := 0; i < totalImps; i++ { + sourceId, err := validateImpression(&request.Imp[i]) + if err != nil { + errors = append(errors, err) + continue + } + + if _, ok := imp2source[sourceId]; !ok { + imp2source[sourceId] = make([]int, 0, totalImps-i) + } + + imp2source[sourceId] = append(imp2source[sourceId], i) + } + + totalReqs := len(imp2source) + if totalReqs == 0 { + return nil, errors + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + reqs := make([]*adapters.RequestData, 0, totalReqs) + + imps := request.Imp + request.Imp = make([]openrtb2.Imp, 0, len(imps)) + for sourceId, impIds := range imp2source { + request.Imp = request.Imp[:0] + + for i := 0; i < len(impIds); i++ { + request.Imp = append(request.Imp, imps[impIds[i]]) + } + + body, err := json.Marshal(request) + if err != nil { + errors = append(errors, fmt.Errorf("error while encoding bidRequest, err: %s", err)) + return nil, errors + } + + reqs = append(reqs, &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint + fmt.Sprintf("?source=%d", sourceId), + Body: body, + Headers: headers, + ImpIDs: openrtb_ext.GetImpIDs(request.Imp), + }) + } + + if len(reqs) == 0 { + return nil, errors + } + + return reqs, errors +} + +func (a *adapter) MakeBids(bidReq *openrtb2.BidRequest, unused *adapters.RequestData, httpRes *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(httpRes) { + return nil, nil + } + if err := adapters.CheckResponseStatusCodeForErrors(httpRes); err != nil { + return nil, []error{err} + } + + var bidResp openrtb2.BidResponse + if err := json.Unmarshal(httpRes.Body, &bidResp); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: fmt.Sprintf("error while decoding response, err: %s", err), + }} + } + + bidResponse := adapters.NewBidderResponse() + var errors []error + + var impOK bool + for _, sb := range bidResp.SeatBid { + for i := 0; i < len(sb.Bid); i++ { + + bid := sb.Bid[i] + + impOK = false + mediaType := openrtb_ext.BidTypeBanner + bid.MType = openrtb2.MarkupBanner + loop: + for _, imp := range bidReq.Imp { + if imp.ID == bid.ImpID { + + impOK = true + + switch { + case imp.Video != nil: + mediaType = openrtb_ext.BidTypeVideo + bid.MType = openrtb2.MarkupVideo + break loop + case imp.Banner != nil: + mediaType = openrtb_ext.BidTypeBanner + bid.MType = openrtb2.MarkupBanner + break loop + case imp.Audio != nil: + mediaType = openrtb_ext.BidTypeAudio + bid.MType = openrtb2.MarkupAudio + break loop + case imp.Native != nil: + mediaType = openrtb_ext.BidTypeNative + bid.MType = openrtb2.MarkupNative + break loop + } + } + } + + if !impOK { + errors = append(errors, &errortypes.BadServerResponse{ + Message: fmt.Sprintf("ignoring bid id=%s, request doesn't contain any impression with id=%s", bid.ID, bid.ImpID), + }) + continue + } + + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &bid, + BidType: mediaType, + }) + } + } + + return bidResponse, errors +} + +func validateImpression(imp *openrtb2.Imp) (int, error) { + if len(imp.Ext) == 0 { + return 0, &errortypes.BadInput{ + Message: fmt.Sprintf("ignoring imp id=%s, extImpBidder is empty", imp.ID), + } + } + + var bidderExt adapters.ExtImpBidder + + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return 0, &errortypes.BadInput{ + Message: fmt.Sprintf("ignoring imp id=%s, error while decoding extImpBidder, err: %s", imp.ID, err), + } + } + + impExt := openrtb_ext.ExtImpBidmatic{} + err := json.Unmarshal(bidderExt.Bidder, &impExt) + if err != nil { + return 0, &errortypes.BadInput{ + Message: fmt.Sprintf("ignoring imp id=%s, error while decoding impExt, err: %s", imp.ID, err), + } + } + + // common extension for all impressions + var impExtBuffer []byte + + impExtBuffer, err = json.Marshal(&bidmaticImpExt{ + Bidmatic: impExt, + }) + if err != nil { + return 0, &errortypes.BadInput{ + Message: fmt.Sprintf("ignoring imp id=%s, error while marshaling impExt, err: %s", imp.ID, err), + } + } + + if impExt.BidFloor > 0 { + imp.BidFloor = impExt.BidFloor + } + + imp.Ext = impExtBuffer + + source, err := impExt.SourceId.Int64() // json.Unmarshal returns err if it isn't valid + if err != nil { + return 0, err + } + return int(source), nil +} + +// Builder builds a new instance of the bidmatic adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + return &adapter{endpoint: config.Endpoint}, nil +} diff --git a/adapters/bidmatic/bidmatic_test.go b/adapters/bidmatic/bidmatic_test.go new file mode 100644 index 00000000000..c6a31823223 --- /dev/null +++ b/adapters/bidmatic/bidmatic_test.go @@ -0,0 +1,23 @@ +package bidmatic + +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" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder( + openrtb_ext.BidderBidmatic, + config.Adapter{Endpoint: "http://adapter.bidmatic.io/pbs/ortb"}, + config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}, + ) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "bidmatictest", bidder) +} diff --git a/adapters/bidmatic/bidmatictest/exemplary/media-type-mapping.json b/adapters/bidmatic/bidmatictest/exemplary/media-type-mapping.json new file mode 100644 index 00000000000..57f1215af43 --- /dev/null +++ b/adapters/bidmatic/bidmatictest/exemplary/media-type-mapping.json @@ -0,0 +1,91 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidder": { + "source": 1000 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://adapter.bidmatic.io/pbs/ortb?source=1000", + "body": { + "id": "test-request-id", + "imp": [ + { + "id":"test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidmatic": { + "source": 1000 + } + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "test-imp-id", + "mtype": 2, + "price": 3.5, + "w": 900, + "h": 250 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id", + "mtype": 2, + "price": 3.5, + "w": 900, + "h": 250 + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/bidmatic/bidmatictest/exemplary/simple-banner.json b/adapters/bidmatic/bidmatictest/exemplary/simple-banner.json new file mode 100644 index 00000000000..d3c41278231 --- /dev/null +++ b/adapters/bidmatic/bidmatictest/exemplary/simple-banner.json @@ -0,0 +1,98 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "source": 1000, + "siteId": 1234, + "bidFloor": 20 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://adapter.bidmatic.io/pbs/ortb?source=1000", + "body": { + "id": "test-request-id", + "imp": [ + { + "id":"test-imp-id", + "banner": { + "format": [ + {"w":300,"h":250}, + {"w":300,"h":600} + ] + }, + "bidfloor": 20, + "ext": { + "bidmatic": { + "source": 1000, + "siteId": 1234, + "bidFloor": 20 + } + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "test-imp-id", + "mtype": 2, + "price": 3.5, + "w": 900, + "h": 250 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id", + "mtype": 1, + "price": 3.5, + "w": 900, + "h": 250 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/bidmatic/bidmatictest/exemplary/simple-video.json b/adapters/bidmatic/bidmatictest/exemplary/simple-video.json new file mode 100644 index 00000000000..a9bcb6a141e --- /dev/null +++ b/adapters/bidmatic/bidmatictest/exemplary/simple-video.json @@ -0,0 +1,57 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidder": { + "source": 1000 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://adapter.bidmatic.io/pbs/ortb?source=1000", + "body": { + "id": "test-request-id", + "imp": [ + { + "id":"test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidmatic": { + "source": 1000 + } + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 204 + } + } + ], + "expectedBidResponses": [] +} \ No newline at end of file diff --git a/adapters/bidmatic/bidmatictest/supplemental/explicit-dimensions.json b/adapters/bidmatic/bidmatictest/supplemental/explicit-dimensions.json new file mode 100644 index 00000000000..b1f2f6ea510 --- /dev/null +++ b/adapters/bidmatic/bidmatictest/supplemental/explicit-dimensions.json @@ -0,0 +1,60 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 100, + "h": 400 + }, + "ext": { + "bidder": { + "source": 1000 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://adapter.bidmatic.io/pbs/ortb?source=1000", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 100, + "h": 400 + }, + "ext": { + "bidmatic": { + "source": 1000 + } + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 204 + } + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/bidmatic/bidmatictest/supplemental/imp-ext-empty.json b/adapters/bidmatic/bidmatictest/supplemental/imp-ext-empty.json new file mode 100644 index 00000000000..0607da05fb9 --- /dev/null +++ b/adapters/bidmatic/bidmatictest/supplemental/imp-ext-empty.json @@ -0,0 +1,21 @@ +{ + "mockBidRequest": { + "id": "unsupported-native-request", + "imp": [ + { + "id": "unsupported-native-imp", + "video": { + "w": 100, + "h": 200 + } + } + ] + }, + + "expectedMakeRequestsErrors": [ + { + "value": "ignoring imp id=unsupported-native-imp, extImpBidder is empty", + "comparison": "literal" + } + ] +} diff --git a/adapters/bidmatic/bidmatictest/supplemental/wrong-impression-ext.json b/adapters/bidmatic/bidmatictest/supplemental/wrong-impression-ext.json new file mode 100644 index 00000000000..8154afed75f --- /dev/null +++ b/adapters/bidmatic/bidmatictest/supplemental/wrong-impression-ext.json @@ -0,0 +1,26 @@ +{ + "mockBidRequest": { + "id": "unsupported-native-request", + "imp": [ + { + "id": "unsupported-native-imp", + "video": { + "w": 100, + "h": 200 + }, + "ext": { + "bidder": { + "source": "some string instead of int" + } + } + } + ] + }, + + "expectedMakeRequestsErrors": [ + { + "value": "ignoring imp id=unsupported-native-imp, error while decoding impExt, err: json: invalid number literal, trying to unmarshal \"\\\"some string instead of int\\\"\" into Number", + "comparison": "literal" + } + ] +} diff --git a/adapters/bidmatic/bidmatictest/supplemental/wrong-impression-mapping.json b/adapters/bidmatic/bidmatictest/supplemental/wrong-impression-mapping.json new file mode 100644 index 00000000000..05679082aa3 --- /dev/null +++ b/adapters/bidmatic/bidmatictest/supplemental/wrong-impression-mapping.json @@ -0,0 +1,79 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidder": { + "source": 1000 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://adapter.bidmatic.io/pbs/ortb?source=1000", + "body": { + "id": "test-request-id", + "imp": [ + { + "id":"test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/x-flv", + "video/mp4" + ] + }, + "ext": { + "bidmatic": { + "source": 1000 + } + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "SOME-WRONG-IMP-ID", + "price": 3.5, + "w": 900, + "h": 250 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [{"currency":"USD","bids":[]}], + "expectedMakeBidsErrors": [ + { + "value": "ignoring bid id=test-bid-id, request doesn't contain any impression with id=SOME-WRONG-IMP-ID", + "comparison": "literal" + } + ] +} diff --git a/adapters/bidmatic/bidmatictest/supplemental/wrong-response.json b/adapters/bidmatic/bidmatictest/supplemental/wrong-response.json new file mode 100644 index 00000000000..ad09b32cd1a --- /dev/null +++ b/adapters/bidmatic/bidmatictest/supplemental/wrong-response.json @@ -0,0 +1,65 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 100, + "h": 400 + }, + "ext": { + "bidder": { + "source": 1000 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://adapter.bidmatic.io/pbs/ortb?source=1000", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 100, + "h": 400 + }, + "ext": { + "bidmatic": { + "source": 1000 + } + } + } + ] + }, + "impIDs":["test-imp-id"] + }, + "mockResponse": { + "status": 200 + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "error while decoding response, err: unexpected end of JSON input", + "comparison": "literal" + } + ] +} diff --git a/adapters/bidmatic/params_test.go b/adapters/bidmatic/params_test.go new file mode 100644 index 00000000000..6bdc5f4339d --- /dev/null +++ b/adapters/bidmatic/params_test.go @@ -0,0 +1,64 @@ +package bidmatic + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +// This file actually intends to test static/bidder-params/bidmatic.json +// These also validate the format of the external API: request.imp[i].ext.prebid.bidder.bidmatic +// TestValidParams makes sure that the bidmatic schema accepts all imp.ext fields which we intend to support. + +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.BidderBidmatic, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected bidmatic params: %s", validParam) + } + } +} + +// TestInvalidParams makes sure that the bidmatic schema rejects all the imp.ext fields we don't support. +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.BidderBidmatic, json.RawMessage(invalidParam)); err == nil { + ext := openrtb_ext.ExtImpBidmatic{} + err = json.Unmarshal([]byte(invalidParam), &ext) + if err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } + } +} + +var validParams = []string{ + `{"source":123}`, + `{"source":"123"}`, + `{"source":123,"placementId":1234}`, + `{"source":123,"siteId":4321}`, + `{"source":"123","siteId":0,"bidFloor":0}`, +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `4.2`, + `[]`, + `{}`, + `{"source":"qwerty"}`, + `{"source":"123","placementId":"123"}`, + `{"source":123, "placementId":"123", "siteId":"321"}`, +} diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index b05c048ec7c..88948460aff 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -52,6 +52,7 @@ import ( "github.com/prebid/prebid-server/v2/adapters/between" "github.com/prebid/prebid-server/v2/adapters/beyondmedia" "github.com/prebid/prebid-server/v2/adapters/bidmachine" + "github.com/prebid/prebid-server/v2/adapters/bidmatic" "github.com/prebid/prebid-server/v2/adapters/bidmyadz" "github.com/prebid/prebid-server/v2/adapters/bidscube" "github.com/prebid/prebid-server/v2/adapters/bidstack" @@ -279,6 +280,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderBetween: between.Builder, openrtb_ext.BidderBeyondMedia: beyondmedia.Builder, openrtb_ext.BidderBidmachine: bidmachine.Builder, + openrtb_ext.BidderBidmatic: bidmatic.Builder, openrtb_ext.BidderBidmyadz: bidmyadz.Builder, openrtb_ext.BidderBidsCube: bidscube.Builder, openrtb_ext.BidderBidstack: bidstack.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 0570d8a468d..522e2490da1 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -68,6 +68,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderBetween, BidderBeyondMedia, BidderBidmachine, + BidderBidmatic, BidderBidmyadz, BidderBidsCube, BidderBidstack, @@ -395,6 +396,7 @@ const ( BidderBetween BidderName = "between" BidderBeyondMedia BidderName = "beyondmedia" BidderBidmachine BidderName = "bidmachine" + BidderBidmatic BidderName = "bidmatic" BidderBidmyadz BidderName = "bidmyadz" BidderBidsCube BidderName = "bidscube" BidderBidstack BidderName = "bidstack" diff --git a/openrtb_ext/imp_bidmatic.go b/openrtb_ext/imp_bidmatic.go new file mode 100644 index 00000000000..935c977e7ac --- /dev/null +++ b/openrtb_ext/imp_bidmatic.go @@ -0,0 +1,11 @@ +package openrtb_ext + +import "encoding/json" + +// ExtImpBidmatic defines the contract for bidrequest.imp[i].ext.prebid.bidder.bidmatic +type ExtImpBidmatic struct { + SourceId json.Number `json:"source"` + PlacementId int `json:"placementId,omitempty"` + SiteId int `json:"siteId,omitempty"` + BidFloor float64 `json:"bidFloor,omitempty"` +} diff --git a/static/bidder-info/bidmatic.yaml b/static/bidder-info/bidmatic.yaml new file mode 100644 index 00000000000..19211190033 --- /dev/null +++ b/static/bidder-info/bidmatic.yaml @@ -0,0 +1,18 @@ +endpoint: "http://adapter.bidmatic.io/pbs/ortb" +maintainer: + email: "advertising@bidmatic.io" +gvlVendorID: 1134 +capabilities: + app: + mediaTypes: + - banner + - video + site: + mediaTypes: + - banner + - video +userSync: + # bidmatic supports user syncing, but requires configuration by the host. contact this + # bidder directly at the email address in this file to ask about enabling user sync. + supports: + - iframe diff --git a/static/bidder-params/bidmatic.json b/static/bidder-params/bidmatic.json new file mode 100644 index 00000000000..b3002a55ac5 --- /dev/null +++ b/static/bidder-params/bidmatic.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Bidmatic Adapter Params", + "description": "A schema which validates params accepted by the Bidmatic adapter", + + "type": "object", + "properties": { + "placementId": { + "type": "integer", + "description": "An ID which identifies this placement of the impression" + }, + "siteId": { + "type": "integer", + "description": "An ID which identifies the site selling the impression" + }, + "source": { + "type": [ + "integer", + "string" + ], + "description": "An ID which identifies the channel" + }, + "bidFloor": { + "type": "number", + "description": "BidFloor, US Dollars" + } + }, + "required": ["source"] +}