Skip to content

New Adapter: Panxo - #4723

Open
css12s wants to merge 5 commits into
prebid:masterfrom
css12s:add-panxo-adapter
Open

css12s wants to merge 5 commits into
prebid:masterfrom
css12s:add-panxo-adapter

Conversation

@css12s

@css12s css12s commented Mar 20, 2026 •

Copy link
Copy Markdown

Adds the Panxo OpenRTB 2.6 adapter for banner, video and native inventory.

This update addresses review feedback:

  • Group impressions by propertyKey and escape routing keys.
  • Use jsonutil and standard HTTP status handling.
  • Validate response media types instead of treating every bid as banner.
  • Convert impression and deal floors to USD without modifying the input auction.
  • Remove legacy identity fields and internal impression wrappers.
  • Add error, currency and media fixtures.

Adapter tests and the exchange QA check pass locally. Endpoint gzip support is not declared. Private-deal fulfillment is not supported; app/CTV fixtures are protocol tests only.

PBS uses standard OpenRTB floors. The JS adapter's deprecated floor parameter remains a compatibility point for maintainer review.

Docs: prebid/prebid.github.io#6494
Maintainer: prebid@panxo.ai

JavaScript update: prebid/Prebid.js#15875

Panxo detects and monetizes AI-referred traffic (ChatGPT, Perplexity,
Claude, Gemini) with premium native ad formats. This adapter enables
server-side bidding through Prebid Server.

- Banner media type support (site only, no app)
- Property key based endpoint routing
- GVL ID 1527 for TCF 2.2 compliance
- GPP support
- Redirect-based user sync
- Full JSON test coverage (exemplary + supplemental)

Owner: Panxo Stacks, Inc.
@github-actions

Copy link
Copy Markdown

Code coverage summary

Note:

  • Prebid team doesn't anticipate tests covering code paths that might result in marshal and unmarshal errors
  • Coverage summary encompasses all commits leading up to the latest one, 179939a

panxo

Refer here for heat map coverage report

github.com/prebid/prebid-server/v4/adapters/panxo/panxo.go:20:	Builder		100.0%
github.com/prebid/prebid-server/v4/adapters/panxo/panxo.go:27:	MakeRequests	57.1%
github.com/prebid/prebid-server/v4/adapters/panxo/panxo.go:95:	MakeBids	78.6%
total:								(statements)	65.9%

css12s pushed a commit to PanxoAI/prebid-server-adapter that referenced this pull request Mar 20, 2026
Adapter files, test fixtures, and installation instructions for adding
Panxo server-side bidding to an existing Prebid Server build.

Pending official Prebid Server review: prebid/prebid-server#4723
@bsardo bsardo added the adapter label Mar 31, 2026
@css12s

css12s commented Jun 2, 2026

Copy link
Copy Markdown
Author

@bsardo Can you merge when u can? Thanks

@github-actions

Copy link
Copy Markdown

Code coverage summary

Note:

  • Prebid team doesn't anticipate tests covering code paths that might result in marshal and unmarshal errors
  • Coverage summary encompasses all commits leading up to the latest one, d0d75f2

panxo

Refer here for heat map coverage report

github.com/prebid/prebid-server/v4/adapters/panxo/panxo.go:20:	Builder		100.0%
github.com/prebid/prebid-server/v4/adapters/panxo/panxo.go:27:	MakeRequests	57.1%
github.com/prebid/prebid-server/v4/adapters/panxo/panxo.go:95:	MakeBids	78.6%
total:								(statements)	65.9%

@postindustria-code

Copy link
Copy Markdown
Contributor

Open and link a docs PR. A prebid.github.io documentation PR (params table + metadata) is required before a new adapter can merge, and I don't see one linked.

"We will not merge your bid adapter until you've at least opened a documentation PR and comment with a link to it." — [Prebid Server · Adding a New Bidder (Go)](https://docs.prebid.org/prebid-server/developers/add-new-bidder-go.html)

Comment thread adapters/panxo/panxo.go Outdated
}
}

reqJSON, err := json.Marshal(request)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Use jsonutil.Marshal instead of encoding/json (panxo.go:75). The encoding/json import is only used for this one call, so switch to jsonutil.Marshal(request) and drop the import.

"Adapters must use jsonutil.Marshal and jsonutil.Unmarshal instead of the Go standard package functions, as those are not optimized for Prebid Server's high performance needs." —
[Prebid Server · Adding a New Bidder (Go)](https://docs.prebid.org/prebid-server/developers/add-new-bidder-go.html)

Comment thread adapters/panxo/panxo.go Outdated
Comment on lines +96 to +110
if response.StatusCode == http.StatusNoContent {
return nil, nil
}

if response.StatusCode == http.StatusBadRequest {
return nil, []error{&errortypes.BadInput{
Message: fmt.Sprintf("unexpected status code: %d", response.StatusCode),
}}
}

if response.StatusCode != http.StatusOK {
return nil, []error{&errortypes.BadServerResponse{
Message: fmt.Sprintf("unexpected status code: %d", response.StatusCode),
}}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reuse the response-status helpers (panxo.go:96-110). Your manual 204/400/non-200 handling is correct, but adapters.IsResponseStatusCodeNoContent(response) and adapters.CheckResponseStatusCodeForErrors(response) do exactly this — worth using them instead of hand-rolling.

Comment thread adapters/panxo/panxo.go Outdated
}}
}

if bidderExt.PropertyKey == "" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

propertyKey goes into the endpoint URL via fmt.Sprintf without escaping (panxo.go:80); a value with &/space would break the query. Wrap it in url.QueryEscape (or build with url.Values), or add a pattern/maxLength to the schema.

  • The propertyKey == "" check (:52) is already guaranteed by the schema (required + minLength:1), so it's effectively dead — safe to drop (keep the cross-imp consistency loop, that one's useful).
  • The consistency loop continues on a later imp's imp.ext parse error (:62-67), silently skipping the check for that imp while still sending it — consider erroring for consistency with imp[0].
  • Media type is hardcoded to banner (:123) — fine for a banner-only adapter, just noting for the future.
  • Consider supplementals for a 400/non-200 response, a malformed JSON body, and the multi-imp propertyKey-mismatch path.
  • endpointCompression isn't set — does the endpoint support gzip? And the sync userMacro is empty with no UID macro in the URL — is that intended?

@postindustria-code

Copy link
Copy Markdown
Contributor

Sent the verification email, got the next error:
Screenshot 2026-07-24 at 12 21 19

@postindustria-code

Copy link
Copy Markdown
Contributor

$ curl -i --location --request POST
'https://panxo-sys.com/openrtb/2.5/bid?key=test&source=prebid-server'
--header 'Content-Type: application/json;charset=utf-8'
--header 'Accept: application/json'
--data @request.json

HTTP/2 204
date: Fri, 24 Jul 2026 09:18:10 GMT
cache-control: no-store
x-panxo-worker-version: d72b8f12-cfd1-46f3-8068-f5e5b395ad15
server: cloudflare

→ HTTP 204 No Content (~278ms, empty body) — endpoint reachable; no real bid returned, as expected for a test request.

- Use jsonutil.Marshal instead of encoding/json
- Use adapters.IsResponseStatusCodeNoContent and
  adapters.CheckResponseStatusCodeForErrors in MakeBids
- URL-escape propertyKey when building the endpoint query
- Error instead of silently skipping imps whose ext fails to parse
- Drop the propertyKey empty check already guaranteed by the schema
- Set the usersync userMacro so the redirect carries the Panxo uid
- Send the TC string as gdpr_consent in the sync URL, matching what
  the sync endpoint reads
- Add supplementals: 400, 500, malformed body, multi-imp key mismatch,
  invalid imp.ext (first and later imp), no impressions,
  propertyKey escaping
@css12s

css12s commented Jul 29, 2026

Copy link
Copy Markdown
Author

Thanks for the thorough review @postindustria-code. Everything is addressed in the commit above.

Docs PR: prebid/prebid.github.io#6494

It was opened on 20 March, the same day as this PR, but never linked here, which is why you could not find it. @muuki88 has already reviewed it and confirmed it merges once this one does.

Changes

  • jsonutil.Marshal replaces encoding/json, and the import is gone.
  • MakeBids now uses adapters.IsResponseStatusCodeNoContent and adapters.CheckResponseStatusCodeForErrors instead of the hand-rolled status handling.
  • propertyKey is now escaped with url.QueryEscape. Good catch, and it was worse than a broken query: an unescaped & let a caller inject query params, so a key of abc&source=evil produced ?key=abc&source=evil&source=prebid-server. Locked down by property-key-escaping.json.
  • Dropped the propertyKey == "" check, redundant with required plus minLength: 1 in the schema.
  • The cross-imp consistency loop now returns an error when a later imp's ext fails to parse, instead of continue-ing and sending that imp anyway. Consistent with how imp[0] is handled.

New supplementals

status-400, status-500, malformed-response-body, mismatched-property-keys, invalid-imp-ext, invalid-imp-ext-later-imp, invalid-imp-ext-object-later-imp, no-impressions, property-key-escaping.

Adapter coverage goes from 65.9% to 97.5% (MakeBids 100%, MakeRequests 96.2%). The only statement left uncovered is the request marshal error path, which per the coverage bot note is not expected to be tested.

Your two questions

  • endpointCompression: left unset deliberately. The endpoint does not decompress gzipped request bodies today, so declaring GZIP would break the integration. We will submit it separately once the endpoint supports it.
  • userMacro: you were right, empty was wrong. It is now {PANXO_UID}, which our sync endpoint substitutes with the user id before redirecting to {{.RedirectURL}}.

While double-checking the sync flow against our endpoint we also found the sync URL was passing the TC string as consent=, while the endpoint reads gdpr_consent=. Since the endpoint fails closed when GDPR applies and no TC string is present, EU syncs would have been dropped. Fixed in the same commit.

Banner-only is intentional, it is the only format we serve today.

On the maintainer address: prebid@panxo.ai is receiving mail correctly now, so a re-send of the verification email should go through.

@postindustria-code

Copy link
Copy Markdown
Contributor

@css12s, I sent an email for verification from the following address: marina.lebedeva@postindustria.com

@postindustria-code

Copy link
Copy Markdown
Contributor

@css12s, do not forget upgrade your branch:
This branch is out-of-date with the base branch

@postindustria-code

Copy link
Copy Markdown
Contributor

@css12s, I sent an email for verification from the following address: marina.lebedeva@postindustria.com

The answer has been received.

image

@css12s

css12s commented Aug 4, 2026

Copy link
Copy Markdown
Author

@postindustria-code Seams merging is still blocked "At least 2 approving reviews are required by reviewers with write access."

@postindustria-code

Copy link
Copy Markdown
Contributor

@przemkaczmarek, @linux019
Good day! Perhaps you have some free time and you can take this PR for review?

@przemkaczmarek przemkaczmarek self-assigned this Aug 4, 2026
Comment thread static/bidder-info/panxo.yaml Outdated
@@ -0,0 +1,15 @@
endpoint: "https://panxo-sys.com/openrtb/2.5/bid"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

works

endpoint: "https://panxo-sys.com/openrtb/2.5/bid"
maintainer:
email: "prebid@panxo.ai"
gvlVendorID: 1527

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

veirfied:

Image

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Code coverage summary

Note:

  • Prebid team doesn't anticipate tests covering code paths that might result in marshal and unmarshal errors
  • Coverage summary encompasses all commits leading up to the latest one, d873273

panxo

Refer here for heat map coverage report

github.com/prebid/prebid-server/v4/adapters/panxo/panxo.go:20:	Builder		100.0%
github.com/prebid/prebid-server/v4/adapters/panxo/panxo.go:27:	MakeRequests	96.2%
github.com/prebid/prebid-server/v4/adapters/panxo/panxo.go:93:	MakeBids	100.0%
total:								(statements)	97.5%

Comment thread adapters/panxo/panxo.go Outdated
}
}

reqJSON, err := jsonutil.Marshal(request)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

imp.Ext is forwarded to Panxo's endpoint as-is, which means
imp.ext.bidder — a Prebid-internal wrapper — is sent to the SSP.
Panxo's server should not receive the bidder key.

Before marshaling, each imp's Ext should be cleaned up or replaced.
The minimal fix is to set imp.Ext = nil (or replace it with just
the raw bidder params) before sending:

requestCopy := *request
impsCopy := make([]openrtb2.Imp, len(request.Imp))
copy(impsCopy, request.Imp)
for i := range impsCopy {
impsCopy[i].Ext = nil
}
requestCopy.Imp = impsCopy
reqJSON, err := jsonutil.Marshal(&requestCopy)

The fixture simple-banner.json confirms the current (wrong) behavior:
expectedRequest.body.imp[0].ext.bidder is present in the outgoing
request, which means this is not just a fixture issue.

Comment thread adapters/panxo/panxo.go Outdated
for i := range sb.Bid {
bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
Bid: &sb.Bid[i],
BidType: openrtb_ext.BidTypeBanner,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

BidType is hardcoded to BidTypeBanner regardless of what mtype
the SSP returns on the bid. If Panxo ever returns a bid with a
different mtype (e.g. native), it will be silently mistyped as banner.

PBS convention is to determine bid type from bid.MType, with a
fallback to impression media type. At minimum, please return an error
when the mtype is unexpected rather than silently coercing it:

bidType, err := getMediaTypeForBid(bid)
if err != nil {
errs = append(errs, err)
continue
}

Since the bidder-info YAML only declares banner, this may be
intentional — but it should be documented with a comment explaining
why mtype is not checked.

Comment thread adapters/panxo/panxo.go Outdated
endpoint string
}

func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Unused parameters should be named _ per PBS convention:

func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error)

func (a *adapter) MakeRequests(request *openrtb2.BidRequest, _ *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error)

func (a *adapter) MakeBids(_ *openrtb2.BidRequest, _ *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error)

Comment thread adapters/panxo/panxo.go Outdated
}

func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
var errs []error

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

errs is declared at the top of MakeRequests but is always nil at
the point it is returned (return [...], errs). The only other use
is return nil, append(errs, err) in the marshal error branch, where
it is equivalent to return nil, []error{err}.

Remove the var errs []error declaration and use nil / []error{err}
directly to avoid misleading readers into thinking errors accumulate here.

Comment thread static/bidder-info/panxo.yaml Outdated
@@ -0,0 +1,15 @@
endpoint: "https://panxo-sys.com/openrtb/2.5/bid"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The endpoint URL path is /openrtb/2.5/bid but openrtb.version: 2.6
is declared. These are inconsistent — the path suggests a 2.5 API
while the declaration tells PBS to pass 2.6-specific fields through.

If Panxo's endpoint accepts OpenRTB 2.6 requests (the path is just a
label), please confirm. If it is truly a 2.5 endpoint, remove the
version: 2.6 line to avoid unexpected field forwarding.

@@ -0,0 +1,124 @@
{

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The supplemental fixtures cover error paths thoroughly. However, the
primary validation logic — that all imps must share the same propertyKey
— has no happy-path test exercising it. Please add:

exemplary/multi-imp.json — two imps with the same propertyKey,
verifying that both are forwarded in a single request and both bids
are returned.

Without this fixture, the validation loop in MakeRequests (lines 52-71)
is only exercised by the error case (mismatched-property-keys.json).

openrtb:
version: 2.6
gpp-supported: true
capabilities:

@linux019 linux019 Aug 14, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

please check if you support gzip and add

endpointCompression: gzip

@github-actions

Copy link
Copy Markdown

Code coverage summary

Note:

  • Prebid team doesn't anticipate tests covering code paths that might result in marshal and unmarshal errors
  • Coverage summary encompasses all commits leading up to the latest one, a56af25

panxo

Refer here for heat map coverage report

github.com/prebid/prebid-server/v4/adapters/panxo/panxo.go:23:	Builder				100.0%
github.com/prebid/prebid-server/v4/adapters/panxo/panxo.go:30:	MakeRequests			98.3%
github.com/prebid/prebid-server/v4/adapters/panxo/panxo.go:137:	impressionWithUSDFloors		100.0%
github.com/prebid/prebid-server/v4/adapters/panxo/panxo.go:163:	floorInUSD			100.0%
github.com/prebid/prebid-server/v4/adapters/panxo/panxo.go:183:	inventoryExtWithPanxoSignal	95.2%
github.com/prebid/prebid-server/v4/adapters/panxo/panxo.go:219:	consentOnlyUser			100.0%
github.com/prebid/prebid-server/v4/adapters/panxo/panxo.go:241:	withoutDeviceIdentity		100.0%
github.com/prebid/prebid-server/v4/adapters/panxo/panxo.go:265:	propertyKeyFromImpExt		100.0%
github.com/prebid/prebid-server/v4/adapters/panxo/panxo.go:285:	MakeBids			100.0%
github.com/prebid/prebid-server/v4/adapters/panxo/panxo.go:355:	getBidType			100.0%
total:								(statements)			99.0%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants