Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
152 changes: 152 additions & 0 deletions modules/goadserverBidAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { ortbConverter } from '../libraries/ortbConverter/converter.js';
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js';
import { deepSetValue, triggerPixel, isStr } from '../src/utils.js';

/**
* Prebid.js adapter for goadserver — a self-hosted, multi-tenant ad
* serving platform with OpenRTB 2.5 Prebid Server support.
*
* Each goadserver deployment runs under its own domain and exposes the
* standard `/openrtb2/auction` endpoint. Publishers point the adapter at
* their specific deployment via `params.host`; the authentication token
* (the SSP campaign hash from the publisher's goadserver panel) is
* passed via `params.token` and lands in the outgoing BidRequest as
* `site.publisher.id` — the location goadserver's auction handler
* resolves the publisher account from.
*
* One adapter serves every goadserver deployment. There is deliberately
* no bidder-code alias per deployment because the endpoint URL and token
* are per-bid parameters, not per-registration; publishers running
* multiple goadserver instances just pass different `params.host` values.
*
* @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid
* @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest
*/

const BIDDER_CODE = 'goadserver';
const DEFAULT_CURRENCY = 'USD';
const DEFAULT_TTL = 300;
// GVL ID: not yet registered with IAB Europe. File a TCF registration at
// https://iabeurope.eu/tcf/ and populate this field before EU traffic
// goes through the adapter, otherwise CMPs may drop bid requests.
// const GVLID = 0;

const converter = ortbConverter({
context: {
netRevenue: true,
ttl: DEFAULT_TTL,
currency: DEFAULT_CURRENCY,
},

// Per-impression hook: apply the optional per-bid floor override the
// publisher set via `params.floor`. Only applied when the Price Floors
// module hasn't already populated `imp.bidfloor` from a floors config.
imp(buildImp, bidRequest, context) {
const imp = buildImp(bidRequest, context);
if (bidRequest.params?.floor != null && !imp.bidfloor) {
imp.bidfloor = Number(bidRequest.params.floor);
imp.bidfloorcur = DEFAULT_CURRENCY;
}
return imp;
},

// Request-level hook: inject the publisher token into
// `site.publisher.id`, which is where goadserver's /openrtb2/auction
// handler resolves the SSP campaign from.
request(buildRequest, imps, bidderRequest, context) {
const request = buildRequest(imps, bidderRequest, context);
if (!request.cur || request.cur.length === 0) {
request.cur = [DEFAULT_CURRENCY];
}
const token = context.bidRequests?.[0]?.params?.token;
if (token) {
deepSetValue(request, 'site.publisher.id', token);
}
return request;
},
});

/** @type {import('../src/adapters/bidderFactory.js').BidderSpec} */
export const spec = {
code: BIDDER_CODE,
// gvlid: GVLID, // TODO: populate once registered with IAB Europe
supportedMediaTypes: [BANNER, VIDEO, NATIVE],

/**
* Every bid must carry a host (goadserver deployment domain) and a
* token (SSP campaign hash from the publisher panel). Without both,
* the auction can't be authenticated or routed.
*
* @param {Object} bid
* @returns {boolean}
*/
isBidRequestValid: function (bid) {
return Boolean(bid?.params?.host) &&
typeof bid.params.host === 'string' &&
Boolean(bid?.params?.token) &&
typeof bid.params.token === 'string';
},

/**
* Build an OpenRTB 2.5 BidRequest and POST it to the publisher's
* specific goadserver deployment. All bids in a single buildRequests
* call share the same publisher context (same page, same token) so
* we emit one BidRequest with N imps to one endpoint URL.
*
* @param {Object[]} validBidRequests
* @param {Object} bidderRequest
* @returns {ServerRequest}
*/
buildRequests: function (validBidRequests, bidderRequest) {
if (!validBidRequests || validBidRequests.length === 0) {
return [];
}
const host = validBidRequests[0].params.host;
const url = `https://${host}/openrtb2/auction`;
const data = converter.toORTB({
bidRequests: validBidRequests,
Comment on lines +185 to +188
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Split bid requests by host and token

buildRequests always picks validBidRequests[0].params.host and then serializes all bids into one ORTB request, so auctions that include multiple goadserver tenants (different host and/or token values across ad units) will route impressions to the wrong endpoint and apply the wrong publisher token, leading to rejected or misattributed bids. Group requests by tenant key (host + token) and return one server request per group.

Useful? React with 👍 / 👎.

bidderRequest,
});
return {
method: 'POST',
url,
data,
options: { contentType: 'application/json', withCredentials: true },
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Send ORTB payload with a simple content type

Using contentType: 'application/json' makes this a non-simple cross-origin request, which triggers a CORS preflight (OPTIONS) before each auction call; that adds latency and can cause bidder timeouts or dropped responses when the endpoint does not fully handle preflight. Prebid adapters usually avoid this by posting the same JSON payload with a simple content type (for example text/plain).

Useful? React with 👍 / 👎.

};
},

/**
* Translate goadserver's OpenRTB 2.5 BidResponse back into Prebid
* bids. ortbConverter handles the bulk of the mapping — we just
* delegate.
*
* @param {Object} serverResponse
* @param {ServerRequest} request
* @returns {Bid[]}
*/
interpretResponse: function (serverResponse, request) {
if (!serverResponse?.body) {
return [];
}
return converter.fromORTB({
response: serverResponse.body,
request: request.data,
}).bids;
},

/**
* Fire the impression URL when a bid wins. goadserver uses the same
* `nurl` tracking pattern as its existing RTB path, so triggering the
* pixel here unifies win notification with the rest of the platform.
*
* @param {Bid} bid
*/
onBidWon: function (bid) {
if (bid?.nurl && isStr(bid.nurl)) {
triggerPixel(bid.nurl);
}
},
};

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

```
Module Name: goadserver Bidder Adapter
Module Type: Bidder Adapter
Maintainer: support@goadserver.com
```

# Description

Prebid.js adapter for the goadserver platform — a self-hosted, multi-tenant ad serving stack with a built-in OpenRTB 2.5 Prebid Server endpoint.

One adapter serves every goadserver deployment. The specific ad server to route bids to is passed per-ad-unit via `params.host`, and the publisher's SSP campaign authentication token (generated in the goadserver panel) is passed via `params.token`. Publishers running multiple goadserver instances can mix and match them in a single Prebid.js config by setting different `params.host` values on different ad units.

Supported media types: `banner`, `video`, `native`.

# Bid Parameters

| Name | Scope | Description | Example | Type |
| -------- | -------- | ------------------------------------------------------------------------------------------------------------- | --------------------- | -------- |
| `host` | required | The goadserver deployment's public domain. The adapter POSTs to `https://{host}/openrtb2/auction`. | `"ads.example.com"` | `string` |
| `token` | required | SSP campaign authentication token from the publisher's goadserver panel. Goes into `site.publisher.id`. | `"a1b2c3d4..."` | `string` |
| `floor` | optional | Per-bid CPM floor (USD). Honored only if the Price Floors module hasn't already set `imp.bidfloor`. | `0.50` | `number` |

# Test Parameters

```js
const adUnits = [
{
code: "top-banner",
mediaTypes: {
banner: {
sizes: [[728, 90], [970, 250]]
}
},
bids: [
{
bidder: "goadserver",
params: {
host: "ads.example.com",
token: "your-sspcampaigns-hash",
floor: 0.50
}
}
]
},
{
code: "preroll",
mediaTypes: {
video: {
context: "instream",
playerSize: [[640, 480]],
mimes: ["video/mp4"]
}
},
bids: [
{
bidder: "goadserver",
params: {
host: "ads.example.com",
token: "your-sspcampaigns-hash"
}
}
]
},
{
code: "native-1",
mediaTypes: {
native: {
title: { required: true },
image: { required: true },
sponsoredBy: { required: true }
}
},
bids: [
{
bidder: "goadserver",
params: {
host: "ads.example.com",
token: "your-sspcampaigns-hash"
}
}
]
}
];
```

# Multi-Deployment Example

Two ad units auctioned against two different goadserver deployments in one request:

```js
pbjs.addAdUnits([
{
code: "slot-a",
mediaTypes: { banner: { sizes: [[300, 250]] } },
bids: [{
bidder: "goadserver",
params: { host: "deployment1.example.com", token: "token-a" }
}]
},
{
code: "slot-b",
mediaTypes: { banner: { sizes: [[728, 90]] } },
bids: [{
bidder: "goadserver",
params: { host: "deployment2.example.com", token: "token-b" }
}]
}
]);
```

# Consent & Privacy

The adapter honors Prebid.js's standard consent plumbing (`ortbConverter` handles it): GDPR (`regs.ext.gdpr`, `user.ext.consent`), US Privacy (`regs.ext.us_privacy`), GPP (`regs.gpp` + `regs.gpp_sid`), and COPPA (`regs.coppa`). No bidder-specific consent handling is required on the publisher side.

**GVL ID note:** the goadserver adapter is not yet registered with the IAB Global Vendor List. EU publishers using CMPs that enforce GVL may see bid requests dropped pre-auction until the registration is filed at https://iabeurope.eu/tcf/.
Loading