diff --git a/modules/padsquadBidAdapter.js b/modules/padsquadBidAdapter.js
new file mode 100644
index 00000000000..f5800bed7c9
--- /dev/null
+++ b/modules/padsquadBidAdapter.js
@@ -0,0 +1,132 @@
+import {registerBidder} from '../src/adapters/bidderFactory';
+import * as utils from '../src/utils';
+import {BANNER} from '../src/mediaTypes';
+
+const ENDPOINT_URL = '//x.padsquad.com/auction';
+
+const DEFAULT_BID_TTL = 30;
+const DEFAULT_CURRENCY = 'USD';
+const DEFAULT_NET_REVENUE = true;
+
+export const spec = {
+ code: 'padsquad',
+ supportedMediaTypes: [BANNER],
+
+ isBidRequestValid: function (bid) {
+ return (!!bid.params.unitId && typeof bid.params.unitId === 'string') ||
+ (!!bid.params.networkId && typeof bid.params.networkId === 'string') ||
+ (!!bid.params.publisherId && typeof bid.params.publisherId === 'string');
+ },
+
+ buildRequests: function (validBidRequests, bidderRequest) {
+ if (!validBidRequests || !bidderRequest) {
+ return;
+ }
+ const publisherId = validBidRequests[0].params.publisherId;
+ const networkId = validBidRequests[0].params.networkId;
+ const impressions = validBidRequests.map(bidRequest => ({
+ id: bidRequest.bidId,
+ banner: {
+ format: bidRequest.sizes.map(sizeArr => ({
+ w: sizeArr[0],
+ h: sizeArr[1]
+ }))
+ },
+ ext: {
+ exchange: {
+ unitId: bidRequest.params.unitId
+ }
+ }
+ }));
+
+ const openrtbRequest = {
+ id: bidderRequest.auctionId,
+ imp: impressions,
+ site: {
+ domain: window.location.hostname,
+ page: window.location.href,
+ ref: bidderRequest.refererInfo ? bidderRequest.refererInfo.referer || null : null
+ },
+ ext: {
+ exchange: {
+ publisherId: publisherId,
+ networkId: networkId,
+ }
+ }
+ };
+
+ // apply gdpr
+ if (bidderRequest.gdprConsent) {
+ openrtbRequest.regs = {ext: {gdpr: bidderRequest.gdprConsent.gdprApplies ? 1 : 0}};
+ openrtbRequest.user = {ext: {consent: bidderRequest.gdprConsent.consentString}};
+ }
+
+ const payloadString = JSON.stringify(openrtbRequest);
+ return {
+ method: 'POST',
+ url: ENDPOINT_URL,
+ data: payloadString,
+ };
+ },
+
+ interpretResponse: function (serverResponse, request) {
+ const bidResponses = [];
+ const response = (serverResponse || {}).body;
+ // response is always one seat with (optional) bids for each impression
+ if (response && response.seatbid && response.seatbid.length === 1 && response.seatbid[0].bid && response.seatbid[0].bid.length) {
+ response.seatbid[0].bid.forEach(bid => {
+ bidResponses.push({
+ requestId: bid.impid,
+ cpm: bid.price,
+ width: bid.w,
+ height: bid.h,
+ ad: bid.adm,
+ ttl: DEFAULT_BID_TTL,
+ creativeId: bid.crid,
+ netRevenue: DEFAULT_NET_REVENUE,
+ currency: DEFAULT_CURRENCY,
+ })
+ })
+ } else {
+ utils.logInfo('padsquad.interpretResponse :: no valid responses to interpret');
+ }
+ return bidResponses;
+ },
+ getUserSyncs: function (syncOptions, serverResponses) {
+ utils.logInfo('padsquad.getUserSyncs', 'syncOptions', syncOptions, 'serverResponses', serverResponses);
+ let syncs = [];
+
+ if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) {
+ return syncs;
+ }
+
+ serverResponses.forEach(resp => {
+ const userSync = utils.deepAccess(resp, 'body.ext.usersync');
+ if (userSync) {
+ let syncDetails = [];
+ Object.keys(userSync).forEach(key => {
+ const value = userSync[key];
+ if (value.syncs && value.syncs.length) {
+ syncDetails = syncDetails.concat(value.syncs);
+ }
+ });
+ syncDetails.forEach(syncDetails => {
+ syncs.push({
+ type: syncDetails.type === 'iframe' ? 'iframe' : 'image',
+ url: syncDetails.url
+ });
+ });
+
+ if (!syncOptions.iframeEnabled) {
+ syncs = syncs.filter(s => s.type !== 'iframe')
+ }
+ if (!syncOptions.pixelEnabled) {
+ syncs = syncs.filter(s => s.type !== 'image')
+ }
+ }
+ });
+ return syncs;
+ },
+
+};
+registerBidder(spec);
diff --git a/modules/padsquadBidAdapter.md b/modules/padsquadBidAdapter.md
new file mode 100644
index 00000000000..0a69db42ce3
--- /dev/null
+++ b/modules/padsquadBidAdapter.md
@@ -0,0 +1,33 @@
+# Overview
+
+```
+Module Name: Padsquad Bid Adapter
+Module Type: Bidder Adapter
+Maintainer: yeeldpadsquad@gmail.com
+```
+
+# Description
+
+Connects to Padsquad exchange for bids.
+
+Padsquad bid adapter supports Banner ads.
+
+# Test Parameters
+```
+var adUnits = [
+ {
+ code: 'banner-ad-div',
+ mediaTypes: {
+ banner: {
+ sizes: [[300, 250], [300,600]]
+ }
+ },
+ bids: [{
+ bidder: 'padsquad',
+ params: {
+ unitId: 'test'
+ }
+ }]
+ }
+];
+```
diff --git a/test/spec/modules/padsquadBidAdapter_spec.js b/test/spec/modules/padsquadBidAdapter_spec.js
new file mode 100644
index 00000000000..aba1efea32f
--- /dev/null
+++ b/test/spec/modules/padsquadBidAdapter_spec.js
@@ -0,0 +1,261 @@
+import {expect} from 'chai';
+import {spec} from 'modules/padsquadBidAdapter';
+
+const REQUEST = {
+ 'bidderCode': 'padsquad',
+ 'auctionId': 'auctionId-56a2-4f71-9098-720a68f2f708',
+ 'bidderRequestId': 'requestId',
+ 'bidRequest': [{
+ 'bidder': 'padsquad',
+ 'params': {
+ 'unitId': 123456,
+ },
+ 'placementCode': 'div-gpt-dummy-placement-code',
+ 'sizes': [
+ [300, 250]
+ ],
+ 'bidId': 'bidId1',
+ 'bidderRequestId': 'bidderRequestId',
+ 'auctionId': 'auctionId-56a2-4f71-9098-720a68f2f708'
+ },
+ {
+ 'bidder': 'padsquad',
+ 'params': {
+ 'unitId': 123456,
+ },
+ 'placementCode': 'div-gpt-dummy-placement-code',
+ 'sizes': [
+ [300, 250]
+ ],
+ 'bidId': 'bidId2',
+ 'bidderRequestId': 'bidderRequestId',
+ 'auctionId': 'auctionId-56a2-4f71-9098-720a68f2f708'
+ }],
+ 'start': 1487883186070,
+ 'auctionStart': 1487883186069,
+ 'timeout': 3000
+};
+
+const RESPONSE = {
+ 'headers': null,
+ 'body': {
+ 'id': 'responseId',
+ 'seatbid': [
+ {
+ 'bid': [
+ {
+ 'id': 'bidId1',
+ 'impid': 'bidId1',
+ 'price': 0.18,
+ 'adm': '',
+ 'adid': '144762342',
+ 'adomain': [
+ 'http://dummydomain.com'
+ ],
+ 'iurl': 'iurl',
+ 'cid': '109',
+ 'crid': 'creativeId',
+ 'cat': [],
+ 'w': 300,
+ 'h': 250,
+ 'ext': {
+ 'prebid': {
+ 'type': 'banner'
+ },
+ 'bidder': {
+ 'appnexus': {
+ 'brand_id': 334553,
+ 'auction_id': 514667951122925701,
+ 'bidder_id': 2,
+ 'bid_ad_type': 0
+ }
+ }
+ }
+ },
+ {
+ 'id': 'bidId2',
+ 'impid': 'bidId2',
+ 'price': 0.1,
+ 'adm': '',
+ 'adid': '144762342',
+ 'adomain': [
+ 'http://dummydomain.com'
+ ],
+ 'iurl': 'iurl',
+ 'cid': '109',
+ 'crid': 'creativeId',
+ 'cat': [],
+ 'w': 300,
+ 'h': 250,
+ 'ext': {
+ 'prebid': {
+ 'type': 'banner'
+ },
+ 'bidder': {
+ 'appnexus': {
+ 'brand_id': 386046,
+ 'auction_id': 517067951122925501,
+ 'bidder_id': 2,
+ 'bid_ad_type': 0
+ }
+ }
+ }
+ }
+ ],
+ 'seat': 'seat'
+ }
+ ],
+ 'ext': {
+ 'usersync': {
+ 'sovrn': {
+ 'status': 'none',
+ 'syncs': [
+ {
+ 'url': 'urlsovrn',
+ 'type': 'iframe'
+ }
+ ]
+ },
+ 'appnexus': {
+ 'status': 'none',
+ 'syncs': [
+ {
+ 'url': 'urlappnexus',
+ 'type': 'pixel'
+ }
+ ]
+ }
+ },
+ 'responsetimemillis': {
+ 'appnexus': 127
+ }
+ }
+ }
+};
+
+describe('Padsquad bid adapter', function () {
+ describe('isBidRequestValid', function () {
+ it('should accept request if only unitId is passed', function () {
+ let bid = {
+ bidder: 'padsquad',
+ params: {
+ unitId: 'unitId',
+ }
+ };
+ expect(spec.isBidRequestValid(bid)).to.equal(true);
+ });
+ it('should accept request if only networkId is passed', function () {
+ let bid = {
+ bidder: 'padsquad',
+ params: {
+ networkId: 'networkId',
+ }
+ };
+ expect(spec.isBidRequestValid(bid)).to.equal(true);
+ });
+ it('should accept request if only publisherId is passed', function () {
+ let bid = {
+ bidder: 'padsquad',
+ params: {
+ publisherId: 'publisherId',
+ }
+ };
+ expect(spec.isBidRequestValid(bid)).to.equal(true);
+ });
+
+ it('reject requests without params', function () {
+ let bid = {
+ bidder: 'padsquad',
+ params: {}
+ };
+ expect(spec.isBidRequestValid(bid)).to.equal(false);
+ });
+ });
+
+ describe('buildRequests', function () {
+ it('creates request data', function () {
+ let request = spec.buildRequests(REQUEST.bidRequest, REQUEST);
+
+ expect(request).to.exist.and.to.be.a('object');
+ const payload = JSON.parse(request.data);
+ expect(payload.imp[0]).to.have.property('id', REQUEST.bidRequest[0].bidId);
+ expect(payload.imp[1]).to.have.property('id', REQUEST.bidRequest[1].bidId);
+ });
+
+ it('has gdpr data if applicable', function () {
+ const req = Object.assign({}, REQUEST, {
+ gdprConsent: {
+ consentString: 'consentString',
+ gdprApplies: true,
+ }
+ });
+ let request = spec.buildRequests(REQUEST.bidRequest, req);
+
+ const payload = JSON.parse(request.data);
+ expect(payload.user.ext).to.have.property('consent', req.gdprConsent.consentString);
+ expect(payload.regs.ext).to.have.property('gdpr', 1);
+ });
+ });
+
+ describe('interpretResponse', function () {
+ it('have bids', function () {
+ let bids = spec.interpretResponse(RESPONSE, REQUEST);
+ expect(bids).to.be.an('array').that.is.not.empty;
+ validateBidOnIndex(0);
+ validateBidOnIndex(1);
+
+ function validateBidOnIndex(index) {
+ expect(bids[index]).to.have.property('currency', 'USD');
+ expect(bids[index]).to.have.property('requestId', RESPONSE.body.seatbid[0].bid[index].impid);
+ expect(bids[index]).to.have.property('cpm', RESPONSE.body.seatbid[0].bid[index].price);
+ expect(bids[index]).to.have.property('width', RESPONSE.body.seatbid[0].bid[index].w);
+ expect(bids[index]).to.have.property('height', RESPONSE.body.seatbid[0].bid[index].h);
+ expect(bids[index]).to.have.property('ad', RESPONSE.body.seatbid[0].bid[index].adm);
+ expect(bids[index]).to.have.property('creativeId', RESPONSE.body.seatbid[0].bid[index].crid);
+ expect(bids[index]).to.have.property('ttl', 30);
+ expect(bids[index]).to.have.property('netRevenue', true);
+ }
+ });
+
+ it('handles empty response', function () {
+ const EMPTY_RESP = Object.assign({}, RESPONSE, {'body': {}});
+ const bids = spec.interpretResponse(EMPTY_RESP, REQUEST);
+
+ expect(bids).to.be.empty;
+ });
+ });
+
+ describe('getUserSyncs', function () {
+ it('handles no parameters', function () {
+ let opts = spec.getUserSyncs({});
+ expect(opts).to.be.an('array').that.is.empty;
+ });
+ it('returns non if sync is not allowed', function () {
+ let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false});
+
+ expect(opts).to.be.an('array').that.is.empty;
+ });
+
+ it('iframe sync enabled should return results', function () {
+ let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [RESPONSE]);
+
+ expect(opts.length).to.equal(1);
+ expect(opts[0].type).to.equal('iframe');
+ expect(opts[0].url).to.equal(RESPONSE.body.ext.usersync['sovrn'].syncs[0].url);
+ });
+
+ it('pixel sync enabled should return results', function () {
+ let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [RESPONSE]);
+
+ expect(opts.length).to.equal(1);
+ expect(opts[0].type).to.equal('image');
+ expect(opts[0].url).to.equal(RESPONSE.body.ext.usersync['appnexus'].syncs[0].url);
+ });
+
+ it('all sync enabled should return all results', function () {
+ let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [RESPONSE]);
+
+ expect(opts.length).to.equal(2);
+ });
+ });
+});