Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
cbd3b73
RM-1476 : Prebid adapter for adsmartx
pritishmd-talentica Jan 13, 2026
2088c35
Merge pull request #2 from prebid/master
sushant-dey-talentica Jan 16, 2026
f184a06
RM-1476 : Improved unit test coverage
pritishmd-talentica Jan 21, 2026
9723093
RM-1476 : Updated bidder documentation description
pritishmd-talentica Jan 22, 2026
9c9b88f
RM-1476 : Review comments handled
pritishmd-talentica Feb 25, 2026
595a234
RM-1476 : Minor changes
pritishmd-talentica Feb 25, 2026
d894733
RM-1476 : Dedupe code for adapter risemediatech
pritishmd-talentica Feb 25, 2026
a80a4eb
Added only ssp_id for user sync flow
pritishmd-talentica Mar 4, 2026
f228d14
Fixed bugs
pritishmd-talentica Mar 4, 2026
0b044a5
RM-1476 : handled all copilot review comments
pritishmd-talentica Mar 6, 2026
a81a61a
RM-1476 : Handled copilot review comments
pritishmd-talentica Mar 6, 2026
c4fe9ea
Merge pull request #4 from smart-exchange-ai-digital/RM-1476-prebid-j…
pritishmd-talentica Mar 6, 2026
81a2480
RM-1476 : Handled prebid js PR review comments
pritishmd-talentica Mar 10, 2026
550551c
Merge remote-tracking branch 'upstream/master' into RM-1476-prebid-js…
pritishmd-talentica Mar 17, 2026
6a92720
Merge branch 'master' into RM-1476-prebid-js-adapter-smart-exchange
pritishmd-talentica Mar 17, 2026
0ed1c9a
Merge pull request #5 from smart-exchange-ai-digital/RM-1476-prebid-j…
pritishmd-talentica Mar 17, 2026
dbdb1f2
Merge pull request #6 from prebid/master
pritishmd-talentica Apr 14, 2026
089ee2a
RM-1476 : Handled review comment to log a warning that risemediatech …
pritishmd-talentica Apr 14, 2026
34fc43a
RM-1476: Added a function to disable the adapter.
pritishmd-talentica Apr 14, 2026
c5cb672
Merge pull request #7 from smart-exchange-ai-digital/RM-1476-prebid-j…
pritishmd-talentica Apr 14, 2026
7844adc
RM-1476 : Updated unit tests
pritishmd-talentica Apr 14, 2026
f3b8946
Merge pull request #8 from smart-exchange-ai-digital/RM-1476-prebid-j…
pritishmd-talentica Apr 14, 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
263 changes: 263 additions & 0 deletions libraries/adsmartxUtils/bidderUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
import { BANNER, VIDEO } from '../../src/mediaTypes.js';
import { ortbConverter } from '../ortbConverter/converter.js';
import { deepAccess, logInfo, logWarn } from '../../src/utils.js';

const DEFAULT_CURRENCY = 'USD';
const DEFAULT_TTL = 60;

/**
* Get publisher user ID with priority:
* 1. Bid params (sspUserId)
* 2. ORTB2 first party data (ortb2.user.id)
* @param {Object} bidParams - Bid parameters from first bid
* @param {Object} bidderRequest - Bidder request object containing ortb2
* @returns {string|null} Publisher user ID if found, null otherwise
*/
export function getPublisherUserId(bidParams, bidderRequest) {
if (bidParams?.sspUserId) {
logInfo('Using SSP user ID from bid params:', bidParams.sspUserId);
return bidParams.sspUserId;
}
const ortb2UserId = deepAccess(bidderRequest, 'ortb2.user.id');
if (ortb2UserId) {
logInfo('Using SSP user ID from ORTB2 user.id:', ortb2UserId);
Comment thread
pritishmd-talentica marked this conversation as resolved.
return ortb2UserId;
}
logInfo('No SSP user ID found in bid params or ORTB2');
return null;
}

/**
* Creates ORTB converter with shared imp/request logic.
* @param {Object} config - { defaultCurrency, defaultTtl }
* @returns {Object} ortbConverter instance
*/
export function createConverter(config = {}) {
const currency = config.defaultCurrency ?? DEFAULT_CURRENCY;
const ttl = config.defaultTtl ?? DEFAULT_TTL;

return ortbConverter({
context: {
netRevenue: true,
ttl,
currency,
},
imp(buildImp, bidRequest, context) {
logInfo('Building impression object for bidRequest:', bidRequest);
const imp = buildImp(bidRequest, context);
const { mediaTypes } = bidRequest;
if (bidRequest.params?.bidfloor) {
logInfo('Setting bid floor for impression:', bidRequest.params.bidfloor);
imp.bidfloor = bidRequest.params.bidfloor;
}
if (mediaTypes[BANNER]) {
logInfo('Adding banner media type to impression:', mediaTypes[BANNER]);
imp.banner = { ...(imp.banner || {}), format: mediaTypes[BANNER].sizes.map(([w, h]) => ({ w, h })) };
} else if (mediaTypes[VIDEO]) {
logInfo('Adding video media type to impression:', mediaTypes[VIDEO]);
imp.video = { ...(imp.video || {}), ...mediaTypes[VIDEO] };
}
return imp;
},
request(buildRequest, imps, bidderRequest, context) {
logInfo('Building server request with impressions:', imps);
const request = buildRequest(imps, bidderRequest, context);
request.cur = [currency];
request.tmax = bidderRequest.timeout;
request.test = bidderRequest.test || 0;

if (Array.isArray(bidderRequest.bids)) {
const hasTestMode = bidderRequest.bids.some(bid => bid.params?.testMode === 1);
if (hasTestMode) {
request.ext = request.ext || {};
request.ext.test = 1;
logInfo('Test mode detected in bid params, setting test flag in request:', request.ext.test);
}
const sspIdBid = bidderRequest.bids.find(bid => bid.params?.sspId);
if (sspIdBid) {
request.ext = request.ext || {};
request.ext.sspId = sspIdBid.params.sspId;
logInfo('sspId detected in bid params, setting sspId in request:', request.ext.sspId);
}
const siteIdBid = bidderRequest.bids.find(bid => bid.params?.siteId);
if (siteIdBid) {
request.ext = request.ext || {};
request.ext.siteId = siteIdBid.params.siteId;
logInfo('siteId detected in bid params, setting siteId in request:', request.ext.siteId);
}
}

if (bidderRequest.gdprConsent || bidderRequest.uspConsent) {
request.regs = request.regs || {};
request.user = request.user || {};
}
if (bidderRequest.gdprConsent) {
logInfo('Adding GDPR consent information to request:', bidderRequest.gdprConsent);
request.regs.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0;
request.user.consent = bidderRequest.gdprConsent.consentString;
Comment thread
pritishmd-talentica marked this conversation as resolved.
Comment thread
pritishmd-talentica marked this conversation as resolved.
}
if (bidderRequest.uspConsent) {
logInfo('Adding USP consent information to request:', bidderRequest.uspConsent);
request.regs.ext = request.regs.ext || {};
request.regs.ext.us_privacy = bidderRequest.uspConsent;
}
return request;
},
});
}

/**
* Validates the bid request (video mimes/sizes, etc.).
* @param {Object} bid - The bid request object.
* @returns {boolean} True if the bid request is valid.
*/
export function isBidRequestValid(bid) {
logInfo('Validating bid request:', bid);
const { mediaTypes } = bid;

if (mediaTypes?.[VIDEO]) {
const video = mediaTypes[VIDEO];
if (!video.mimes || !Array.isArray(video.mimes) || video.mimes.length === 0) {
logWarn('Invalid video bid request: Missing or invalid mimes.');
return false;
}
// w and h are optional; if provided they must be positive
if (video.w != null && video.w <= 0) {
logWarn('Invalid video bid request: Invalid width.');
return false;
}
if (video.h != null && video.h <= 0) {
logWarn('Invalid video bid request: Invalid height.');
return false;
}
}
return true;
}

/**
* Builds buildRequests function that uses the given converter and endpoint.
* @param {Object} config - { converter, endpointUrl }
* @returns {function(Array, Object): Object}
*/
export function createBuildRequests(config) {
const { converter, endpointUrl } = config;

return function buildRequests(validBidRequests, bidderRequest) {
logInfo('Building server request for valid bid requests:', validBidRequests);

const request = converter.toORTB({ bidRequests: validBidRequests, bidderRequest });
logInfo('Converted to ORTB request:', request);
return {
method: 'POST',
url: endpointUrl,
data: request,
options: { endpointCompression: true },
};
};
}

/**
* Interprets the server response and extracts bid information.
* @param {Object} serverResponse - The response from the server.
* @param {Object} request - The original request sent to the server.
* @param {Object} config - { defaultCurrency, defaultTtl }
* @returns {Array} Array of bid objects.
*/
export function interpretResponse(serverResponse, request, config = {}) {
const defaultCurrency = config.defaultCurrency ?? DEFAULT_CURRENCY;
const defaultTtl = config.defaultTtl ?? DEFAULT_TTL;

logInfo('Interpreting server response:', serverResponse);
const bidResp = serverResponse?.body;
if (!bidResp || !Array.isArray(bidResp.seatbid)) {
logWarn('Server response is empty, invalid, or does not contain seatbid array.');
return [];
}

const responses = [];
bidResp.seatbid.forEach(seatbid => {
if (!Array.isArray(seatbid.bid) || seatbid.bid.length === 0) return;
const bid = seatbid.bid[0];
Comment thread
pritishmd-talentica marked this conversation as resolved.
if (!bid.impid || bid.price == null) {
logWarn('Skipping bid with missing impid or price, bidId:', bid.id);
return;
}
logInfo('Processing bid response:', bid);
const bidResponse = {
requestId: bid.impid,
cpm: bid.price,
currency: bidResp.cur || defaultCurrency,
width: bid.w,
height: bid.h,
ad: bid.adm,
creativeId: bid.crid,
netRevenue: true,
ttl: defaultTtl,
meta: { advertiserDomains: bid.adomain || [] },
};
Comment thread
pritishmd-talentica marked this conversation as resolved.

switch (bid.mtype) {
case 1:
bidResponse.mediaType = BANNER;
break;
case 2:
bidResponse.mediaType = VIDEO;
bidResponse.vastXml = bid.adm;
break;
default:
if (bid.mtype != null) {
logWarn('Unknown media type: ', bid.mtype, ' for bidId: ', bid.id);
} else {
logWarn('Bid response does not contain media type for bidId: ', bid.id);
}
bidResponse.mediaType = BANNER;
break;
}

if (bid.dealid) bidResponse.dealId = bid.dealid;
logInfo('Interpreted response:', bidResponse, ' for bidId: ', bid.id);
responses.push(bidResponse);
Comment thread
pritishmd-talentica marked this conversation as resolved.
});

logInfo('Interpreted bid responses:', responses);
return responses;
}

/**
* Creates getUserSyncs function that builds sync URL with privacy params.
* @param {string} syncUrl - Base sync URL (e.g. 'https://sync.adsmartx.com/sync')
* @returns {function(Object, Array, Object, string, Object): Array}
*/
export function createGetUserSyncs(syncUrl) {
return function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) {
logInfo('getUserSyncs called with options:', syncOptions);
if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) {
logWarn('User sync disabled: neither iframe nor pixel is enabled');
return [];
}

const params = [];
if (gdprConsent) {
params.push('gdpr=' + (gdprConsent.gdprApplies ? 1 : 0));
params.push('gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''));
}
if (uspConsent) {
params.push('us_privacy=' + encodeURIComponent(uspConsent));
}
if (gppConsent?.gppString && gppConsent?.applicableSections?.length) {
params.push('gpp=' + encodeURIComponent(gppConsent.gppString));
params.push('gpp_sid=' + encodeURIComponent(gppConsent.applicableSections.join(',')));
}

params.push('ssp_id=630141');
params.push('iframe_enabled=' + (syncOptions.iframeEnabled ? 'true' : 'false'));

const queryString = params.length ? '?' + params.join('&') : '';
const syncs = [{
type: syncOptions.iframeEnabled ? 'iframe' : 'image',
url: syncUrl + queryString,
}];
logInfo('Returning user syncs, type:', syncs[0]?.type);
return syncs;
};
}
43 changes: 43 additions & 0 deletions modules/adsmartxBidAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { BANNER, VIDEO } from '../src/mediaTypes.js';
import {
createConverter,
isBidRequestValid as validateBidRequest,
createBuildRequests,
interpretResponse as interpretResponseUtil,
createGetUserSyncs,
} from '../libraries/adsmartxUtils/bidderUtils.js';

const BIDDER_CODE = 'adsmartx';
const ENDPOINT_URL = 'https://ads.adsmartx.com/ads/rtb/prebid/js';
const SYNC_URL = 'https://sync.adsmartx.com/sync';
const DEFAULT_CURRENCY = 'USD';
const DEFAULT_TTL = 60;

const converter = createConverter({ defaultCurrency: DEFAULT_CURRENCY, defaultTtl: DEFAULT_TTL });

const isBidRequestValid = validateBidRequest;
const buildRequests = createBuildRequests(
{ converter, endpointUrl: ENDPOINT_URL }
);
const getUserSyncs = createGetUserSyncs(SYNC_URL);

const interpretResponse = (serverResponse, request) => {
return interpretResponseUtil(serverResponse, request, {
defaultCurrency: DEFAULT_CURRENCY,
defaultTtl: DEFAULT_TTL,
});
};

export const spec = {
code: BIDDER_CODE,
// TODO: set gvlid once confirmed with AI Digital / AdSmartX team
gvlid: undefined,
Comment thread
pritishmd-talentica marked this conversation as resolved.
supportedMediaTypes: [BANNER, VIDEO],
isBidRequestValid,
buildRequests,
interpretResponse,
getUserSyncs,
};

registerBidder(spec);
72 changes: 72 additions & 0 deletions modules/adsmartxBidAdapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Overview

Module Name : AdSmartX Bidder Adapter
Module Type : Bid Adapter
Maintainer : prebid@aidigital.com

# Description
Connects to AdSmartX Exchange for bids
AdSmartX supports Display & Video(Instream) currently.

This adapter is maintained by Smart Exchange, the legal entity behind this implementation. Our official domain is [AI Digital](https://www.aidigital.com/).
Comment thread
pritishmd-talentica marked this conversation as resolved.
# Sample Ad Unit : Banner
```
var adUnits = [
{
code: 'test-banner-div',
mediaTypes: {
banner: {
sizes:[
[320,50]
]
}
},
bids:[
{
bidder: 'adsmartx',
params: {
bidfloor: 0.001,
testMode: 1,
sspId: 123456,
siteId: 987654,
sspUserId: 'u1234'
}
}
]
}
]
```

# Sample Ad Unit : Video
```
var videoAdUnit = [
{
code: 'adsmartx',
mediaTypes: {
video: {
playerSize: [640, 480], // required
context: 'instream',
mimes: ['video/mp4','video/webm'],
minduration: 5,
maxduration: 30,
startdelay: 30,
maxseq: 2,
poddur: 30,
protocols: [1,3,4],
}
},
bids:[
{
bidder: 'adsmartx',
params: {
bidfloor: 0.001,
testMode: 1,
sspId: 123456,
siteId: 987654,
sspUserId: 'u1234'
}
}
]
}
]
```
Loading
Loading