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
130 changes: 130 additions & 0 deletions modules/haloadsBidAdapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Overview

Module Name: Haloads Bidder Adapter
Module Type: Bidder Adapter
Maintainer: info.litelabs@gmail.com

**Bidder code:** `haloads`

# Description

The Haloads Bidder Adapter connects to Haloads exchange for bids. This adapter supports banner, video, and native ad formats and implements the OpenRTB 2.5 protocol.

**Currency**

- Haloads returns prices in **USD**. The adapter always sets `bid.currency` to `USD`

**Placement mapping**

- The adapter sets `imp.tagid` from `bid.params.placementId`. This value must match the placement identifier provided by Haloads team.
- The adapter also requires accountId as provided by Haloads team.

**Supported media types**

- Banner (`adm` → `bid.ad`)
- Video (`adm` → `bid.vastXml`; optional `nurl` → `bid.vastUrl`)
- Native (`adm` as OpenRTB Native JSON → `bid.native.ortb`)

# Bid Parameters

| Name | Scope | Type | Description |
|----------------|----------|--------|-------------|
| `placementId` | Required | String | Placement ID on Haloads |
| `accountId` | Required | String | Account ID for the publisher as configured on Haloads |

# Test Parameters

## Banner Ad Unit

```javascript
var adUnits = [
{
code: 'banner-ad-unit',
mediaTypes: {
banner: {
sizes: [[300, 250], [728, 90]]
}
},
bids: [{
bidder: 'haloads',
params: {
accountId: '29291001',
placementId: '12345'
}
}]
}
];
```

## Video Ad Unit

```javascript
var adUnits = [
{
code: 'video-ad-unit',
mediaTypes: {
video: {
context: 'instream',
playerSize: [[640, 480]],
mimes: ['video/mp4'],
protocols: [2, 3, 5, 6],
api: [2]
}
},
bids: [{
bidder: 'haloads',
params: {
accountId: '291001',
placementId: 'video-12345'
}
}]
}
];
```

## Native Ad Unit

```javascript
var adUnits = [
{
code: 'native-ad-unit',
mediaTypes: {
native: {
title: {
required: true,
len: 80
},
image: {
required: true,
sizes: [300, 250]
},
sponsoredBy: {
required: true
}
}
},
bids: [{
bidder: 'haloads',
params: {
accountId: '291001',
placementId: 'native-12345'
}
}]
}
];
```

## Configuration

### User Sync

The adapter automatically handles user syncing with privacy consent parameters (GDPR, USP, GPP).

### Event Tracking

The adapter supports the following event callbacks:
- `onBidWon`: Triggered when a bid wins the auction
- `onAdRenderSucceeded`: Triggered when an ad successfully renders
- `onBidderError`: Triggered when a bidder error occurs
- `onTimeout`: Triggered when a bid request times out

195 changes: 195 additions & 0 deletions modules/haloadsBidAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { BidderSpec, ExtendedResponse, registerBidder } from '../src/adapters/bidderFactory.js';
import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js';
import { ortbConverter } from '../libraries/ortbConverter/converter.js';
import { triggerPixel, deepAccess } from '../src/utils.js';
import { tryAppendQueryString } from '../libraries/urlUtils/urlUtils.js';
import type { BidRequest } from '../src/adapterManager.js';

const BIDDER_CODE = 'haloads';
const ENDPOINT = 'https://ads.haloads.io/bid';
const SYNC_URL = 'https://ads.haloads.io/cookie_sync';
const EVENT_TRACKING_URL = 'https://analytics.haloads.io/event';

interface HaloadsBidParams {
accountId: string;
placementId: string | number;
[key: string]: unknown;
}

declare module '../src/adUnits' {
interface BidderParams {
[BIDDER_CODE]: HaloadsBidParams;
}
}

declare module '../src/bidfactory' {
interface BannerBidProperties {
pbsWurl?: string;
ext?: Record<string, unknown>;
}
interface VideoBidProperties {
pbsWurl?: string;
ext?: Record<string, unknown>;
}
interface NativeBidProperties {
pbsWurl?: string;
ext?: Record<string, unknown>;
}
}

const converter = ortbConverter({
context: {
netRevenue: true,
ttl: 300
},
imp: function (buildImp, bidRequest: BidRequest<typeof BIDDER_CODE>, context) {
const imp = buildImp(bidRequest, context);
imp.tagid = bidRequest.params.placementId.toString();
return imp;
},
bidResponse: function (buildBidResponse, bid, context) {
const bidResponse = buildBidResponse(bid, context) as ReturnType<typeof buildBidResponse> & { ext?: unknown };
const bidAny = bid as unknown as Record<string, unknown>;
if (bidAny.ext) {
bidResponse.ext = bidAny.ext;
}
if (bidResponse.mediaType === VIDEO) {
const vBidResponse = bidResponse as typeof bidResponse & { vastXml?: string };
if (!vBidResponse.vastXml && bidAny.adm) {
vBidResponse.vastXml = bidAny.adm as string;
}
}
return bidResponse;
}
});

export const spec: BidderSpec<typeof BIDDER_CODE> = {
code: BIDDER_CODE,
supportedMediaTypes: [BANNER, VIDEO, NATIVE],

isBidRequestValid: function (bid) {
return !!(bid.params && bid.params.accountId && bid.params.placementId);
},

buildRequests: function (validBidRequests, bidderRequest) {
const data = converter.toORTB({ bidderRequest, bidRequests: validBidRequests as any });

const accountId = validBidRequests[0].params.accountId;
if (accountId && data.site) {
if (!data.site.publisher) {
data.site.publisher = {};
}
data.site.publisher.id = accountId.toString();
}

return {
method: 'POST',
url: ENDPOINT,
data: data,
options: {
withCredentials: true
}
};
},

interpretResponse: function (serverResponse, bidRequest) {
if (!serverResponse || !serverResponse.body) {
return [];
}

const ortbResponse = converter.fromORTB({ response: serverResponse.body, request: bidRequest.data });
return (ortbResponse as ExtendedResponse).bids ?? [];
},

getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) {
const syncs: { type: 'iframe' | 'image'; url: string }[] = [];

if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) {
return syncs;
}

let query = '';
if (gdprConsent) {
query = tryAppendQueryString(query, 'gdpr', String(gdprConsent.gdprApplies ? 1 : 0));
}
if (gdprConsent && typeof gdprConsent.consentString === 'string') {
query = tryAppendQueryString(query, 'gdpr_consent', gdprConsent.consentString);
}
if (uspConsent) {
query = tryAppendQueryString(query, 'us_privacy', uspConsent as string);
}
if (gppConsent?.gppString && gppConsent?.applicableSections?.length) {
query = tryAppendQueryString(query, 'gpp', gppConsent.gppString);
query = tryAppendQueryString(query, 'gpp_sid', gppConsent.applicableSections.join(','));
}
if (query.slice(-1) === '&') {
query = query.slice(0, -1);
}

if (syncOptions.iframeEnabled) {
syncs.push({
type: 'iframe',
url: `${SYNC_URL}/sync.html${query ? '?' + query : ''}`
});
}
if (syncOptions.pixelEnabled) {
syncs.push({
type: 'image',
url: `${SYNC_URL}/sync.png${query ? '?' + query : ''}`
});
}
return syncs;
},

onBidWon: function (bid) {
if (bid.pbsWurl) {
triggerPixel(bid.pbsWurl);
}
},

onAdRenderSucceeded: function (bid) {
const impUrl = deepAccess(bid, 'ext.prebid.events.imp') || deepAccess(bid, 'meta.ext.prebid.events.imp');
if (impUrl) {
triggerPixel(impUrl);
}
},

onBidderError: function (errorData) {
let query = 'eventType=error';

if (errorData) {
if (errorData.error) {
const status = errorData.error.status != null ? String(errorData.error.status) : 'unknown';
query = tryAppendQueryString(query, 'error', status);
query = tryAppendQueryString(query, 'status', status);
}
if (errorData.bidderRequest) {
query = tryAppendQueryString(query, 'auctionId', errorData.bidderRequest.auctionId || '');
}
if (query.slice(-1) === '&') {
query = query.slice(0, -1);
}
}

triggerPixel(`${EVENT_TRACKING_URL}?${query}`);
},

onTimeout: function (timeoutData) {
if (!timeoutData || timeoutData.length === 0) {
return;
}

timeoutData.forEach(function (entry) {
let query = 'eventType=timeout';
query = tryAppendQueryString(query, 'auctionId', entry.auctionId || '');
query = tryAppendQueryString(query, 'timeout', String(entry.timeout));
query = tryAppendQueryString(query, 'adUnitCode', entry.adUnitCode || '');
if (query.slice(-1) === '&') {
query = query.slice(0, -1);
}
triggerPixel(`${EVENT_TRACKING_URL}?${query}`);
});
}
};

registerBidder(spec);
Loading
Loading