|
| 1 | +/** |
| 2 | + * @module panxoBidAdapter |
| 3 | + * @description Panxo Bid Adapter for Prebid.js - AI-referred traffic monetization |
| 4 | + * @requires Panxo Signal script (cdn.panxo-sys.com) loaded before Prebid |
| 5 | + */ |
| 6 | + |
| 7 | +import { registerBidder } from '../src/adapters/bidderFactory.js'; |
| 8 | +import { BANNER } from '../src/mediaTypes.js'; |
| 9 | +import { deepAccess, logWarn, isFn, isPlainObject } from '../src/utils.js'; |
| 10 | +import { getStorageManager } from '../src/storageManager.js'; |
| 11 | + |
| 12 | +const BIDDER_CODE = 'panxo'; |
| 13 | +const ENDPOINT_URL = 'https://panxo-sys.com/openrtb/2.5/bid'; |
| 14 | +const USER_ID_KEY = 'panxo_uid'; |
| 15 | +const SYNC_URL = 'https://panxo-sys.com/usersync'; |
| 16 | +const DEFAULT_CURRENCY = 'USD'; |
| 17 | +const TTL = 300; |
| 18 | +const NET_REVENUE = true; |
| 19 | + |
| 20 | +export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); |
| 21 | + |
| 22 | +export function getPanxoUserId() { |
| 23 | + try { |
| 24 | + return storage.getDataFromLocalStorage(USER_ID_KEY); |
| 25 | + } catch (e) { |
| 26 | + // storageManager handles errors internally |
| 27 | + } |
| 28 | + return null; |
| 29 | +} |
| 30 | + |
| 31 | +function buildBanner(bid) { |
| 32 | + const sizes = deepAccess(bid, 'mediaTypes.banner.sizes') || bid.sizes || []; |
| 33 | + if (sizes.length === 0) return null; |
| 34 | + |
| 35 | + return { |
| 36 | + format: sizes.map(size => ({ w: size[0], h: size[1] })), |
| 37 | + w: sizes[0][0], |
| 38 | + h: sizes[0][1] |
| 39 | + }; |
| 40 | +} |
| 41 | + |
| 42 | +function getFloorPrice(bid, size) { |
| 43 | + if (isFn(bid.getFloor)) { |
| 44 | + try { |
| 45 | + const floorInfo = bid.getFloor({ |
| 46 | + currency: DEFAULT_CURRENCY, |
| 47 | + mediaType: BANNER, |
| 48 | + size: size |
| 49 | + }); |
| 50 | + if (floorInfo && floorInfo.floor) { |
| 51 | + return floorInfo.floor; |
| 52 | + } |
| 53 | + } catch (e) { |
| 54 | + // Floor module error |
| 55 | + } |
| 56 | + } |
| 57 | + return deepAccess(bid, 'params.floor') || 0; |
| 58 | +} |
| 59 | + |
| 60 | +function buildUser(panxoUid, bidderRequest) { |
| 61 | + const user = { buyeruid: panxoUid }; |
| 62 | + |
| 63 | + // GDPR consent |
| 64 | + const gdprConsent = deepAccess(bidderRequest, 'gdprConsent'); |
| 65 | + if (gdprConsent && gdprConsent.consentString) { |
| 66 | + user.ext = { consent: gdprConsent.consentString }; |
| 67 | + } |
| 68 | + |
| 69 | + // First Party Data - user |
| 70 | + const fpd = deepAccess(bidderRequest, 'ortb2.user'); |
| 71 | + if (isPlainObject(fpd)) { |
| 72 | + user.ext = { ...user.ext, ...fpd.ext }; |
| 73 | + if (fpd.data) user.data = fpd.data; |
| 74 | + } |
| 75 | + |
| 76 | + return user; |
| 77 | +} |
| 78 | + |
| 79 | +function buildRegs(bidderRequest) { |
| 80 | + const regs = { ext: {} }; |
| 81 | + |
| 82 | + // GDPR |
| 83 | + const gdprConsent = deepAccess(bidderRequest, 'gdprConsent'); |
| 84 | + if (gdprConsent) { |
| 85 | + regs.ext.gdpr = gdprConsent.gdprApplies ? 1 : 0; |
| 86 | + } |
| 87 | + |
| 88 | + // CCPA / US Privacy |
| 89 | + const uspConsent = deepAccess(bidderRequest, 'uspConsent'); |
| 90 | + if (uspConsent) { |
| 91 | + regs.ext.us_privacy = uspConsent; |
| 92 | + } |
| 93 | + |
| 94 | + // GPP |
| 95 | + const gppConsent = deepAccess(bidderRequest, 'gppConsent'); |
| 96 | + if (gppConsent) { |
| 97 | + regs.ext.gpp = gppConsent.gppString; |
| 98 | + regs.ext.gpp_sid = gppConsent.applicableSections; |
| 99 | + } |
| 100 | + |
| 101 | + // COPPA |
| 102 | + const coppa = deepAccess(bidderRequest, 'ortb2.regs.coppa'); |
| 103 | + if (coppa) { |
| 104 | + regs.coppa = 1; |
| 105 | + } |
| 106 | + |
| 107 | + return regs; |
| 108 | +} |
| 109 | + |
| 110 | +function buildDevice() { |
| 111 | + const device = { |
| 112 | + ua: navigator.userAgent, |
| 113 | + language: navigator.language, |
| 114 | + js: 1, |
| 115 | + dnt: navigator.doNotTrack === '1' ? 1 : 0 |
| 116 | + }; |
| 117 | + |
| 118 | + if (typeof screen !== 'undefined') { |
| 119 | + device.w = screen.width; |
| 120 | + device.h = screen.height; |
| 121 | + } |
| 122 | + |
| 123 | + return device; |
| 124 | +} |
| 125 | + |
| 126 | +function buildSite(bidderRequest) { |
| 127 | + const site = { |
| 128 | + page: deepAccess(bidderRequest, 'refererInfo.page') || '', |
| 129 | + domain: deepAccess(bidderRequest, 'refererInfo.domain') || '', |
| 130 | + ref: deepAccess(bidderRequest, 'refererInfo.ref') || '' |
| 131 | + }; |
| 132 | + |
| 133 | + // First Party Data - site |
| 134 | + const fpd = deepAccess(bidderRequest, 'ortb2.site'); |
| 135 | + if (isPlainObject(fpd)) { |
| 136 | + Object.assign(site, { |
| 137 | + name: fpd.name, |
| 138 | + cat: fpd.cat, |
| 139 | + sectioncat: fpd.sectioncat, |
| 140 | + pagecat: fpd.pagecat, |
| 141 | + content: fpd.content |
| 142 | + }); |
| 143 | + if (fpd.ext) site.ext = fpd.ext; |
| 144 | + } |
| 145 | + |
| 146 | + return site; |
| 147 | +} |
| 148 | + |
| 149 | +function buildSource(bidderRequest) { |
| 150 | + const source = { |
| 151 | + tid: deepAccess(bidderRequest, 'ortb2.source.tid') || bidderRequest.auctionId |
| 152 | + }; |
| 153 | + |
| 154 | + // Supply Chain (schain) - read from ortb2 where Prebid normalizes it |
| 155 | + const schain = deepAccess(bidderRequest, 'ortb2.source.ext.schain'); |
| 156 | + if (isPlainObject(schain)) { |
| 157 | + source.ext = { schain: schain }; |
| 158 | + } |
| 159 | + |
| 160 | + return source; |
| 161 | +} |
| 162 | + |
| 163 | +export const spec = { |
| 164 | + code: BIDDER_CODE, |
| 165 | + gvlid: 1527, |
| 166 | + supportedMediaTypes: [BANNER], |
| 167 | + |
| 168 | + isBidRequestValid(bid) { |
| 169 | + const propertyKey = deepAccess(bid, 'params.propertyKey'); |
| 170 | + if (!propertyKey) { |
| 171 | + logWarn('Panxo: Missing required param "propertyKey"'); |
| 172 | + return false; |
| 173 | + } |
| 174 | + if (!deepAccess(bid, 'mediaTypes.banner')) { |
| 175 | + logWarn('Panxo: Only banner mediaType is supported'); |
| 176 | + return false; |
| 177 | + } |
| 178 | + return true; |
| 179 | + }, |
| 180 | + |
| 181 | + buildRequests(validBidRequests, bidderRequest) { |
| 182 | + const panxoUid = getPanxoUserId(); |
| 183 | + if (!panxoUid) { |
| 184 | + logWarn('Panxo: panxo_uid not found. Ensure Signal script is loaded before Prebid.'); |
| 185 | + return []; |
| 186 | + } |
| 187 | + |
| 188 | + // Group bids by propertyKey to handle multiple properties on same page |
| 189 | + const bidsByPropertyKey = {}; |
| 190 | + validBidRequests.forEach(bid => { |
| 191 | + const key = deepAccess(bid, 'params.propertyKey'); |
| 192 | + if (!bidsByPropertyKey[key]) { |
| 193 | + bidsByPropertyKey[key] = []; |
| 194 | + } |
| 195 | + bidsByPropertyKey[key].push(bid); |
| 196 | + }); |
| 197 | + |
| 198 | + // Build one request per propertyKey |
| 199 | + const requests = []; |
| 200 | + Object.keys(bidsByPropertyKey).forEach(propertyKey => { |
| 201 | + const bidsForKey = bidsByPropertyKey[propertyKey]; |
| 202 | + |
| 203 | + const impressions = bidsForKey.map((bid) => { |
| 204 | + const banner = buildBanner(bid); |
| 205 | + if (!banner) return null; |
| 206 | + |
| 207 | + const sizes = deepAccess(bid, 'mediaTypes.banner.sizes') || []; |
| 208 | + const primarySize = sizes[0] || [300, 250]; |
| 209 | + |
| 210 | + // Include ortb2Imp if available |
| 211 | + const ortb2Imp = deepAccess(bid, 'ortb2Imp.ext'); |
| 212 | + |
| 213 | + return { |
| 214 | + id: bid.bidId, |
| 215 | + banner: banner, |
| 216 | + bidfloor: getFloorPrice(bid, primarySize), |
| 217 | + bidfloorcur: DEFAULT_CURRENCY, |
| 218 | + secure: 1, |
| 219 | + tagid: bid.adUnitCode, |
| 220 | + ext: ortb2Imp || undefined |
| 221 | + }; |
| 222 | + }).filter(Boolean); |
| 223 | + |
| 224 | + if (impressions.length === 0) return; |
| 225 | + |
| 226 | + const openrtbRequest = { |
| 227 | + id: bidderRequest.bidderRequestId, |
| 228 | + imp: impressions, |
| 229 | + site: buildSite(bidderRequest), |
| 230 | + device: buildDevice(), |
| 231 | + user: buildUser(panxoUid, bidderRequest), |
| 232 | + regs: buildRegs(bidderRequest), |
| 233 | + source: buildSource(bidderRequest), |
| 234 | + at: 1, |
| 235 | + cur: [DEFAULT_CURRENCY], |
| 236 | + tmax: bidderRequest.timeout || 1000 |
| 237 | + }; |
| 238 | + |
| 239 | + requests.push({ |
| 240 | + method: 'POST', |
| 241 | + url: `${ENDPOINT_URL}?key=${encodeURIComponent(propertyKey)}&source=prebid`, |
| 242 | + data: openrtbRequest, |
| 243 | + options: { contentType: 'application/json', withCredentials: false }, |
| 244 | + bidderRequest: bidderRequest |
| 245 | + }); |
| 246 | + }); |
| 247 | + |
| 248 | + return requests; |
| 249 | + }, |
| 250 | + |
| 251 | + interpretResponse(serverResponse, request) { |
| 252 | + const bids = []; |
| 253 | + const response = serverResponse.body; |
| 254 | + |
| 255 | + if (!response || !response.seatbid) return bids; |
| 256 | + |
| 257 | + const bidRequestMap = {}; |
| 258 | + if (request.bidderRequest && request.bidderRequest.bids) { |
| 259 | + request.bidderRequest.bids.forEach(bid => { |
| 260 | + bidRequestMap[bid.bidId] = bid; |
| 261 | + }); |
| 262 | + } |
| 263 | + |
| 264 | + response.seatbid.forEach(seatbid => { |
| 265 | + if (!seatbid.bid) return; |
| 266 | + |
| 267 | + seatbid.bid.forEach(bid => { |
| 268 | + const originalBid = bidRequestMap[bid.impid]; |
| 269 | + if (!originalBid) return; |
| 270 | + |
| 271 | + bids.push({ |
| 272 | + requestId: bid.impid, |
| 273 | + cpm: bid.price, |
| 274 | + currency: response.cur || DEFAULT_CURRENCY, |
| 275 | + width: bid.w, |
| 276 | + height: bid.h, |
| 277 | + creativeId: bid.crid || bid.id, |
| 278 | + dealId: bid.dealid || null, |
| 279 | + netRevenue: NET_REVENUE, |
| 280 | + ttl: bid.exp || TTL, |
| 281 | + ad: bid.adm, |
| 282 | + nurl: bid.nurl, |
| 283 | + meta: { |
| 284 | + advertiserDomains: bid.adomain || [], |
| 285 | + mediaType: BANNER |
| 286 | + } |
| 287 | + }); |
| 288 | + }); |
| 289 | + }); |
| 290 | + |
| 291 | + return bids; |
| 292 | + }, |
| 293 | + |
| 294 | + getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { |
| 295 | + const syncs = []; |
| 296 | + |
| 297 | + if (syncOptions.pixelEnabled) { |
| 298 | + let syncUrl = SYNC_URL + '?source=prebid'; |
| 299 | + |
| 300 | + // GDPR |
| 301 | + if (gdprConsent) { |
| 302 | + syncUrl += `&gdpr=${gdprConsent.gdprApplies ? 1 : 0}`; |
| 303 | + if (gdprConsent.consentString) { |
| 304 | + syncUrl += `&gdpr_consent=${encodeURIComponent(gdprConsent.consentString)}`; |
| 305 | + } |
| 306 | + } |
| 307 | + |
| 308 | + // US Privacy |
| 309 | + if (uspConsent) { |
| 310 | + syncUrl += `&us_privacy=${encodeURIComponent(uspConsent)}`; |
| 311 | + } |
| 312 | + |
| 313 | + // GPP |
| 314 | + if (gppConsent) { |
| 315 | + if (gppConsent.gppString) { |
| 316 | + syncUrl += `&gpp=${encodeURIComponent(gppConsent.gppString)}`; |
| 317 | + } |
| 318 | + if (gppConsent.applicableSections) { |
| 319 | + syncUrl += `&gpp_sid=${encodeURIComponent(gppConsent.applicableSections.join(','))}`; |
| 320 | + } |
| 321 | + } |
| 322 | + |
| 323 | + syncs.push({ type: 'image', url: syncUrl }); |
| 324 | + } |
| 325 | + |
| 326 | + return syncs; |
| 327 | + }, |
| 328 | + |
| 329 | + onBidWon(bid) { |
| 330 | + if (bid.nurl) { |
| 331 | + const winUrl = bid.nurl.replace(/\$\{AUCTION_PRICE\}/g, bid.cpm); |
| 332 | + const img = document.createElement('img'); |
| 333 | + img.src = winUrl; |
| 334 | + img.style.display = 'none'; |
| 335 | + document.body.appendChild(img); |
| 336 | + } |
| 337 | + }, |
| 338 | + |
| 339 | + onTimeout(timeoutData) { |
| 340 | + logWarn('Panxo: Bid timeout', timeoutData); |
| 341 | + } |
| 342 | +}; |
| 343 | + |
| 344 | +registerBidder(spec); |
0 commit comments