Skip to content

Epom Ad Server Bid Adapter: new adapter - #15505

Open
ruin wants to merge 10 commits into
prebid:masterfrom
epom:epom_as-bid-adapter
Open

Epom Ad Server Bid Adapter: new adapter#15505
ruin wants to merge 10 commits into
prebid:masterfrom
epom:epom_as-bid-adapter

Conversation

@ruin

@ruin ruin commented Aug 20, 2026

Copy link
Copy Markdown

Type of change

  • New bidder adapter

Description of change

New bid adapter for Epom Ad Server (epom_as), built on ortbConverter, banner only.

Epom Ad Server is white-label: each network runs its own deployment on its own domain, so the
serving host is supplied per ad unit via params.host and the adapter posts to
https://{host}/hb/bid. Only the host is configurable — the request path is fixed by the adapter,
so a page configuration cannot redirect the auction payload to an arbitrary URL. A page may mix
several deployments; the adapter groups impressions by host and sends one request to each. The host
pattern is byte-identical to the expression Prebid Server validates the same parameter with.

GVL vendor id 849, with the device-storage disclosure published on that vendor list entry. The
adapter itself uses no storage manager and writes nothing; the identity cookie is set by the ad
server on its own domain and reaches the auction only because the POST is credentialed.

Maintainer: support@epom.com

Test parameters for validating bids:

{
  bidder: 'epom_as',
  params: {
    host: 'aj2494.online',
    placementKey: '63bad7a99f270394e7b4b370952cbff2'
  }
}

Verified against the Hello World sample page: the placement bids 1.23 USD on a 300x250 slot and
renders. A request carrying several impressions is answered with a bid per impression; an unknown
placement key and a size the placement does not carry both return a no-fill.

Tests: 57, with 100% line, function and branch coverage of the module.

Other information

Documentation PR: prebid/prebid.github.io#6714
A Prebid Server adapter for the same bidder is submitted alongside this one.

One known gap, in the ad server rather than in the adapter: meta.advertiserDomains is populated
from bid.adomain, which the serving deployment behind the test parameters does not send yet. The
advertiser-domain field ships in the ad server's next release; the adapter needs no change for it.

epomrnd and others added 9 commits August 20, 2026 11:52
Bid adapter for the sell side of the Epom platform, where a publisher's
own direct-sold campaigns are booked. Distinct from the existing epom_dsp
adapter, which is the buy side.

Epom Ad Server is white-label, so the serving host arrives per bid in
params.host and the adapter groups impressions by host, emitting one
request per host with every impression addressed to it. Keeping a host's
impressions in one auction is a requirement rather than an optimisation:
the ad server resolves a page as a unit, so its roadblock and
one-campaign-per-page rules only hold when all slots are decided together.

Banner only, built on ortbConverter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Array.from over the map builds the requests directly, so the accumulator
and the `as unknown as` cast around the empty group both go away.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two Epom concepts the standard ORTB request cannot express. Channel is a
publisher traffic-slice label that drives targeting and reporting, and it
has no ORTB analogue of any kind, so it goes under our own imp.ext
namespace. Custom parameters go to imp.ext.data instead, because that is
where first-party data and RTD modules already contribute, and the ad
server should have one place to read targetable key-values from.

Both are sanitised to the limits the ad server enforces on ingest -
scalars only, 32 keys, 128-byte keys, 512-byte values - so a publisher
sees the same set accepted here and applied there rather than discovering
the difference in a report.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two adapters are meant to take byte-identical params, but the Prebid
Server JSON schema allowed host:port while the Prebid.js pattern did not.
A port cannot redirect the request: scheme, path, query and userinfo stay
excluded, which is the property that matters.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The endpoint echoes the request Origin and sets Allow-Credentials, so a
first-party Epom identity on the ad-server domain can reach the auction
instead of every request looking like a new user.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The adapter told Prebid a bid was good for 300 s while the ad server only
accepts its impression beacon within 30 s of the bid, so a cached bid
rendered a minute later counted for nothing - Prebid documents exactly
this: an ad rendered after the TTL may not be attributed.

25 s leaves five for the beacon to travel. A deployment that widens its
window can say so per bid in bid.exp, which the converter already prefers
over this default, so nothing here has to change for it.

Also fixes a test title that still said "without credentials".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comment stated the beacon is accepted within 30 s, which is one deployment's
configuration rather than anything about the protocol — and a deployment widening
that window makes the comment wrong without touching the code. The constant is
the floor; a server with a wider window says so per bid in bid.exp, which is what
the sentence now says.
The block used ads.example.com, which does not resolve, so the one thing a
reviewer is asked to do — load the ad unit and confirm a bid comes back — could
not be done. The first block is now a live placement kept filled for this; the
fictional hosts stay in the second, where they illustrate grouping by deployment
rather than something to run.
…blisher's parameters

The host pattern is now byte-identical to the expression Prebid Server validates the same parameter
with. The previous one was strictly narrower — it demanded two labels, so an internal deployment
reachable as a single-label host was refused here and accepted there — while a comment claimed the
two matched. The comment is true now.

Custom parameters are no longer capped in the adapter. The caps mirrored an ad-server ingest limit,
but enforcing them here dropped entries silently, so a publisher whose targeting set was too large
saw half of it never arrive with nothing to indicate why. What the adapter does check is the shape:
a nested object or an array stringifies to something no campaign can match, so it is a
misconfiguration and the bid is refused outright, which is what the server's schema does with the
same input. The ingest limits remain the ad server's own and are documented as such.

bidFloor applies only as a positive finite number, and a negative one is refused to match the
schema's minimum. The request options block is gone: it set exactly the content type and credentials
core already applies to every POST, and the assertions that covered it were testing core.

The module declares its device-storage disclosure at the URL published on GVL vendor 849. The field
is not derived from the vendor list when omitted — it is simply recorded as null — so a vendor that
declares cookie use has to name it.

Tests cover what a misconfiguration actually looks like: every rejection path, a response body that
is not ORTB at all, and the floor precedence in both directions.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9d7cbd967f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread modules/epom_asBidAdapter.ts Outdated
* Single-label hosts are deliberately allowed: an internal deployment reachable as `api-us`
* is a legitimate configuration on both transports.
*/
const HOST_PATTERN = /^[a-zA-Z0-9.-]+(:[0-9]+)?$/;

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 Reject malformed hostname and port values

When params.host is syntactically invalid but matches this expression—for example ads.example.com:99999, ., or a..bisBidRequestValid accepts it and buildRequests constructs an unusable HTTPS URL. Core's Request construction can then throw before sending, causing the Epom adapter to complete without bids for that request. Validate hostname labels and the port range, or parse and verify the constructed URL before accepting the bid.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Taken in part, and thank you — the port case is real.

ads.example.com:99999 makes new URL() throw rather than return something unusable, and core does not call buildRequests inside a try (bidderFactory.ts:436), so one mistyped port in a page's configuration would take the whole request down rather than cost a single bid. Fixed in 479e28e: the port is range-checked to 1-65535 on top of the existing shape, with boundary tests for :0, :65536, :99999 and :65535.

I left the hostname labels alone. . and a..b both construct a valid URL and simply fail to resolve, which is an ordinary no-fill rather than a thrown request. The expression itself is the one Prebid Server validates this same parameter with (util/urlutil/security.go), and its purpose there is to keep URL control characters out of a host rather than to check that a host resolves — tightening it here would put the two transports back out of step, and would reject single-label hosts, which are a legitimate deployment shape for this white-label ad server.

The host shape alone accepted a port outside 1-65535. `new URL()` throws on one rather than
returning something unusable, and core does not call buildRequests inside a try, so a single
mistyped port in a page's configuration took the whole request down instead of costing one bid.

The range is checked on top of the same expression Prebid Server validates this parameter with,
which is deliberately about keeping URL control characters out of a host rather than about
resolving one — hostname labels are left alone, so a single-label internal deployment still works
and the two transports still accept the same inputs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants