Blog · Protocol

Prebid vs Amazon TAM vs Google Open Bidding lines up three assemblies of one impression.

People search Prebid vs Amazon TAM vs Google Open Bidding because a publisher runs all three on one slot and wants to know which path pays. The fee tables are already published. The bid request is not the same object on the way out, and a field you set in Prebid does not ride along on the other two.

The comparison is who builds the request

Prebid.js runs in the browser. The publisher page calls the auction, bidder adapters build requests, and those requests can be OpenRTB. First party data is an ortb2 object, plus ortb2Imp on the ad unit, merged into what the adapter sends. If you put source.schain, source.tid, user.eids, regs.gpp, device.sua, or imp.video.plcmt there, a client-side bidder can see them because you wrote them into the merge.

Amazon Transparent Ad Marketplace does not hand you that object. The page calls apstag, Amazon runs the auction on its servers, and a bid comes back to the page for the ad server. The OpenRTB Amazon sends onward to demand is Amazon's assembly. Your controls are the slot, the sizes, the publisher id, and the line item that result becomes. There is no ortb2 document on the page that Amazon is obliged to forward field for field.

Google Open Bidding, the product that used to be called Exchange Bidding, runs inside Google Ad Manager. At ad server time Google calls the yield partners server to server and brings them into the unified auction. The publisher configures a yield group. Google builds the request those partners see. The page does not hold the JSON.

PathWho assembles the OpenRTBWhat you can edit
Prebid.jsThe page, then each bidder adapterortb2 and ortb2Imp on the ad unit. The JSON the adapter sends is that merge.
Prebid ServerYour PBS, from a stored request plus the client OpenRTBThe stored request and whatever the browser actually forwarded. Adapter ext on the client is not this object.
Amazon TAMAmazon, after apstag returns a bid to the pageSlot, sizes, pub id, and the bid that lands in the ad server. Not an ortb2 document.
Google Open BiddingGoogle, inside Ad Manager, to each yield partnerThe yield group and what the Google tag collected. Not an ortb2 document.

A Prebid debug log is evidence about Prebid

The usual QA is a Prebid debug console on one page view. source.schain is populated, regs.gpp matches the CMP, device.sua is present, video.plcmt replaced video.placement. That log is the client-side bidder request. It is not the request Amazon sent, and it is not the request Google sent to a yield partner for the same impression.

Buyers who later reject the domain for a missing chain, or for a US Privacy string where they expected GPP, may be looking at a different path than the one you screenshotted. The impression can clear on Prebid with a complete chain and clear on Open Bidding with whatever Google assembled. Both are the same ad unit in the ad server report. They are not the same payload.

The fields that matter for that argument are the ones a buyer actually filters on: source.schain and whether complete is 1, user.eids, regs.gpp plus regs.gpp_sid against regs.us_privacy, device.sua against a frozen device.ua, and imp.video.plcmt against a legacy placement. Set them where you own the JSON. Then capture the request on each other path before you tell a buyer the slot "has schain."

Prebid Server is a second assembly, even inside Prebid

Prebid.js and Prebid Server are not one request with the transport swapped. The browser sends an auction to your Prebid Server. PBS merges that OpenRTB with a stored request and calls bidders from the server. A key a client adapter hung on imp.ext for its own bidder does not become a field on a different server-side bidder's OpenRTB. The stored request is the other half of the merge. A source.schain that exists only in the stored request is what the bidder sees when the client omits source. A client that sends a source object is now part of that merge, and you have to know which copy wins for the fields you care about.

Timeouts shrink on the way. The browser auction has its own limit. By the time PBS calls a bidder, part of that budget is gone, and the tmax on the server-side request is the time PBS still has, not the number you configured on the page. A bidder that looks slow only on the server path may be seeing a shorter tmax than the client adapter on the same slot. Floors and currency can also be rewritten before the bidder sees them. The ad unit's floor in the page config is not automatically imp.bidfloor plus bidfloorcur on every server hop.

Hybrid pages make this worse. Some bidders stay client-side and receive the ortb2 merge. Some are marked server-side and receive the PBS merge. A publisher who "set GPP in Prebid" set it for the path whose merge includes it. The other bidder in the same auction can be on the other merge. Debug one bidder and you have not debugged the auction.

The same SSP on two paths is two requests

Mature stacks run Prebid and Open Bidding together, and often TAM as well. The mistake that shows up in the ad server is the same SSP enabled as a Prebid bidder and as an Open Bidding yield partner. That SSP receives two requests for one impression. It may bid both. If the Open Bidding response wins, the impression clears through Google's path, including Google's fee, on demand you might already have reached from Prebid without that hop.

Deduping those bids is an ad server problem: line item priority, price granularity, and whether you meant to prefer one path. It does not make the two OpenRTB bodies identical. One of them went out from the page or from your PBS, with your ortb2. The other went out from Google, with the yield group's request. Turning the yield partner off, or turning the Prebid bidder off, is how you stop paying twice for one look. It is also how you stop pretending a single debug log describes both.

TAM beside Prebid is the same shape. Amazon's bid returns to the page as something the ad server can rank against Prebid's bid. The request Amazon already sent to its demand is not editable at that moment. If a deal or a floor existed only in the Prebid ad unit, Amazon's callers did not see that ad unit.

tid is three scopes, and only the path you built will share them

Auction dedup and log join depend on which id you stored. source.tid is the transaction. imp.ext.tid is the impression inside it. A page-level refresh or a second path's auction will not reuse them unless that path was given the same values. Prebid can set both from ortb2 and ortb2Imp. A TAM bid and an Open Bidding bid for the same slot arrive with the identifiers those platforms put on their own requests. Joining all three in a log on source.tid fails open: the rows do not share a tid, so they look like three auctions, which they are.

source.schain has the same problem. A complete chain on the Prebid request means the nodes you, or your wrapper, wrote. It does not mean the Open Bidding request's source.schain lists the same nodes, or that TAM forwarded the chain you would have written. Supply-path QA that crawls ads.txt and sellers.json still has to be pointed at the chain that was actually on that request. The file on the publisher host does not change per path. The schain object does.

Privacy strings split the same way. A CMP that wrote regs.gpp into Prebid's ortb2 did not automatically write it into Google's yield-partner request or into Amazon's. A US Privacy string left in regs.us_privacy on one path, and a GPP string on another, is two different signals for one user. Buyers who drop one and keep the other are not contradicting themselves. They saw two requests.

The same integer is not the same video placement

video.placement and video.plcmt are both integers, and they do not share a table. A 1 on the old field is not a 1 on the new field. Prebid can send plcmt because you set it on ortb2Imp. Another path for the same slot can still be sending placement, or inferring a placement from the ad unit size, or sending both and letting them disagree. A buyer who filters instream on plcmt will keep one path and drop the other while the ad server report shows one line item.

The same trap sits on the flag fields. The OpenRTB specification types regs.gdpr, imp.secure, device.dnt, and the rest of that family as integers, 0 or 1. The IAB protobuf schema types those same fields as bool. A browser Prebid request is usually the integer JSON. A server-side hop that was built from the protobuf dialect sends true and false. A filter written as gdpr == 1 drops the bool path and keeps the integer path. Both requests were trying to say the call is in scope. The two JSON dialects are not a Prebid-only problem, and they show up the moment one of the three assemblies is not the page's JSON.

device.sua against a reduced device.ua splits the same way. Prebid can pass the structured user agent because the page collected client hints and wrote ortb2.device.sua. A path that only forwards the frozen user-agent string will classify the device from placeholders. Buyers who require sua are not rejecting your inventory. They are rejecting the assembly that never received it.

A floor and a deal on the Prebid ad unit are not on the other auctions

imp.bidfloor and imp.bidfloorcur are properties of the request that bidder sees. Prebid can set them on the ad unit that becomes the client request, and Prebid Server can rewrite them again before its own bidder call. Amazon's auction and Google's yield-partner call have their own floors. A bid that clears the Prebid floor can lose the Open Bidding auction for price, and the loss notice, if you get one, is 100, below the auction floor of the auction that actually ran. That floor is not the number in the Prebid config.

Deals are the same split. imp.pmp on the Prebid request lists deals you put on that ad unit. TAM and Open Bidding do not inherit it. A bidder that replies with a dealid the exchange has never been told about gets loss code 4, invalid deal id. A bidder that sees the deal only on the Prebid path and bids the open auction on the other paths is not under-bidding a deal. The deal was never in those requests. Loss code 101, below the deal floor, only makes sense on a path where that deal was actually offered.

The price the ad server ranks is not automatically the OpenRTB price. It has already been interpreted in some currency, and ${AUCTION_PRICE} later will be the clearing price after discount, in the bid's units, CPM. Comparing a Prebid CPM, a TAM CPM, and an Open Bidding CPM as three readings of one number skips cur, the auction type, and which assembly applied a floor. Pick the path after you have those, not after you have a fee card.

source.fd says who is still allowed to pick the winner

source.fd is who makes the final sale. 0 means the exchange that received this request. 1 means an upstream source: a header-bidding wrapper, another exchange, or an ad server that still mixes direct and programmatic demand. The field exists because header bidding is the case where the exchange on this hop does not control the sale. source.tid is how those hops are supposed to share an id. fd is how a bidder is supposed to know this hop is not the end.

A Prebid call that sits in front of Google Ad Manager is upstream of the final decision, so fd = 1 is the honest value: the ad server still ranks that bid against TAM and Open Bidding. Marking the Prebid request fd = 0 tells every bidder that this exchange already is the sale. They shade, and they spend, as if the ad server were not about to rank that bid against TAM and Open Bidding. The hop that really is the final sale is the one that can be fd = 0. Read the flag on each captured request. Copying one path's value onto the others inverts it on at least one of them.

Do that read before you describe the slot as one auction. A bidder that offers its full value only when fd is 0, and shades when fd is 1, will price the two hops differently even when the impression, the floor, and the fee are the same. That gap shows up in the ad server as a path preference. It is the flag.

schain and the consent string moved, and only some paths moved

In OpenRTB 2.6, source.schain is the SupplyChain object. The previous location was source.ext.schain. user.consent is the TCF string on the user object. It used to live under user.ext, the same move regs.gdpr made out of ext. A buyer who reads only the 2.6 field sees an empty chain and an empty consent on a path that is still sending the ext form, and passes a path that moved.

Prebid can be configured to send the core fields. A stored request written for 2.5, or a yield partner still on the ext object, will not match that capture. RTBlint reports the old schain path as openrtb.field.moved. The finding is per payload. It does not propagate to the other two assemblies of the impression. Run it on each request you saved, not on the Prebid debug object alone.

imp.exp is seconds from auction to impression. tmax is milliseconds to answer the bid. They are not substitutes, and they are not shared across paths. Prebid Server's tmax is the time it has left. Amazon and Google set the deadline on their own calls. A bidder that no-bids one path for timeout and bids another is answering two clocks. Loss code 2 later, on the path that did bid, means that path's impression window expired. It does not explain the path that never received a bid.

buyeruid is one bidder on one path, and the other paths do not have the sync

user.id is the exchange's identifier for the browser. user.buyeruid is the bidder's identifier, written from a cookie sync: the exchange looks up its own cookie and inserts that DSP's id before it sends the request. user.eids is the array of extended ids, each with a source and a uid. A DSP can frequency-cap and retarget on buyeruid. It cannot do that with user.id unless it built its own map.

Prebid's user-id modules run on the page and can fill eids, and sometimes buyeruid, on the request those bidders receive. That sync is Prebid's. Amazon's call and Google's yield-partner call do not execute those modules. The same DSP, on the same page view, can see a buyeruid from Prebid and an empty buyeruid from Open Bidding. It bids like it knows the user on one path and like the user is new on the others. The ad server then ranks a recognized bid against an anonymous one and calls the difference a price.

source.pchain is the older payment-chain string, still a field on source beside schain. A checker that only reads the SupplyChain object treats a pchain-only request as having no chain. One assembly can be on schain, one still on source.ext.schain, and one on pchain alone. All three can be authorized supply. Only one of them looks authorized to a reader of a single field.

imp.id is this auction, and tagid is the placement

imp.id is required, and bids copy it into impid. It is often just 1, 2, 3 for the slots in that request. It is not stable across refreshes, and it is not stable across paths. Joining Prebid, TAM, and Open Bidding on imp.id joins unrelated auctions that happened to number their first slot 1.

imp.tagid is the placement id the publisher means: the ad unit, the slot name. It joins the three paths only when each assembly was given the same string. Prebid's ad unit code does not become Amazon's slot name, and neither becomes the Ad Manager ad unit path, unless you set tagid yourself on each request. A log that almost matches on tagid, off by a prefix one platform added, is three placements to a buyer and one slot to you.

Without a shared tagid, the ids you do have are source.tid if you passed one, and the ad server's own auction id after the fact. The OpenRTB requests do not share an id just because they competed for the same line item. Capture tagid, imp.id, and source.tid per path and expect them to differ. The bug is a report that averages them into one request.

The floor currency defaults to USD, and secure omitted is not secure

imp.bidfloor is a minimum CPM. imp.bidfloorcur is the currency of that floor, and it defaults to USD. It does not inherit cur on the request. A floor of 120 that was meant as yen, sent without bidfloorcur of JPY, is a 120 USD floor, and it kills every bid on that path. Each deal in imp.pmp.deals has its own floor and its own currency, with the same USD default. Prebid can set one floor. Amazon and Google set theirs. A loss code 100 on one path is that path's floor in that path's currency, not the number in another system's config.

imp.secure is an integer. 1 means the impression requires HTTPS markup. 0 means non-secure is acceptable. Omission does not mean secure. If the field is absent, the secure state is unknown and non-secure HTTP can be assumed. Sending true instead of 1 is the bool dialect, and a strict reader drops it. A Prebid request built from an HTTPS page can send 1. A server-side assembly that never received the flag can omit it. Buyers who require secure impressions keep one path and reject the other. Loss code 207, creative not secure, is the creative loading HTTP into an HTTPS impression. It only happens on the path that actually said secure was 1.

A request must not contain more than one of site, app, and dooh. They answer the same question, and many bidders drop a payload that has two. That payload shows up when a stored request still carries site and the live call adds app, or the reverse. The other assemblies of the same impression may be a clean site or a clean app. One path no-bids as malformed. The ad server still fills from the path that was only one object. The fix is to look at the JSON that left, not at the ad unit form that produced it.

at is per request, so the three CPMs are not one kind of price

at is the auction type on that request. 1 is first price: the winner pays their own bid. 2 is second price: the winner pays just above the next bid. OpenRTB has no field for bid shading. The model lives in the bidder and learns from lurl, especially ${AUCTION_MIN_TO_WIN}. A path that is at = 1 and a path that is at = 2 teach that model two different lessons, and the CPM that comes back to the ad server is not the same kind of number.

Read at on each captured request before you rank the paths by the price in the ad server. A first-price bid is what the bidder was willing to pay on that hop. A second-price clear is what the auction settled at, which ${AUCTION_PRICE} will confirm later, after discount, in CPM. Averaging them, or picking the path whose ad-server CPM is higher, mixes a bid with a clear. The fee is a third adjustment on top of that, and it is still not this comparison.

A block list on one request does not protect the other two

bcat, badv, and bapp are the blocked categories, advertiser domains, and app ids on this request. battr is the blocked creative attributes on the banner, video, audio, or native object. Only one of acat and bcat should be present. Category numbers mean what cattax says they mean. The same integer under two taxonomies is two categories.

Prebid sends the lists you put in ortb2 and on the format object. Amazon and Google send the lists on the requests they build. An ad-server category exclusion is not automatically bcat on the Prebid call, and a Prebid badv is not automatically a protection on Open Bidding. Loss 209, 205, 206, or 210 on one path means that path's list rejected the creative. The other paths can still serve it. A buyer who says the slot blocks a category saw one request.

pmp.private_auction = 1 limits the impression to the deals in imp.pmp.deals. wseat and wadomain on a deal limit who may bid it, and omission means no limit. A private auction on the Prebid ad unit is not a private auction on TAM or Open Bidding unless those requests also say so. An open bid that loses with code 103 lost to a deal on the request that had one.

If at is missing, the auction is second price

at defaults to 2. That is second price plus, not first price. A request that omits at is second price plus: the winner pays just above the next bid. A path that sends at = 1 is first price, and the winner pays their bid. Reading a missing field as 1, because that is the auction you run in the ad server, makes ${AUCTION_PRICE} look like a bug when it comes back below the bid.

Values 500 and above are exchange-specific. A 500 on one path and a 1 on another are not comparable, and a bidder that does not know that exchange's private type should not treat it as first or second. Deal.at overrides the request for that deal only, including 3, where the floor is the fixed price. Capture at on the request and on each deal before you explain a CPM in the ad server. The default is the part people skip, and it is 2.

What to capture before you pick a path on price

  • For each Prebid client bidder, save one real request and check source.schain, source.tid, user.eids, regs.gpp, device.sua, and video.plcmt.
  • For each Prebid Server bidder, save the request PBS sent, not the browser adapter's imp.ext. Compare tmax to the page timeout.
  • For TAM, save the bid that returned to the ad server and, if Amazon gives you a sample of the outbound request, the fields on that sample. Do not paste the Prebid log into the TAM column.
  • For Open Bidding, list yield partners against Prebid bidders. One SSP on both lists is two requests. Confirm which OpenRTB body each partner received before you describe the slot as a single chain.
  • Do not join the three paths on source.tid unless you passed the same tid into each assembly. You usually did not.
  • Compare video.plcmt to video.placement per path. The integers do not map.
  • Note whether regs.gdpr and the other flags are 0 or 1, or true or false. A filter for one dialect drops the other.
  • List imp.pmp and imp.bidfloor per path. A loss code 4 or 100 on TAM or Open Bidding is that path's auction, not the Prebid ad unit.
  • Read source.fd per path. 0 means this exchange decides the sale. 1 means an upstream ad server still will. Header bidding is the second case.
  • Look for source.schain and, if it is absent, source.ext.schain. The same for user.consent and user.ext.consent. A pchain string is not a schain object.
  • Compare user.buyeruid and user.eids per path. A sync that ran in Prebid did not run for TAM or Open Bidding.
  • Record imp.tagid and imp.id separately. Only tagid can name the placement, and only if you set the same string on each path.
  • Record imp.bidfloor and imp.bidfloorcur per path. A missing currency is USD, not the request cur.
  • Record imp.secure per path. Omitted is not 1. true is not 1 either.
  • Record at per path before you compare the CPM numbers in the ad server. 1 and 2 are different prices. If the field is absent, it is 2.
  • Reject a request that contains more than one of site, app, and dooh. Bidders drop it, and the fill you see came from a different path.
  • Diff bcat, badv, bapp, and battr per path, and note cattax. A block on one request is not a block on the others.
  • Note pmp.private_auction and each deal's at. 3 means the floor is the price. The deal currency does not inherit the impression floor currency.

Fee schedules still matter, and they are a different article. This one is the request. Prebid's first party data merge is documented as ortb2. Prebid Server's half of the merge is the stored request. Amazon and Google document their own auction surfaces, and neither of those surfaces is that JSON. RTBlint checks the OpenRTB you actually have: the chain, the GPP pair, the plcmt value, the tids. Point it at each path's payload. A single green Prebid request does not clear the other two.

Further reading