diff --git a/integrationExamples/gpt/adcluster_banner_example.html b/integrationExamples/gpt/adcluster_banner_example.html
new file mode 100644
index 0000000000..4f7bf646bb
--- /dev/null
+++ b/integrationExamples/gpt/adcluster_banner_example.html
@@ -0,0 +1,115 @@
+
+
+
+
+ Adcluster Adapter Test
+
+
+
+
+
+
+
+
+
+
+ Prebid.js Live Adapter Test
+
+
+
+
+
diff --git a/integrationExamples/gpt/adcluster_video_example.html b/integrationExamples/gpt/adcluster_video_example.html
new file mode 100644
index 0000000000..0a309b2474
--- /dev/null
+++ b/integrationExamples/gpt/adcluster_video_example.html
@@ -0,0 +1,291 @@
+
+
+
+
+ Adcluster Adapter – Outstream Test with Fallback
+
+
+
+
+
+
+
+
+
+ Adcluster Adapter – Outstream Test (AN renderer + IMA fallback)
+
+
+
+
+
+
+
diff --git a/modules/adclusterBidAdapter.js b/modules/adclusterBidAdapter.js
new file mode 100644
index 0000000000..b8b1f24858
--- /dev/null
+++ b/modules/adclusterBidAdapter.js
@@ -0,0 +1,183 @@
+import { registerBidder } from "../src/adapters/bidderFactory.js";
+import { BANNER, VIDEO } from "../src/mediaTypes.js";
+
+const BIDDER_CODE = "adcluster";
+const ENDPOINT = "https://core.adcluster.com.tr/bid";
+
+export const spec = {
+ code: BIDDER_CODE,
+ supportedMediaTypes: [BANNER, VIDEO],
+
+ isBidRequestValid(bid) {
+ return !!bid?.params?.unitId;
+ },
+
+ buildRequests(validBidRequests, bidderRequest) {
+ const _auctionId = bidderRequest.auctionId || "";
+ const payload = {
+ bidderCode: bidderRequest.bidderCode,
+ auctionId: _auctionId,
+ bidderRequestId: bidderRequest.bidderRequestId,
+ bids: validBidRequests.map((b) => buildImp(b)),
+ auctionStart: bidderRequest.auctionStart,
+ timeout: bidderRequest.timeout,
+ start: bidderRequest.start,
+ regs: { ext: {} },
+ user: { ext: {} },
+ source: { ext: {} },
+ };
+
+ // privacy
+ if (bidderRequest?.gdprConsent) {
+ payload.regs = payload.regs || { ext: {} };
+ payload.regs.ext = payload.regs.ext || {};
+ payload.regs.ext.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0;
+ payload.user.ext.consent = bidderRequest.gdprConsent.consentString || "";
+ }
+ if (bidderRequest?.uspConsent) {
+ payload.regs = payload.regs || { ext: {} };
+ payload.regs.ext.us_privacy = bidderRequest.uspConsent;
+ }
+ if (bidderRequest?.ortb2?.regs?.gpp) {
+ payload.regs = payload.regs || { ext: {} };
+ payload.regs.ext.gpp = bidderRequest.ortb2.regs.gpp;
+ payload.regs.ext.gppSid = bidderRequest.ortb2.regs.gpp_sid;
+ }
+ if (validBidRequests[0]?.userIdAsEids) {
+ payload.user.ext.eids = validBidRequests[0].userIdAsEids;
+ }
+ if (validBidRequests[0]?.ortb2?.source?.ext?.schain) {
+ payload.source.ext.schain = validBidRequests[0].ortb2.source.ext.schain;
+ }
+
+ return {
+ method: "POST",
+ url: ENDPOINT,
+ data: payload,
+ options: { contentType: "text/plain" },
+ };
+ },
+
+ interpretResponse(serverResponse) {
+ const body = serverResponse?.body;
+ if (!body || !Array.isArray(body)) return [];
+ const bids = [];
+
+ body.forEach((b) => {
+ const mediaType = detectMediaType(b);
+ const bid = {
+ requestId: b.requestId,
+ cpm: b.cpm,
+ currency: b.currency,
+ width: b.width,
+ height: b.height,
+ creativeId: b.creativeId,
+ ttl: b.ttl,
+ netRevenue: b.netRevenue,
+ meta: {
+ advertiserDomains: b.meta?.advertiserDomains || [],
+ },
+ mediaType,
+ };
+
+ if (mediaType === BANNER) {
+ bid.ad = b.ad;
+ }
+ if (mediaType === VIDEO) {
+ bid.vastUrl = b.ad;
+ }
+ bids.push(bid);
+ });
+
+ return bids;
+ },
+};
+
+/* ---------- helpers ---------- */
+
+function buildImp(bid) {
+ const _transactionId = bid.transactionId || "";
+ const _adUnitId = bid.adUnitId || "";
+ const _auctionId = bid.auctionId || "";
+ const imp = {
+ params: {
+ unitId: bid.params.unitId,
+ },
+ bidId: bid.bidId,
+ bidderRequestId: bid.bidderRequestId,
+ transactionId: _transactionId,
+ adUnitId: _adUnitId,
+ auctionId: _auctionId,
+ ext: {
+ floors: getFloorsAny(bid),
+ },
+ };
+
+ if (bid.params && bid.params.previewMediaId) {
+ imp.params.previewMediaId = bid.params.previewMediaId;
+ }
+
+ const mt = bid.mediaTypes || {};
+
+ // BANNER
+ if (mt.banner?.sizes?.length) {
+ imp.width = mt.banner.sizes[0] && mt.banner.sizes[0][0];
+ imp.height = mt.banner.sizes[0] && mt.banner.sizes[0][1];
+ }
+ if (mt.video) {
+ const v = mt.video;
+ const playerSize = toSizeArray(v.playerSize);
+ const [vw, vh] = playerSize?.[0] || [];
+ imp.width = vw;
+ imp.height = vh;
+ imp.video = {
+ minduration: v.minduration || 1,
+ maxduration: v.maxduration || 120,
+ ext: {
+ context: v.context || "instream",
+ floor: getFloors(bid, "video", playerSize?.[0]),
+ },
+ };
+ }
+
+ return imp;
+}
+
+function toSizeArray(s) {
+ if (!s) return null;
+ // playerSize can be [w,h] or [[w,h], [w2,h2]]
+ return Array.isArray(s[0]) ? s : [s];
+}
+
+function getFloors(bid, mediaType = "banner", size) {
+ try {
+ if (!bid.getFloor) return null;
+ // size can be [w,h] or '*'
+ const sz = Array.isArray(size) ? size : "*";
+ const res = bid.getFloor({ mediaType, size: sz });
+ return res && typeof res.floor === "number" ? res.floor : null;
+ } catch {
+ return null;
+ }
+}
+
+function detectMediaType(bid) {
+ if (bid.mediaType === "video") return VIDEO;
+ else return BANNER;
+}
+
+function getFloorsAny(bid) {
+ // Try to collect floors per type
+ const out = {};
+ const mt = bid.mediaTypes || {};
+ if (mt.banner) {
+ out.banner = getFloors(bid, "banner", "*");
+ }
+ if (mt.video) {
+ const ps = toSizeArray(mt.video.playerSize);
+ out.video = getFloors(bid, "video", (ps && ps[0]) || "*");
+ }
+ return out;
+}
+
+registerBidder(spec);
diff --git a/modules/adclusterBidAdapter.md b/modules/adclusterBidAdapter.md
new file mode 100644
index 0000000000..59300e2c85
--- /dev/null
+++ b/modules/adclusterBidAdapter.md
@@ -0,0 +1,46 @@
+# Overview
+
+**Module Name**: Adcluster Bidder Adapter
+**Module Type**: Bidder Adapter
+**Maintainer**: dev@adcluster.com.tr
+
+# Description
+
+Prebid.js bidder adapter module for connecting to Adcluster.
+
+# Test Parameters
+
+```
+var adUnits = [
+ {
+ code: 'adcluster-banner',
+ mediaTypes: {
+ banner: {
+ sizes: [[300, 250]],
+ }
+ },
+ bids: [{
+ bidder: 'adcluster',
+ params: {
+ unitId: '42d1f525-5792-47a6-846d-1825e53c97d6',
+ previewMediaId: "b4dbc48c-0b90-4628-bc55-f46322b89b63",
+ },
+ }]
+ },
+ {
+ code: 'adcluster-video',
+ mediaTypes: {
+ video: {
+ playerSize: [[640, 480]],
+ }
+ },
+ bids: [{
+ bidder: 'adcluster',
+ params: {
+ unitId: "37dd91b2-049d-4027-94b9-d63760fc10d3",
+ previewMediaId: "133b7dc9-bb6e-4ab2-8f95-b796cf19f27e",
+ },
+ }]
+ }
+];
+```
diff --git a/test/spec/modules/adclusterBidAdapter_spec.js b/test/spec/modules/adclusterBidAdapter_spec.js
new file mode 100644
index 0000000000..d068a0b594
--- /dev/null
+++ b/test/spec/modules/adclusterBidAdapter_spec.js
@@ -0,0 +1,415 @@
+// test/spec/modules/adclusterBidAdapter_spec.js
+
+import { expect } from "chai";
+import { spec } from "modules/adclusterBidAdapter.js"; // adjust path if needed
+import { BANNER, VIDEO } from "src/mediaTypes.js";
+
+const BIDDER_CODE = "adcluster";
+const ENDPOINT = "https://core.adcluster.com.tr/bid";
+
+describe("adclusterBidAdapter", function () {
+ // ---------- Test Fixtures (immutable) ----------
+ const baseBid = Object.freeze({
+ bidder: BIDDER_CODE,
+ bidId: "2f5d",
+ bidderRequestId: "breq-1",
+ auctionId: "auc-1",
+ transactionId: "txn-1",
+ adUnitCode: "div-1",
+ adUnitId: "adunit-1",
+ params: { unitId: "61884b5c-9420-4f15-871f-2dcc2fa1cff5" },
+ });
+
+ const gdprConsent = Object.freeze({
+ gdprApplies: true,
+ consentString: "BOJ/P2HOJ/P2HABABMAAAAAZ+A==",
+ });
+
+ const uspConsent = "1---";
+ const gpp = "DBABLA..";
+ const gppSid = [7, 8];
+
+ const bidderRequestBase = Object.freeze({
+ auctionId: "auc-1",
+ bidderCode: BIDDER_CODE,
+ bidderRequestId: "breq-1",
+ auctionStart: 1111111111111,
+ timeout: 2000,
+ start: 1111111111112,
+ ortb2: { regs: { gpp, gpp_sid: gppSid } },
+ gdprConsent,
+ uspConsent,
+ });
+
+ // helpers return fresh objects to avoid cross-test mutation
+ function mkBidBanner(extra = {}) {
+ return {
+ ...baseBid,
+ mediaTypes: {
+ banner: {
+ sizes: [
+ [300, 250],
+ [300, 600],
+ ],
+ },
+ },
+ getFloor: ({ mediaType }) => {
+ if (mediaType === "banner") return { currency: "USD", floor: 0.5 };
+ return { currency: "USD", floor: 0.0 };
+ },
+ userIdAsEids: [
+ { source: "example.com", uids: [{ id: "abc", atype: 1 }] },
+ ],
+ ortb2: {
+ source: { ext: { schain: { ver: "1.0", complete: 1, nodes: [] } } },
+ },
+ ...extra,
+ };
+ }
+
+ function mkBidVideo(extra = {}) {
+ return {
+ ...baseBid,
+ mediaTypes: {
+ video: {
+ context: "instream",
+ playerSize: [640, 360],
+ minduration: 5,
+ maxduration: 30,
+ },
+ },
+ getFloor: ({ mediaType, size }) => {
+ if (mediaType === "video" && Array.isArray(size)) {
+ return { currency: "USD", floor: 1.2 };
+ }
+ return { currency: "USD", floor: 0.0 };
+ },
+ userIdAsEids: [
+ { source: "example.com", uids: [{ id: "xyz", atype: 1 }] },
+ ],
+ ortb2: {
+ source: { ext: { schain: { ver: "1.0", complete: 1, nodes: [] } } },
+ },
+ ...extra,
+ };
+ }
+
+ describe("isBidRequestValid", function () {
+ it("returns true when params.unitId is present", function () {
+ // Arrange
+ const bid = { ...baseBid };
+
+ // Act
+ const valid = spec.isBidRequestValid(bid);
+
+ // Assert
+ expect(valid).to.equal(true);
+ });
+
+ it("returns false when params.unitId is missing", function () {
+ // Arrange
+ const bid = { ...baseBid, params: {} };
+
+ // Act
+ const valid = spec.isBidRequestValid(bid);
+
+ // Assert
+ expect(valid).to.equal(false);
+ });
+
+ it("returns false when params is undefined", function () {
+ // Arrange
+ const bid = { ...baseBid, params: undefined };
+
+ // Act
+ const valid = spec.isBidRequestValid(bid);
+
+ // Assert
+ expect(valid).to.equal(false);
+ });
+ });
+
+ describe("buildRequests", function () {
+ it("builds a POST request with JSON body to the right endpoint", function () {
+ // Arrange
+ const br = { ...bidderRequestBase };
+ const bids = [mkBidBanner(), mkBidVideo()];
+
+ // Act
+ const req = spec.buildRequests(bids, br);
+
+ // Assert
+ expect(req.method).to.equal("POST");
+ expect(req.url).to.equal(ENDPOINT);
+ expect(req.options).to.deep.equal({ contentType: "text/plain" });
+ expect(req.data).to.be.an("object");
+ expect(req.data.bidderCode).to.equal(BIDDER_CODE);
+ expect(req.data.auctionId).to.equal(br.auctionId);
+ expect(req.data.bids).to.be.an("array").with.length(2);
+ });
+
+ it("includes privacy signals (GDPR, USP, GPP) when present", function () {
+ // Arrange
+ const br = { ...bidderRequestBase };
+ const bids = [mkBidBanner()];
+
+ // Act
+ const req = spec.buildRequests(bids, br);
+
+ // Assert
+ const { regs, user } = req.data;
+ expect(regs).to.be.an("object");
+ expect(regs.ext.gdpr).to.equal(1);
+ expect(user.ext.consent).to.equal(gdprConsent.consentString);
+ expect(regs.ext.us_privacy).to.equal(uspConsent);
+ expect(regs.ext.gpp).to.equal(gpp);
+ expect(regs.ext.gppSid).to.deep.equal(gppSid);
+ });
+
+ it("omits privacy fields when not provided", function () {
+ // Arrange
+ const minimalBR = {
+ auctionId: "auc-2",
+ bidderCode: BIDDER_CODE,
+ bidderRequestId: "breq-2",
+ auctionStart: 1,
+ timeout: 1000,
+ start: 2,
+ };
+ const bids = [mkBidBanner()];
+
+ // Act
+ const req = spec.buildRequests(bids, minimalBR);
+
+ // Assert
+ // regs.ext should exist but contain no privacy flags
+ expect(req.data.regs).to.be.an("object");
+ expect(req.data.regs.ext).to.deep.equal({});
+ // user.ext.consent must be undefined when no GDPR
+ expect(req.data.user).to.be.an("object");
+ expect(req.data.user.ext).to.be.an("object");
+ expect(req.data.user.ext.consent).to.be.undefined;
+ // allow eids to be present (they come from bids)
+ // don't assert deep-equality on user.ext, just ensure no privacy fields
+ expect(req.data.user.ext.gdpr).to.be.undefined;
+ });
+
+ it("passes userIdAsEids and schain when provided", function () {
+ // Arrange
+ const br = { ...bidderRequestBase };
+ const bids = [mkBidBanner()];
+
+ // Act
+ const req = spec.buildRequests(bids, br);
+
+ // Assert
+ expect(req.data.user.ext.eids).to.be.an("array").with.length(1);
+ expect(req.data.source.ext.schain).to.be.an("object");
+ });
+
+ it("sets banner dimensions from first size and includes floors ext", function () {
+ // Arrange
+ const br = { ...bidderRequestBase };
+ const bids = [mkBidBanner()];
+
+ // Act
+ const req = spec.buildRequests(bids, br);
+
+ // Assert
+ const imp = req.data.bids[0];
+ expect(imp.width).to.equal(300);
+ expect(imp.height).to.equal(250);
+ expect(imp.ext).to.have.property("floors");
+ expect(imp.ext.floors.banner).to.equal(0.5);
+ });
+
+ it("sets video sizes from playerSize and includes video floors", function () {
+ // Arrange
+ const br = { ...bidderRequestBase };
+ const bids = [mkBidVideo()];
+
+ // Act
+ const req = spec.buildRequests(bids, br);
+
+ // Assert
+ const imp = req.data.bids[0];
+ expect(imp.width).to.equal(640);
+ expect(imp.height).to.equal(360);
+ expect(imp.video).to.be.an("object");
+ expect(imp.video.minduration).to.equal(5);
+ expect(imp.video.maxduration).to.equal(30);
+ expect(imp.video.ext.context).to.equal("instream");
+ expect(imp.video.ext.floor).to.equal(1.2);
+ expect(imp.ext.floors.video).to.equal(1.2);
+ });
+
+ it("gracefully handles missing getFloor", function () {
+ // Arrange
+ const br = { ...bidderRequestBase };
+ const bids = [mkBidBanner({ getFloor: undefined })];
+
+ // Act
+ const req = spec.buildRequests(bids, br);
+
+ // Assert
+ expect(req.data.bids[0].ext.floors.banner).to.equal(null);
+ });
+
+ it("passes previewMediaId when provided", function () {
+ // Arrange
+ const br = { ...bidderRequestBase };
+ const bids = [
+ mkBidVideo({ params: { unitId: "x", previewMediaId: "media-123" } }),
+ ];
+
+ // Act
+ const req = spec.buildRequests(bids, br);
+
+ // Assert
+ expect(req.data.bids[0].params.previewMediaId).to.equal("media-123");
+ });
+ });
+
+ describe("interpretResponse", function () {
+ it("returns empty array when body is missing or not an array", function () {
+ // Arrange
+ const missing = { body: null };
+ const notArray = { body: {} };
+
+ // Act
+ const out1 = spec.interpretResponse(missing);
+ const out2 = spec.interpretResponse(notArray);
+
+ // Assert
+ expect(out1).to.deep.equal([]);
+ expect(out2).to.deep.equal([]);
+ });
+
+ it("maps banner responses to Prebid bids", function () {
+ // Arrange
+ const serverBody = [
+ {
+ requestId: "2f5d",
+ cpm: 1.23,
+ currency: "USD",
+ width: 300,
+ height: 250,
+ creativeId: "cr-1",
+ ttl: 300,
+ netRevenue: true,
+ mediaType: "banner",
+ ad: "creative
",
+ meta: { advertiserDomains: ["advertiser.com"] },
+ },
+ ];
+
+ // Act
+ const out = spec.interpretResponse({ body: serverBody });
+
+ // Assert
+ expect(out).to.have.length(1);
+ const b = out[0];
+ expect(b.requestId).to.equal("2f5d");
+ expect(b.cpm).to.equal(1.23);
+ expect(b.mediaType).to.equal(BANNER);
+ expect(b.ad).to.be.a("string");
+ expect(b.meta.advertiserDomains).to.deep.equal(["advertiser.com"]);
+ });
+
+ it("maps video responses to Prebid bids (vastUrl)", function () {
+ // Arrange
+ const serverBody = [
+ {
+ requestId: "vid-1",
+ cpm: 2.5,
+ currency: "USD",
+ width: 640,
+ height: 360,
+ creativeId: "cr-v",
+ ttl: 300,
+ netRevenue: true,
+ mediaType: "video",
+ ad: "https://vast.tag/url.xml",
+ meta: { advertiserDomains: ["brand.com"] }, // mediaType hint optional
+ },
+ ];
+
+ // Act
+ const out = spec.interpretResponse({ body: serverBody });
+
+ // Assert
+ expect(out).to.have.length(1);
+ const b = out[0];
+ expect(b.requestId).to.equal("vid-1");
+ expect(b.mediaType).to.equal(VIDEO);
+ expect(b.vastUrl).to.equal("https://vast.tag/url.xml");
+ expect(b.ad).to.be.undefined;
+ });
+
+ it("handles missing meta.advertiserDomains safely", function () {
+ // Arrange
+ const serverBody = [
+ {
+ requestId: "2f5d",
+ cpm: 0.2,
+ currency: "USD",
+ width: 300,
+ height: 250,
+ creativeId: "cr-2",
+ ttl: 120,
+ netRevenue: true,
+ ad: "",
+ meta: {},
+ },
+ ];
+
+ // Act
+ const out = spec.interpretResponse({ body: serverBody });
+
+ // Assert
+ expect(out[0].meta.advertiserDomains).to.deep.equal([]);
+ });
+
+ it("supports multiple mixed responses", function () {
+ // Arrange
+ const serverBody = [
+ {
+ requestId: "b-1",
+ cpm: 0.8,
+ currency: "USD",
+ width: 300,
+ height: 250,
+ creativeId: "cr-b",
+ ttl: 300,
+ netRevenue: true,
+ ad: "banner
",
+ mediaType: "banner",
+ meta: { advertiserDomains: [] },
+ },
+ {
+ requestId: "v-1",
+ cpm: 3.1,
+ currency: "USD",
+ width: 640,
+ height: 360,
+ creativeId: "cr-v",
+ ttl: 300,
+ netRevenue: true,
+ mediaType: "video",
+ ad: "https://vast.example/vast.xml",
+ meta: { advertiserDomains: ["x.com"] },
+ },
+ ];
+
+ // Act
+ const out = spec.interpretResponse({ body: serverBody });
+
+ // Assert
+ expect(out).to.have.length(2);
+ const [b, v] = out;
+ expect(b.mediaType).to.equal(BANNER);
+ expect(v.mediaType).to.equal(VIDEO);
+ expect(v.vastUrl).to.match(/^https:\/\/vast\.example/);
+ });
+ });
+});