Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
101 changes: 101 additions & 0 deletions modules/clydoBidAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { deepSetValue, deepAccess, isFn } from '../src/utils.js';
import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js';
import { ortbConverter } from '../libraries/ortbConverter/converter.js';

const BIDDER_CODE = 'clydo';
const METHOD = 'POST';
const DEFAULT_CURRENCY = 'USD';
const params = {
region: "{{region}}",
partnerId: "{{partnerId}}"
}
const BASE_ENDPOINT_URL = `https://${params.region}.clydo.io/${params.partnerId}`

const converter = ortbConverter({
context: {
netRevenue: true,
ttl: 30
},
bidResponse(buildBidResponse, bid, context) {
context.mediaType = deepAccess(bid, 'ext.mediaType');
return buildBidResponse(bid, context)
}
});

export const spec = {
code: BIDDER_CODE,
supportedMediaTypes: [BANNER, VIDEO, NATIVE],
isBidRequestValid: function(bid) {
if (!bid || !bid.params) return false;
const { partnerId, region } = bid.params;
if (typeof partnerId !== 'string' || partnerId.length === 0) return false;
if (typeof region !== 'string') return false;
const allowedRegions = ['us', 'usw', 'eu', 'apac'];
return allowedRegions.includes(region);
},
buildRequests: function(validBidRequests, bidderRequest) {
const data = converter.toORTB({bidRequests: validBidRequests, bidderRequest});
const { partnerId, region } = validBidRequests[0].params;

if (Array.isArray(data.imp)) {
data.imp.forEach((imp, index) => {
const srcBid = validBidRequests[index] || validBidRequests[0];
const bidderParams = deepAccess(srcBid, 'params') || {};
deepSetValue(data, `imp.${index}.ext.clydo`, bidderParams);

const mediaType = imp.banner ? 'banner' : (imp.video ? 'video' : (imp.native ? 'native' : '*'));
let floor = deepAccess(srcBid, 'floor');
if (!floor && isFn(srcBid.getFloor)) {
const floorInfo = srcBid.getFloor({currency: DEFAULT_CURRENCY, mediaType, size: '*'});
if (floorInfo && typeof floorInfo.floor === 'number') {
floor = floorInfo.floor;
}
}

if (typeof floor === 'number') {
deepSetValue(data, `imp.${index}.bidfloor`, floor);
deepSetValue(data, `imp.${index}.bidfloorcur`, DEFAULT_CURRENCY);
}
});
}

const ENDPOINT_URL = BASE_ENDPOINT_URL
.replace(params.partnerId, partnerId)
.replace(params.region, region);

return [{
method: METHOD,
url: ENDPOINT_URL,
data
}]
},
interpretResponse: function(serverResponse, request) {
let bids = [];
let body = serverResponse.body || {};
if (body) {
const normalized = Array.isArray(body.seatbid)
? {
...body,
seatbid: body.seatbid.map(seat => ({
...seat,
bid: (seat.bid || []).map(b => {
if (typeof b?.adm === 'string') {
try {
const parsed = JSON.parse(b.adm);
if (parsed && parsed.native && Array.isArray(parsed.native.assets)) {
return {...b, adm: JSON.stringify(parsed.native)};
}
} catch (e) {}
}
return b;
})
}))
}
: body;
bids = converter.fromORTB({response: normalized, request: request.data}).bids;
}
return bids;
},
}
registerBidder(spec);
93 changes: 93 additions & 0 deletions modules/clydoBidAdapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Overview

```
Module Name: Clydo Bid Adapter
Module Type: Bidder Adapter
Maintainer: [email protected]
```

# Description

The Clydo adapter connects to the Clydo bidding endpoint to request bids using OpenRTB.

- Supported media types: banner, video, native
- Endpoint is derived from parameters: `https://{region}.clydo.io/{partnerId}`
- Passes GDPR, USP/CCPA, and GPP consent when available
- Propagates `schain` and `userIdAsEids`

# Bid Params

- `partnerId` (string, required): Partner identifier provided by Clydo
- `region` (string, required): One of `us`, `usw`, `eu`, `apac`

# Test Parameters (Banner)
```javascript
var adUnits = [{
code: '/15185185/prebid_banner_example_1',
mediaTypes: {
banner: {
sizes: [[300, 250], [300, 600]]
}
},
bids: [{
bidder: 'clydo',
params: {
partnerId: 'abcdefghij',
region: 'us'
}
}]
}];
```

# Test Parameters (Video)
```javascript
var adUnits = [{
code: '/15185185/prebid_video_example_1',
mediaTypes: {
video: {
context: 'instream',
playerSize: [[640, 480]],
mimes: ['video/mp4']
}
},
bids: [{
bidder: 'clydo',
params: {
partnerId: 'abcdefghij',
region: 'us'
}
}]
}];
```

# Test Parameters (Native)
```javascript
var adUnits = [{
code: '/15185185/prebid_native_example_1',
mediaTypes: {
native: {
title: { required: true },
image: { required: true, sizes: [120, 120] },
icon: { required: false, sizes: [50, 50] },
body: { required: false },
sponsoredBy: { required: false },
clickUrl: { required: false },
cta: { required: false }
}
},
bids: [{
bidder: 'clydo',
params: {
partnerId: 'abcdefghij',
region: 'us'
}
}]
}];
```

# Notes

- Floors: If the ad unit implements `getFloor`, the adapter forwards the value as `imp.bidfloor` (USD).
- Consent: When present, the adapter forwards `gdprApplies`/`consentString`, `uspConsent`, and `gpp`/`gpp_sid`.
- Supply Chain and IDs: `schain` is set under `source.ext.schain`; user IDs are forwarded under `user.ext.eids`.

75 changes: 75 additions & 0 deletions test/spec/modules/clydoBidAdapter_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { expect } from 'chai';
import { spec } from 'modules/clydoBidAdapter.js';

function makeBid(overrides = {}) {
return Object.assign({
adUnitCode: '/15185185/prebid_example_1',
bidId: 'bid-1',
ortb2: {},
ortb2Imp: {},
mediaTypes: {
banner: { sizes: [[300, 250]] }
},
bidder: 'clydo',
params: {
partnerId: 'abcdefghij',
region: 'us'
}
}, overrides);
}

describe('clydoBidAdapter', () => {
describe('isBidRequestValid', () => {
it('returns false for missing params', () => {
expect(spec.isBidRequestValid(makeBid({ params: undefined }))).to.equal(false);
});
it('returns false for invalid region', () => {
expect(spec.isBidRequestValid(makeBid({ params: { partnerId: 'x', region: 'xx' } }))).to.equal(false);
});
it('returns true for valid params', () => {
expect(spec.isBidRequestValid(makeBid())).to.equal(true);
});
});

describe('buildRequests', () => {
it('builds POST request with endpoint and JSON content type', () => {
const bid = makeBid();
const reqs = spec.buildRequests([bid], {});
expect(reqs).to.be.an('array').with.lengthOf(1);
const req = reqs[0];
expect(req.method).to.equal('POST');
expect(req.url).to.include('us');
expect(req.url).to.include('abcdefghij');
expect(req).to.not.have.property('options');
expect(req).to.have.property('data');
});

it('adds imp.ext.clydo and bidfloor when available', () => {
const bid = makeBid({
getFloor: ({ currency }) => ({ floor: 1.5, currency })
});
const req = spec.buildRequests([bid], {})[0];
const data = req.data;
expect(data.imp[0].ext.clydo).to.deep.equal(bid.params);
expect(data.imp[0].bidfloor).to.equal(1.5);
expect(data.imp[0].bidfloorcur).to.equal('USD');
});

describe('banner', () => {
it('builds banner imp when mediaTypes.banner present', () => {
const bid = makeBid({ mediaTypes: { banner: { sizes: [[300, 250]] } } });
const data = spec.buildRequests([bid], {})[0].data;
expect(data.imp[0]).to.have.property('banner');
});
});
});

describe('interpretResponse', () => {
it('returns empty when body is null', () => {
const bid = makeBid();
const req = spec.buildRequests([bid], {})[0];
const res = spec.interpretResponse({ body: null }, req);
expect(res).to.be.an('array').that.is.empty;
});
});
});