Epom Ad Server Bid Adapter: new adapter - #15505
Conversation
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.
There was a problem hiding this comment.
💡 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".
| * 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]+)?$/; |
There was a problem hiding this comment.
Reject malformed hostname and port values
When params.host is syntactically invalid but matches this expression—for example ads.example.com:99999, ., or a..b—isBidRequestValid 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 👍 / 👎.
There was a problem hiding this comment.
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.
Type of change
Description of change
New bid adapter for Epom Ad Server (
epom_as), built onortbConverter, 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.hostand the adapter posts tohttps://{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:
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.advertiserDomainsis populatedfrom
bid.adomain, which the serving deployment behind the test parameters does not send yet. Theadvertiser-domain field ships in the ad server's next release; the adapter needs no change for it.