<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aleksander Sekowski</title>
    <description>The latest articles on DEV Community by Aleksander Sekowski (@aleksuix).</description>
    <link>https://dev.to/aleksuix</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3866734%2F559a0a69-3c28-49e2-91c7-503810b941ba.png</url>
      <title>DEV Community: Aleksander Sekowski</title>
      <link>https://dev.to/aleksuix</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aleksuix"/>
    <language>en</language>
    <item>
      <title>A GA4 debug response can expose events your HTTP status hides.</title>
      <dc:creator>Aleksander Sekowski</dc:creator>
      <pubDate>Fri, 06 Nov 2026 19:00:00 +0000</pubDate>
      <link>https://dev.to/aleksuix/a-ga4-debug-response-can-expose-events-your-http-status-hides-3n39</link>
      <guid>https://dev.to/aleksuix/a-ga4-debug-response-can-expose-events-your-http-status-hides-3n39</guid>
      <description>&lt;p&gt;A GA4 Measurement Protocol request returning a successful HTTP status does not establish that its events were processed as intended. A transport check can pass while a malformed event disappears from the analytics you expected to see.&lt;/p&gt;

&lt;p&gt;Google provides a validation endpoint at &lt;code&gt;/debug/mp/collect&lt;/code&gt;. It returns diagnostic messages about the payload. Events sent there do not appear in reports, which makes it useful for development without mistaking a validation run for collected traffic.&lt;/p&gt;

&lt;p&gt;A useful integration has two explicit tests: one for the validation response and another for collection in the intended stream. Their evidence and failure messages should remain separate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the debug response part of the contract
&lt;/h2&gt;

&lt;p&gt;Do not reduce the result to &lt;code&gt;response.ok&lt;/code&gt;. Read the body and require the documented &lt;code&gt;validationMessages&lt;/code&gt; array to exist. An empty array is materially different from a missing array or an unreadable response.&lt;/p&gt;

&lt;p&gt;Preserve the field path, validation code and description for each finding. The status code alone cannot tell the developer which event parameter needs attention.&lt;/p&gt;

&lt;p&gt;This complete Node.js example tests response handling without making any network requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;assert&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:assert/strict&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;inspectDebugResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;transport-failure&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unexpected-response&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid JSON&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;validationMessages&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unexpected-response&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;missing validationMessages&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;validationMessages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payload-findings&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;validationMessages&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-payload-findings&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invalid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;validationMessages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
  &lt;span class="na"&gt;fieldPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;events&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;validationCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NAME_INVALID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Synthetic invalid event name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;inspectDebugResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;invalid&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payload-findings&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;inspectDebugResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{"validationMessages":[]}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-payload-findings&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;inspectDebugResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unexpected-response&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;inspectDebugResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;null&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unexpected-response&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;inspectDebugResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;html&amp;gt;error&amp;lt;/html&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unexpected-response&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;inspectDebugResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{"validationMessages":[]}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;transport-failure&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result names are intentionally narrow. &lt;code&gt;no-payload-findings&lt;/code&gt; does not claim that an event was collected, attributed or visible in a report. It states what this response established.&lt;/p&gt;

&lt;p&gt;The fixture with an empty object is particularly useful. A loose check such as &lt;code&gt;!body.validationMessages?.length&lt;/code&gt; would accept a missing field as if it were a clean diagnostic response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Send the payload you intend to deploy
&lt;/h2&gt;

&lt;p&gt;Construct the request through the same adapter used by your application. Then direct that serialized payload to the validation endpoint in your development test. A hand-written example can pass while the production adapter emits different field names or types.&lt;/p&gt;

&lt;p&gt;Google recommends strict development checks using &lt;code&gt;validation_behavior: "ENFORCE_RECOMMENDATIONS"&lt;/code&gt;. Its guidance also distinguishes that development setting from production collection behavior. Record which mode produced the diagnostic result.&lt;/p&gt;

&lt;p&gt;Use a dedicated test configuration and keep secrets outside fixtures and screenshots. An API secret included in a query string can leak into request logs if the full URL is printed indiscriminately.&lt;/p&gt;

&lt;p&gt;Do not automatically send the same fixture to the collection endpoint after a clean debug response. Make collection an explicit integration-test step with controlled synthetic data and a known observation plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design useful negative cases
&lt;/h2&gt;

&lt;p&gt;Keep one valid baseline payload and change one property per failing fixture. For example, introduce an invalid event name, remove a required field for the tested contract or change a numeric parameter into a string.&lt;/p&gt;

&lt;p&gt;Save the diagnostic response associated with each version. If Google returns several messages, preserve all of them rather than selecting the first and discarding potentially related findings.&lt;/p&gt;

&lt;p&gt;Assert the field path and validation category that matter to your adapter. Avoid making the test depend on the exact punctuation of a human-readable description, which can change without changing the underlying finding.&lt;/p&gt;

&lt;p&gt;Also test unexpected transport behavior. The local program covers a non-success status, invalid JSON and a missing diagnostic array. Your HTTP wrapper should additionally handle a timeout without reporting a payload-validation pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify collection in the intended stream
&lt;/h2&gt;

&lt;p&gt;After the payload test passes, send a permitted synthetic event through the actual collection path. Record the stream identifier, occurrence time, event name and a non-sensitive correlation value supported by your test setup.&lt;/p&gt;

&lt;p&gt;Observe the result in the appropriate destination tooling. Confirm that the event reached the intended stream and that the fields important to your application survived the trip. Choose the observation surface and waiting period explicitly rather than assuming every reporting view updates immediately.&lt;/p&gt;

&lt;p&gt;The debug service does not validate &lt;code&gt;api_secret&lt;/code&gt; or &lt;code&gt;firebase_app_id&lt;/code&gt;. A clean debug response therefore cannot establish that those configuration values identify a working collection setup.&lt;/p&gt;

&lt;p&gt;If a debug-endpoint event never appears in reports, that is expected behavior. Repeatedly sending it to the same debug URL will not turn it into a collected event.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make CI results explain the failure
&lt;/h2&gt;

&lt;p&gt;Give the integration separate result fields for transport, payload findings and collection observation. That lets a failing run distinguish an unreachable validation service from a rejected field or a configuration problem in the collection path.&lt;/p&gt;

&lt;p&gt;Store a sanitized fixture hash and adapter revision with the result. When a developer asks whether a refactor changed the payload, the record should answer which exact serialized input was checked.&lt;/p&gt;

&lt;p&gt;Keep locally executable response-parser tests in the fast test suite. Run the live validation test at an appropriate integration boundary rather than making every unit test depend on an external service.&lt;/p&gt;

&lt;p&gt;When testing a batch, retain the event order and the complete diagnostic array. A field path referring to an event index is only useful if you can reconstruct the same batch. Reordering the saved events for readability can point the investigation at the wrong payload. Include a single-event reproduction once you have isolated the finding, and keep the original batch as evidence of how the adapter produced it.&lt;/p&gt;

&lt;p&gt;I maintain Pixellint. The &lt;a href="https://pixellint.org/packs/google-analytics/?utm_source=dev.to&amp;amp;utm_medium=referral&amp;amp;utm_campaign=reference-series-2026&amp;amp;utm_content=ga4-debug-response-check"&gt;GA4 payload pack&lt;/a&gt; can provide local feedback before the request reaches Google's validation service. Use the external diagnostic result and the collection observation to close the remaining questions, with each result labeled by its scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.google.com/analytics/devguides/collection/protocol/ga4/validating-events" rel="noopener noreferrer"&gt;Google Measurement Protocol validation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference" rel="noopener noreferrer"&gt;Measurement Protocol transport reference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Technical sources checked October 25, 2026. Drafted with AI; examples are synthetic unless explicitly stated otherwise.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>analytics</category>
      <category>debugging</category>
      <category>api</category>
    </item>
    <item>
      <title>VMAP gives ad break inventory a schedule that engineering and revenue teams can test.</title>
      <dc:creator>Aleksander Sekowski</dc:creator>
      <pubDate>Fri, 06 Nov 2026 17:00:00 +0000</pubDate>
      <link>https://dev.to/aleksuix/vmap-gives-ad-break-inventory-a-schedule-that-engineering-and-revenue-teams-can-test-5c06</link>
      <guid>https://dev.to/aleksuix/vmap-gives-ad-break-inventory-a-schedule-that-engineering-and-revenue-teams-can-test-5c06</guid>
      <description>&lt;p&gt;Originally published on &lt;a href="https://vastlint.org/blog/vmap-ad-break-inventory-business/" rel="noopener noreferrer"&gt;VASTLint&lt;/a&gt;. Part 6 of 8 in this series on video standards and business outcomes.&lt;/p&gt;

&lt;p&gt;A long-form video plan may promise a pre-roll and several mid-roll breaks. The sales forecast can look complete while the implementation still disagrees about when a break starts, which ad source it calls or what happens when no ad is available. The schedule becomes a commercial dependency because the opportunity cannot be sold and delivered reliably unless the player knows when to create it.&lt;/p&gt;

&lt;p&gt;IAB Tech Lab's Video Multiple Ad Playlist, or VMAP, gives systems a shared way to describe ad breaks. VAST describes the ad responses used within that workflow. Following both standards can make a break plan easier to exchange and test. Demand, player behavior and the viewer's actual session still determine what is ultimately delivered.&lt;/p&gt;

&lt;h2&gt;
  
  
  A planned break is an opportunity that still needs execution.
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://iabtechlab.com/standards-old/video-multiple-ad-playlist-vmap/" rel="noopener noreferrer"&gt;official VMAP overview&lt;/a&gt; describes the standard for multiple ad opportunities in video content. The useful distinction for a business team is between the break schedule and the ads returned for it. A valid schedule can identify an opportunity even when its ad source returns no eligible ad for the request.&lt;/p&gt;

&lt;p&gt;For a product manager at a streaming publisher, this separates two kinds of problem. If the player never reaches the expected break, investigate scheduling and content behavior. If it reaches the break but no ad is returned, inspect the ad request and demand path. A single fill-rate number can obscure these different causes and send the wrong team to investigate.&lt;/p&gt;

&lt;p&gt;The handoff should identify the intended content type, the relevant break positions and the implementation that consumes the schedule. Record whether the test uses on-demand playback, a live workflow or another supported mode. A successful result in one context does not automatically demonstrate identical break behavior in the others, especially when the content timeline can change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Break scheduling and pod composition need separate acceptance checks.
&lt;/h2&gt;

&lt;p&gt;A break can contain several ads, but the number and duration of those ads must suit the actual break and player configuration. The schedule alone cannot guarantee that the returned combination fits. Check how the destination handles an overlong ad, a partially filled break and an individual creative that fails after the break has started.&lt;/p&gt;

&lt;p&gt;The campaign and content teams also need to agree what experience is being offered. A 30-second opportunity is not interchangeable with two 15-second opportunities in every commercial arrangement. Reporting should preserve the actual placement and delivered creative. If the player substitutes a different ad or returns to content early, keep that outcome visible in the record.&lt;/p&gt;

&lt;p&gt;For implementation guidance on the ad side, the &lt;a href="https://www.iab.com/wp-content/uploads/2015/06/VASTv3_0.pdf" rel="noopener noreferrer"&gt;VAST 3.0 specification&lt;/a&gt; describes ad pods and sequences. Use the version and features supported by the destination. Do not assume that adding a sequence value to a document makes every player execute the same pod behavior or accept an otherwise incompatible creative.&lt;/p&gt;

&lt;h2&gt;
  
  
  The forecast needs to distinguish sessions from billable impressions.
&lt;/h2&gt;

&lt;p&gt;An illustrative plan with 100,000 sessions and four possible breaks describes up to 400,000 break opportunities if every session reaches every break. Viewers who leave early change that count. Some breaks may contain multiple ads; others may be skipped or have no demand. None of those initial figures is automatically a count of billable impressions.&lt;/p&gt;

&lt;p&gt;A useful model moves through the actual stages. Start with sessions that reach the position, identify breaks triggered, count eligible ad requests and record ads that actually render under the billing definition. Use observed rates for each stage where available. If a rate is an assumption, label it and show how sensitive the forecast is to that input.&lt;/p&gt;

&lt;p&gt;For a simple illustrative calculation, assume a scheduling correction produces 20,000 additional billable impressions with no displaced paid demand at a net CPM of $8. The associated revenue is $160. The calculation requires that incremental impression evidence. Counting 20,000 repaired schedule entries would not establish the same outcome because some viewers may never reach those breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  More break opportunities can change the content business too.
&lt;/h2&gt;

&lt;p&gt;Ad load affects the experience around the content. A publisher considering another break should examine session completion, abandonment around breaks and any relevant subscription or engagement measures. The extra ad opportunity has limited value if it changes viewing behavior in a way that harms the broader product. The direction and scale need to be measured for the actual audience.&lt;/p&gt;

&lt;p&gt;This is a reason to separate a technical correction from a product experiment. Restoring a break that the approved plan already required is one change. Increasing the approved ad load is another. They can have different owners, success measures and review requirements. Combining them makes it harder to explain which change caused a shift in revenue or viewing behavior.&lt;/p&gt;

&lt;p&gt;When testing ad load, use an appropriate comparison and retain the observation window. A change in content mix, audience acquisition or seasonality can affect the same metrics. An experiment may show a tradeoff rather than a universal improvement. The standard supplies a way to express the schedule; it does not decide the right commercial experience for the publisher.&lt;/p&gt;

&lt;h2&gt;
  
  
  A reproducible schedule can reduce launch and support work.
&lt;/h2&gt;

&lt;p&gt;Without a shared representation, partners may exchange break positions in prose, screenshots or custom configuration files. Each translation creates a chance for disagreement about the intended schedule. A standard document gives both teams a common artifact to review, provided the consuming implementation supports the features used and the content timeline is identified correctly.&lt;/p&gt;

&lt;p&gt;Retain a short acceptance record that pairs the intended schedule with observed player events. Record seeking behavior when it is relevant to the product, as well as the normal uninterrupted session. A player can appear correct during a simple playback test while behaving differently when a viewer seeks, resumes or enters content partway through a session.&lt;/p&gt;

&lt;p&gt;The operational benefit can be measured in setup effort and defect resolution. Track time configuring a new content integration, schedule-related launch issues and manual steps needed to reproduce them. Keep those figures separate from demand troubleshooting. A lower count of scheduling incidents is useful evidence even when market demand makes a revenue comparison inconclusive for the same period.&lt;/p&gt;

&lt;h2&gt;
  
  
  VASTLint can check the schedule before the player test.
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://vastlint.org/blog/how-to-validate-vmap/" rel="noopener noreferrer"&gt;VASTLint's VMAP guide&lt;/a&gt; describes document checks for the scheduling handoff. Run the supplied schedule through the &lt;a href="https://vastlint.org/validate/" rel="noopener noreferrer"&gt;validator&lt;/a&gt; and inspect the relevant ad responses as well. A schedule that passes its own checks can still point to a VAST document with a different problem. Preserve both inputs when explaining the result.&lt;/p&gt;

&lt;p&gt;Then test the actual player or insertion workflow against known content. Save the scheduled position, observed break event, returned ad and outcome. Include a valid no-ad case so the test can distinguish an intentional empty response from an execution problem. The useful result is an explainable sequence of events that someone else can reproduce.&lt;/p&gt;

&lt;p&gt;Static validation cannot establish how many viewers reached a break or whether an ad was billed. It also cannot guarantee that a particular player implements every permitted schedule feature. Match the document result with the runtime record before accepting the integration. This keeps a clean validation report from being used as evidence for a delivery promise it did not test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Engineering and revenue should review the same break record.
&lt;/h2&gt;

&lt;p&gt;Give the break a stable operational identity within the content and campaign records. Engineering needs to trace its execution; revenue teams need to understand which opportunity was sold. If the meaning changes during a release, document the change and check the reporting mapping. A familiar label is not enough if it now refers to a different position or configuration.&lt;/p&gt;

&lt;p&gt;After rollout, monitor the funnel from reached break positions to accepted billable impressions. Investigate changes at the stage where they occur. A fall in triggered breaks is a different problem from a fall in eligible demand. This view makes it easier to assign work and prevents a scheduling issue from being hidden inside an aggregate monetization metric.&lt;/p&gt;

&lt;p&gt;VMAP adherence supports a break plan that participants can exchange, inspect and test. Its business value can appear as more predictable implementation and clearer inventory accounting. Revenue benefits require actual incremental delivery under the agreed billing rules. Keep the schedule, the observed session and the commercial outcome connected so the team can tell which result the standard helped produce.&lt;/p&gt;

</description>
      <category>adtech</category>
      <category>webdev</category>
      <category>testing</category>
      <category>performance</category>
    </item>
    <item>
      <title>The IAB Privacy String Cheat Sheet Every Ad Ops Engineer Needs</title>
      <dc:creator>Aleksander Sekowski</dc:creator>
      <pubDate>Fri, 06 Nov 2026 00:08:16 +0000</pubDate>
      <link>https://dev.to/aleksuix/the-iab-privacy-string-cheat-sheet-every-ad-ops-engineer-needs-8fm</link>
      <guid>https://dev.to/aleksuix/the-iab-privacy-string-cheat-sheet-every-ad-ops-engineer-needs-8fm</guid>
      <description>&lt;p&gt;On &lt;a href="https://iabtechlab.com/press-releases/iab-tech-lab-expands-global-privacy-frameworks-with-gpp-updates-and-ddrf-v2-release/" rel="noopener noreferrer"&gt;23 October 2025&lt;/a&gt; IAB Tech Lab opened public comment on H2 2025 Global Privacy Platform updates: four new U.S. state sections and a rearchitected state-string layout with explicit section headers. That is not a CMP banner story. It is a wire-format story. The same month, traffickers still paste TC Strings into &lt;code&gt;gpp&lt;/code&gt;, leave &lt;code&gt;us_privacy=1YNN&lt;/code&gt; on CM360 pixels that expect GPP National, and ship OpenRTB bids where &lt;code&gt;regs.gpp_sid&lt;/code&gt; survives a hop but &lt;code&gt;regs.gpp&lt;/code&gt; does not. Every one of those URLs can look "privacy complete" in a spreadsheet while the downstream callee runs the wrong decoder.&lt;/p&gt;

&lt;p&gt;This sheet is for ad ops and bid engineers who own the live tag URL, the Floodlight path, or the OpenRTB &lt;code&gt;regs&lt;/code&gt; object and need one place to map &lt;strong&gt;parameter name → grammar → where it must land&lt;/strong&gt;. One malformed field is never the whole job; the job is knowing which family you actually have before you argue with fill or attribution.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sheet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal family&lt;/th&gt;
&lt;th&gt;Typical pixel / ad-server slot&lt;/th&gt;
&lt;th&gt;OpenRTB 2.6 home&lt;/th&gt;
&lt;th&gt;How you know the grammar&lt;/th&gt;
&lt;th&gt;Wrong cousin engineers swap in&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;IAB TCF v2 TC String&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gdpr_consent&lt;/code&gt; (with &lt;code&gt;gdpr=1&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;user.consent&lt;/code&gt; (TC String)&lt;/td&gt;
&lt;td&gt;Decoded TC version field is &lt;strong&gt;2&lt;/strong&gt;; core segment is long (not a single digit)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gdpr_consent=1&lt;/code&gt; or &lt;code&gt;true&lt;/code&gt; (valid base64, &lt;strong&gt;not&lt;/strong&gt; a TC String)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Global Privacy Platform&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gpp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;regs.gpp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GPP string header &lt;strong&gt;type 3&lt;/strong&gt; after decode&lt;/td&gt;
&lt;td&gt;TC String copied into &lt;code&gt;gpp&lt;/code&gt; (decodes as &lt;strong&gt;type 2&lt;/strong&gt;, wrong parameter)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPP section in force&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gpp_sid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;regs.gpp_sid&lt;/code&gt; (array of section ids)&lt;/td&gt;
&lt;td&gt;Integer(s): e.g. &lt;strong&gt;7&lt;/strong&gt; US National, &lt;strong&gt;8&lt;/strong&gt; California, &lt;strong&gt;2&lt;/strong&gt; TCF section inside GPP&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gpp_sid&lt;/code&gt; present while &lt;code&gt;gpp&lt;/code&gt; is empty or a TC-only blob&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAB US Privacy (legacy)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;us_privacy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;regs.us_privacy&lt;/code&gt; (deprecated)&lt;/td&gt;
&lt;td&gt;Four characters: version digit + three &lt;code&gt;Y&lt;/code&gt;/&lt;code&gt;N&lt;/code&gt;/&lt;code&gt;-&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Treating &lt;code&gt;1YNN&lt;/code&gt; as GPP National v2 on products that read GPP only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GDPR scope flag&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gdpr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;regs.ext.gdpr&lt;/code&gt; / impression regs&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0&lt;/strong&gt; or &lt;strong&gt;1&lt;/strong&gt; only&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gdpr=1&lt;/code&gt; with missing or placeholder &lt;code&gt;gdpr_consent&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Additional Consent&lt;/td&gt;
&lt;td&gt;&lt;code&gt;addtl_consent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;exchange-specific extensions&lt;/td&gt;
&lt;td&gt;AC string for Google ATP vendors&lt;/td&gt;
&lt;td&gt;Used as a stand-in for TC on tags that still require &lt;code&gt;gdpr_consent&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate consent keys&lt;/td&gt;
&lt;td&gt;same URL twice&lt;/td&gt;
&lt;td&gt;duplicate &lt;code&gt;regs&lt;/code&gt; fields&lt;/td&gt;
&lt;td&gt;At most one authoritative value per signal&lt;/td&gt;
&lt;td&gt;Two &lt;code&gt;gdpr_consent&lt;/code&gt; or &lt;code&gt;gpp&lt;/code&gt; values on one pixel (callee must guess)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;OpenRTB and the pixel are parallel hops, not aliases. A CMP can be correct in the page and still wrong on the request the vendor logs if the ad-server macro never expanded. Floodlight-style path parameters count the same as query keys: if the string is not on the HTTP request the collector sees, it does not exist for billing or restriction logic.&lt;/p&gt;

&lt;p&gt;For the October GPP release specifically, new state sections and subsection headers change what parsers must expect inside a type-3 container. Our &lt;a href="https://rtblint.org/blog/gpp-new-sections-2026/" rel="noopener noreferrer"&gt;write-up on the new sections and OpenRTB &lt;code&gt;regs.gpp&lt;/code&gt;&lt;/a&gt; tracks what landed in that comment package; your CMP version and your bid adapter version can drift on different calendars.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three rows that validate and still fail in production
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;gdpr_consent=1&lt;/code&gt;.&lt;/strong&gt; Alphabet and base64 validators green-light it. A TC decoder rejects it immediately because there is no version-2 bitfield and no purpose vector. Google tags interpret &lt;code&gt;gdpr=1&lt;/code&gt; as "TC applies"; an empty or numeric placeholder is how personalized ads quietly turn off while the tag still "has a consent param."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TC String in &lt;code&gt;gpp&lt;/code&gt; with &lt;code&gt;gpp_sid=2&lt;/code&gt;.&lt;/strong&gt; The blob is long, base64-safe, and familiar. Decoded header type is &lt;strong&gt;2&lt;/strong&gt; (TC), not &lt;strong&gt;3&lt;/strong&gt; (GPP). Campaign Manager 360's own documentation says TCF strings sent through GPP are not accepted on that hop; the section id names TCF while the product still wants National or supported state sections on &lt;code&gt;gpp&lt;/code&gt; / &lt;code&gt;gpp_sid&lt;/code&gt;. We walked through that mix-up on &lt;a href="https://pixellint.org/blog/cm360-gpp-national-v2/" rel="noopener noreferrer"&gt;CM360 GPP National v2 versus &lt;code&gt;us_privacy&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;regs.gpp_sid&lt;/code&gt; without &lt;code&gt;regs.gpp&lt;/code&gt; on the bid.&lt;/strong&gt; Diff tools flag it as incoherent OpenRTB: you told the exchange which GPP section is in force but did not ship the string. Prebid Server and exchange normalizers do not always drop both fields together; one side of the hop can inherit a section id from a template while the GPP body was stripped for size. Buyers read that as "restrict processing" with no payload to parse.&lt;/p&gt;

&lt;h2&gt;
  
  
  How you catch it
&lt;/h2&gt;

&lt;p&gt;An LLM implementing tag macros or OpenRTB &lt;code&gt;regs&lt;/code&gt; can emit plausible key names and still ship the wrong grammar. The RPC or the ad server can return 200. The wire contract does not move.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pixellint&lt;/strong&gt; (&lt;code&gt;cargo install pixellint&lt;/code&gt;, &lt;code&gt;npm install pixellint&lt;/code&gt;) decodes IAB consent strings on the pixel URL and path-shaped tags instead of treating them as opaque base64. It checks TC version bits, GPP header type, deprecated &lt;code&gt;us_privacy&lt;/code&gt;, duplicate signals, and template macros that never expanded. It is independent of IAB Tech Lab and of AAO; it is the deterministic check on the string your tag actually fired.&lt;/p&gt;

&lt;p&gt;Paste a live URL into the &lt;a href="https://pixellint.org/" rel="noopener noreferrer"&gt;playground&lt;/a&gt; or run CLI validation in CI on exported tag templates before traffickers clone them across regions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://pixellint.org/docs/gpp/" rel="noopener noreferrer"&gt;GPP strings and &lt;code&gt;gpp_sid&lt;/code&gt;&lt;/a&gt; (header type 3, section ids, TC-in-GPP mistakes)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pixellint.org/docs/tcf-v2/" rel="noopener noreferrer"&gt;TCF v2 on pixels&lt;/a&gt; (&lt;code&gt;gdpr&lt;/code&gt; flag, TC String length and version)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pixellint.org/docs/consent-on-the-wire/" rel="noopener noreferrer"&gt;Consent on the wire&lt;/a&gt; (query vs path, empty macros, vendor-specific dialects kept separate)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pixellint.org/docs/consent/" rel="noopener noreferrer"&gt;Consent overview&lt;/a&gt; and &lt;a href="https://pixellint.org/docs/us-privacy-string/" rel="noopener noreferrer"&gt;deprecated US Privacy&lt;/a&gt; (31 January 2024 deprecation vs tags that still emit &lt;code&gt;1YNN&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the bug is on the bid object rather than the pixel, pair this sheet with OpenRTB privacy field docs on the exchange side; the header rules are the same even when the parameter names move from &lt;code&gt;gdpr_consent&lt;/code&gt; to &lt;code&gt;user.consent&lt;/code&gt; and from &lt;code&gt;gpp&lt;/code&gt; to &lt;code&gt;regs.gpp&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>openrtb</category>
      <category>advertising</category>
      <category>privacy</category>
      <category>javascript</category>
    </item>
    <item>
      <title>OpenRTB Content Taxonomy Explained: How cat, cattax, and gtax Version the Bid</title>
      <dc:creator>Aleksander Sekowski</dc:creator>
      <pubDate>Thu, 05 Nov 2026 00:54:21 +0000</pubDate>
      <link>https://dev.to/aleksuix/openrtb-content-taxonomy-explained-how-cat-cattax-and-gtax-version-the-bid-314c</link>
      <guid>https://dev.to/aleksuix/openrtb-content-taxonomy-explained-how-cat-cattax-and-gtax-version-the-bid-314c</guid>
      <description>&lt;p&gt;On October 28, 2026, &lt;a href="https://digiday.com/future-of-tv/future-of-tv-briefing-ctv-advertising-has-a-genre-label-problem/" rel="noopener noreferrer"&gt;Digiday&lt;/a&gt; reported that most CTV bid requests still carry genre as free text or channel-level labels that do not match the program. IAB Tech Lab’s answer on the wire is not a longer string. It is a pair of integer arrays plus a version field: &lt;code&gt;cat&lt;/code&gt; with &lt;code&gt;cattax&lt;/code&gt;, and &lt;code&gt;genres&lt;/code&gt; with &lt;code&gt;gtax&lt;/code&gt;. If you treat &lt;code&gt;712&lt;/code&gt; as “Sports” without reading which taxonomy table applies, you are guessing about inventory the buyer will block or bid on.&lt;/p&gt;

&lt;p&gt;OpenRTB never promised that category codes were stable across releases. It promised that the receiver could look up the code in the table named by &lt;code&gt;cattax&lt;/code&gt; or &lt;code&gt;gtax&lt;/code&gt;. That lookup is the versioning story practitioners skip when they copy CMS tags into &lt;code&gt;site.cat&lt;/code&gt; and call the request done.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;cat&lt;/code&gt; actually is
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;cat&lt;/code&gt; is an array of integers. Each integer is a row in an IAB Content Taxonomy spreadsheet, not a universal label. The taxonomy in use is declared separately, on the same object, in &lt;code&gt;cattax&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can attach categories at several layers on one bid request:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Site or App&lt;/strong&gt;: &lt;code&gt;site.cat&lt;/code&gt; / &lt;code&gt;app.cat&lt;/code&gt; with matching &lt;code&gt;site.cattax&lt;/code&gt; / &lt;code&gt;app.cattax&lt;/code&gt; for the property.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publisher&lt;/strong&gt;: &lt;code&gt;publisher.cat&lt;/code&gt; and &lt;code&gt;publisher.cattax&lt;/code&gt; when the exchange models the seller separately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content&lt;/strong&gt;: &lt;code&gt;content.cat&lt;/code&gt; and &lt;code&gt;content.cattax&lt;/code&gt; for the specific video, article, or episode in view.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Producer&lt;/strong&gt;: &lt;code&gt;producer.cat&lt;/code&gt; and &lt;code&gt;producer.cattax&lt;/code&gt; when the production entity differs from the publisher.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bid response&lt;/strong&gt;: &lt;code&gt;bid.cat&lt;/code&gt; and &lt;code&gt;bid.cattax&lt;/code&gt; when the buyer echoes what they believe they bought.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OpenRTB 2.6 standardized &lt;code&gt;cattax&lt;/code&gt; on all of those objects. A validator that only checks “is this integer in any taxonomy CSV” will pass codes that mean different verticals depending on which object omitted &lt;code&gt;cattax&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The spec text is explicit: if no &lt;code&gt;cattax&lt;/code&gt; is supplied, &lt;strong&gt;IAB Content Category Taxonomy 1.0&lt;/strong&gt; is assumed. That default is not a hint. It is the table your partner’s block list uses when they never upgraded from 1.0 mappings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Genre is a different column, with a different default
&lt;/h2&gt;

&lt;p&gt;Long-form video added a second labeling path on &lt;code&gt;Content&lt;/code&gt;. Legacy &lt;code&gt;genre&lt;/code&gt; remains free text. For programmatic genre targeting, OpenRTB 2.6-202501 added:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;content.genres&lt;/code&gt;: integer array of genre ids from the taxonomy named by &lt;code&gt;gtax&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;content.gtax&lt;/code&gt;: which AdCOM category taxonomy applies to those ids.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Implementation guidance in the &lt;a href="https://github.com/InteractiveAdvertisingBureau/openrtb2.x/blob/main/implementation.md" rel="noopener noreferrer"&gt;OpenRTB 2.x implementation notes&lt;/a&gt; tells sellers to keep property-level categories in &lt;code&gt;site.cat&lt;/code&gt; / &lt;code&gt;app.cat&lt;/code&gt;, use &lt;code&gt;content.genres&lt;/code&gt; for program genre, and declare &lt;code&gt;gtax&lt;/code&gt; so DSPs know which table to read. The CTV genre list is a subset of Content Taxonomy 3.1, not a reuse of &lt;code&gt;cat&lt;/code&gt; codes from 1.0.&lt;/p&gt;

&lt;p&gt;Here is the asymmetry that breaks production QA. &lt;strong&gt;&lt;code&gt;cattax&lt;/code&gt; defaults to Taxonomy 1.0. &lt;code&gt;gtax&lt;/code&gt; defaults to Content Taxonomy 3.1&lt;/strong&gt; (AdCOM list value 9). On one &lt;code&gt;Content&lt;/code&gt; object you can therefore ship 3.1 genre ids that receivers interpret correctly while 3.1 category ids in &lt;code&gt;content.cat&lt;/code&gt; are read as 1.0 rows because &lt;code&gt;content.cattax&lt;/code&gt; was omitted. Schema validation often stays green; the buyer’s suitability engine does not.&lt;/p&gt;

&lt;p&gt;Example shape (one field is wrong on purpose):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Friday Night Match"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;483&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"genres"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;712&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"gtax"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;content.cattax&lt;/code&gt;, &lt;code&gt;483&lt;/code&gt; is a 1.0 category. With &lt;code&gt;gtax: 9&lt;/code&gt;, &lt;code&gt;712&lt;/code&gt; is read against 3.1’s CTV genre subset. A DSP blocking “Sports” in 1.0 does not see the same signal as a contextual buyer targeting genre 712 in 3.1. Loss reason 209 (category blocked) without the &lt;code&gt;cattax&lt;/code&gt; from that request is unreadable noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  How AdCOM lists tie the wire together
&lt;/h2&gt;

&lt;p&gt;OpenRTB 2.6 moved enumerated lists into &lt;a href="https://rtblint.org/docs/openrtb/adcom/" rel="noopener noreferrer"&gt;AdCOM&lt;/a&gt;. The &lt;strong&gt;Category Taxonomies&lt;/strong&gt; list is what &lt;code&gt;cattax&lt;/code&gt; and &lt;code&gt;gtax&lt;/code&gt; index. Values cover Content Taxonomy 1.0, 2.0, 2.2, 3.0, 3.1, vendor tables, and custom taxonomies when both sides agree.&lt;/p&gt;

&lt;p&gt;When Google Ad Manager started populating &lt;code&gt;site.cat&lt;/code&gt;, &lt;code&gt;site.pagecat&lt;/code&gt;, and app equivalents on Authorized Buyers traffic, it also set &lt;code&gt;cattax&lt;/code&gt; to the 1.0 mapping on those requests. Header bidding paths that map CMS tags into &lt;code&gt;ortb2.site.content&lt;/code&gt; without the same &lt;code&gt;cattax&lt;/code&gt; create a split brain: ADX sees 1.0-labeled inventory while Prebid-sourced requests look valid but label a different vertical. Our write-up on &lt;a href="https://rtblint.org/blog/gam-pagecat-on-adx-not-on-prebid-ortb2/" rel="noopener noreferrer"&gt;GAM pagecat on ADX versus Prebid ortb2&lt;/a&gt; walks through that hop mismatch.&lt;/p&gt;

&lt;p&gt;Migrating to 2.2 or 3.1 category ids requires setting &lt;code&gt;cattax&lt;/code&gt; to the matching list value everywhere you send &lt;code&gt;cat&lt;/code&gt;, not only on &lt;code&gt;content&lt;/code&gt;. Copying 2.2 ids into &lt;code&gt;cat&lt;/code&gt; while leaving the default 1.0 assumption is how “valid” requests fail brand suitability silently.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://rtblint.org/blog/openrtb-2-6-202501-review/" rel="noopener noreferrer"&gt;2.6-202501 release&lt;/a&gt; is the dated snapshot that added &lt;code&gt;gtax&lt;/code&gt; and &lt;code&gt;genres&lt;/code&gt;. Partners on older OpenRTB builds may ignore those fields entirely and fall back to free-text &lt;code&gt;genre&lt;/code&gt;, which is exactly the signal Digiday described as unreliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  How you catch it
&lt;/h2&gt;

&lt;p&gt;An LLM generating OpenRTB from a brief can emit plausible integers and still pick the wrong default table. The RPC or the exchange adapter can return OK while the downstream DSP reads a different category than the seller intended.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://rtblint.org" rel="noopener noreferrer"&gt;RTBlint&lt;/a&gt; is the deterministic check on the bid object: &lt;code&gt;cattax&lt;/code&gt; enums, co-occurrence of &lt;code&gt;cat&lt;/code&gt; with &lt;code&gt;cattax&lt;/code&gt;, &lt;code&gt;genres&lt;/code&gt; with &lt;code&gt;gtax&lt;/code&gt;, and dated-snapshot fields for the OpenRTB version you declare. It is independent of IAB Tech Lab and of AgenticAdvertising.org; it does not replace their specs, and they do not require it. It is what you run when the agent’s JSON needs to match what the exchange actually parses.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rtblint validate &lt;span class="nt"&gt;--version&lt;/span&gt; 2.6-202606 bid.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the &lt;a href="https://rtblint.org/docs/openrtb-versions/" rel="noopener noreferrer"&gt;OpenRTB version picker&lt;/a&gt; before you argue about a field: 2.6-202501 added genre taxonomy fields; 2.6-202606 added liveness attributes on &lt;code&gt;Content&lt;/code&gt; that do not replace &lt;code&gt;gtax&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Rule docs for the 202501 content additions live under &lt;a href="https://rtblint.org/docs/rules/openrtb-2-6-202501-content-gtax_genres/" rel="noopener noreferrer"&gt;content.gtax and content.genres&lt;/a&gt;. For broader AdCOM context on where &lt;code&gt;cat&lt;/code&gt; and &lt;code&gt;cattax&lt;/code&gt; sit in the object model, see the &lt;a href="https://rtblint.org/docs/openrtb/adcom/" rel="noopener noreferrer"&gt;AdCOM reference&lt;/a&gt;. The &lt;a href="https://rtblint.org/guides/" rel="noopener noreferrer"&gt;guides index&lt;/a&gt; collects supply-path and OpenRTB migration notes when you need a checklist beyond one malformed attribute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Read the &lt;a href="https://rtblint.org/blog/openrtb-2-6-202501-review/" rel="noopener noreferrer"&gt;2.6-202501 content object review&lt;/a&gt; before you map CTV genre ids.&lt;/li&gt;
&lt;li&gt;Align header bidding &lt;code&gt;ortb2&lt;/code&gt; category mapping with the &lt;code&gt;cattax&lt;/code&gt; your ADX or SDK path sends.&lt;/li&gt;
&lt;li&gt;When debugging blocks, log &lt;code&gt;cattax&lt;/code&gt; and &lt;code&gt;gtax&lt;/code&gt; alongside &lt;code&gt;cat&lt;/code&gt; and &lt;code&gt;genres&lt;/code&gt; on the same request id.&lt;/li&gt;
&lt;li&gt;Pick a dated snapshot in &lt;a href="https://rtblint.org/docs/openrtb-versions/2-6-202606/" rel="noopener noreferrer"&gt;OpenRTB versions&lt;/a&gt; and validate fixtures against it in CI.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The integer was never the category. The selector beside it was.&lt;/p&gt;

</description>
      <category>openrtb</category>
      <category>advertising</category>
      <category>adtech</category>
      <category>json</category>
    </item>
    <item>
      <title>OMID browserOptional describes the JavaScript environment.</title>
      <dc:creator>Aleksander Sekowski</dc:creator>
      <pubDate>Wed, 04 Nov 2026 19:00:00 +0000</pubDate>
      <link>https://dev.to/aleksuix/omid-browseroptional-describes-the-javascript-environment-35gi</link>
      <guid>https://dev.to/aleksuix/omid-browseroptional-describes-the-javascript-environment-35gi</guid>
      <description>&lt;p&gt;The name &lt;code&gt;browserOptional&lt;/code&gt; is easy to misread as permission to skip verification JavaScript. It describes the script's execution environment. Confusing those meanings can leave a player showing video while the required verification resource never runs.&lt;/p&gt;

&lt;p&gt;This is especially relevant when a player moves from a browser test page to an app or another environment with a different JavaScript runtime. The same resource URL does not establish the same available APIs.&lt;/p&gt;

&lt;p&gt;VAST 4.3 section 3.17.1 provides the selection rules. The integration work is to apply them to a real environment, preserve the selected resource and verify what actually executed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Describe the environment precisely
&lt;/h2&gt;

&lt;p&gt;A resource marked &lt;code&gt;browserOptional="true"&lt;/code&gt; declares support for execution without DOM and other browser built-ins. The resource remains JavaScript. The flag does not replace it with a native measurement vendor SDK.&lt;/p&gt;

&lt;p&gt;For browser or webview execution, the specification prefers resources marked false when both forms are available. A non-browser JavaScript environment may execute only resources marked true. If it receives only unsuitable resources, the associated not-executed reporting must remain visible.&lt;/p&gt;

&lt;p&gt;Start an integration record with the player build, OM SDK integration, runtime and supported framework. Avoid a single &lt;code&gt;isNative&lt;/code&gt; variable that attempts to represent all those properties. An app can host a webview, and a platform label alone does not tell you which environment executes verification code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate the XML attribute before selecting a resource
&lt;/h2&gt;

&lt;p&gt;An XML parser usually returns attribute text. In JavaScript, &lt;code&gt;Boolean("false")&lt;/code&gt; evaluates to true. The string is nonempty, so ordinary truthiness does not interpret its XML meaning.&lt;/p&gt;

&lt;p&gt;The following complete Node.js example accepts XML Schema boolean forms and then applies a narrow resource-selection rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;assert&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:assert/strict&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parseXmlBoolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;browserOptional attribute is missing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lexical&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lexical&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;lexical&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lexical&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;false&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;lexical&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid XML boolean&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;selectForEnvironment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;browserOptional&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;boolean&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;resources must contain validated booleans&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;browser&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;browserOptional&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;non-browser-js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;browserOptional&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown execution environment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;portable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;browserOptional&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parseXmlBoolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;browser&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;browserOptional&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parseXmlBoolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;false&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;selectForEnvironment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;browser&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;browser&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;selectForEnvironment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;non-browser-js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;portable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;selectForEnvironment&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;non-browser-js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parseXmlBoolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parseXmlBoolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; 1 &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;throws&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;parseXmlBoolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;throws&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;parseXmlBoolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;False&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;throws&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;selectForEnvironment&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt; &lt;span class="na"&gt;browserOptional&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;false&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;browser&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The selector deliberately assumes that the caller already filtered resources for the relevant verification entry and supported API framework. It illustrates environment selection, not the complete VAST verification algorithm.&lt;/p&gt;

&lt;p&gt;The parser preserves missing or invalid values as errors. Supplying a default at this stage would hide a malformed declaration and make the later selection look justified.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep eligibility separate from successful execution
&lt;/h2&gt;

&lt;p&gt;Selecting the portable resource establishes that it is eligible under this environment rule. Several later questions remain: did the player fetch it, did loading complete, did its execution succeed and did it participate in the expected measurement session?&lt;/p&gt;

&lt;p&gt;Record these as separate outcomes. A resource that was eligible but failed to load needs a different investigation from a resource the player never selected.&lt;/p&gt;

&lt;p&gt;Keep the verification vendor, selected URL, resource position in the document and environment in the diagnostic record. If wrappers add verification entries, preserve which document introduced each one. Otherwise a report can identify the wrong vendor script when several resources are present.&lt;/p&gt;

&lt;p&gt;Do not conclude that measurement worked merely because a global JavaScript object exists. Check the integration's documented session and event observations. The exact instrumentation depends on the SDK and platform, so retain the build and test environment with that evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the matrix that your deployment will use
&lt;/h2&gt;

&lt;p&gt;Create one fixture with a browser-only resource, one with a portable resource and one containing both. Run them against each execution environment your product supports.&lt;/p&gt;

&lt;p&gt;For the browser environment, verify the selection preference when both forms are offered. For the non-browser environment, verify that the browser-only fixture cannot accidentally pass because a desktop test harness supplies a DOM.&lt;/p&gt;

&lt;p&gt;Add a fixture with a missing attribute and another containing the text &lt;code&gt;false&lt;/code&gt;. The latter specifically guards against a future refactor replacing deliberate parsing with &lt;code&gt;Boolean(value)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Keep environment selection deterministic. If two resources are equally eligible, document the implementation's selection behavior rather than allowing iteration order from an unrelated data structure to choose the script accidentally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use runtime failures to narrow the next step
&lt;/h2&gt;

&lt;p&gt;If a resource is never selected, inspect the parsed declaration and framework filtering first. If it is selected but never requested, inspect the player's execution policy and instrumentation. If the request occurs but execution fails, inspect the response, loading restrictions and runtime diagnostics.&lt;/p&gt;

&lt;p&gt;A script can also depend on browser APIs despite declaring portability. Retain a minimal reproduction in the actual non-browser runtime, with the vendor resource revision. Changing the flag in your local XML without checking the script's behavior does not repair that dependency.&lt;/p&gt;

&lt;p&gt;When no verification executes, preserve the associated failure reporting. Missing measurement is an operational result that needs explanation, not a successful session with zero measured activity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the declaration and observation in the same review
&lt;/h2&gt;

&lt;p&gt;A useful acceptance record includes the XML fixture, parsed boolean, selected resource, runtime identity and measurement-session observation. Another engineer should be able to determine where the check stopped without rerunning the entire campaign.&lt;/p&gt;

&lt;p&gt;I maintain VASTlint. Use the &lt;a href="https://vastlint.org/validate/?utm_source=dev.to&amp;amp;utm_medium=referral&amp;amp;utm_campaign=reference-series-2026&amp;amp;utm_content=omid-browseroptional-environment"&gt;VAST XML validator&lt;/a&gt; to inspect the declaration before the environment test. Keep its structural result beside the player evidence, so each result supports the question it actually checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://iabtechlab.com/wp-content/uploads/2022/09/VAST_4.3.pdf" rel="noopener noreferrer"&gt;VAST 4.3, section 3.17.1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://iabtechlab.com/wp-content/uploads/2024/08/OMID_API_v1.5.pdf" rel="noopener noreferrer"&gt;OMID API 1.5&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Technical sources checked October 25, 2026. Drafted with AI; examples are synthetic unless explicitly stated otherwise.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vast</category>
      <category>javascript</category>
      <category>testing</category>
      <category>adtech</category>
    </item>
    <item>
      <title>SSAI VAST Ingest vs CSAI Ad Tag: The Fill Gap Your Browser QA Cannot See</title>
      <dc:creator>Aleksander Sekowski</dc:creator>
      <pubDate>Wed, 04 Nov 2026 00:03:13 +0000</pubDate>
      <link>https://dev.to/aleksuix/ssai-vast-ingest-vs-csai-ad-tag-the-fill-gap-your-browser-qa-cannot-see-57i8</link>
      <guid>https://dev.to/aleksuix/ssai-vast-ingest-vs-csai-ad-tag-the-fill-gap-your-browser-qa-cannot-see-57i8</guid>
      <description>&lt;p&gt;IAB Tech Lab finalized &lt;a href="https://iabtechlab.com/whats-new-in-ctv-ad-format-signaling/" rel="noopener noreferrer"&gt;CTV Ad Portfolio signaling&lt;/a&gt; in July 2026 with a blunt operational requirement: SSAI vendors, exchanges, and measurement partners have to carry the same VAST and AdCOM enumerations as client-side players. That is not a cosmetic metadata upgrade. It is an admission that two delivery rails read the same campaign name and not the same creative contract. One rail fetches a tag in the living room. The other pulls a master into a transcode farm before the break airs. When fill drops on CTV but desktop web looks fine, the mismatch is usually between those two payloads, not a mysterious SSP bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  What SSAI ingest actually consumes
&lt;/h2&gt;

&lt;p&gt;In server-side ad insertion, the ad decision may still start from a VAST URL, but the money moment is transcode and splice. The stitcher wants a mezzanine or a high-bitrate master it can normalize to the publisher's ladder, a &lt;code&gt;Duration&lt;/code&gt; that fits the pod slot, HTTPS-delivered media, and often a &lt;code&gt;UniversalAdId&lt;/code&gt; block so downstream reconciliation can tie the stitched bytes back to the trafficking row. VPAID and other script-based &lt;code&gt;apiFramework&lt;/code&gt; values are dead on arrival: there is no JavaScript runtime in the packager.&lt;/p&gt;

&lt;p&gt;That is a different readiness bar than "the IMA SDK played an MP4 in Chrome." A tag can validate against VAST 4.x, expose a single progressive &lt;code&gt;MediaFile&lt;/code&gt;, and never include &lt;code&gt;&amp;lt;Mezzanine&amp;gt;&lt;/code&gt; at all. Browsers do not need the mezzanine child; many SSAI pipelines treat its absence as a hard reject before the first frame is encoded. The same InLine can also declare a &lt;code&gt;Duration&lt;/code&gt; of &lt;code&gt;:30&lt;/code&gt; while the mezzanine asset is &lt;code&gt;:29.97&lt;/code&gt;, which passes XML schema checks and still breaks slot math on the stitcher.&lt;/p&gt;

&lt;p&gt;Tracking is part of the contract too. SSAI often fires quartile and complete URLs from the packager's clock, not from the player's JavaScript timer. If your QA only watches the client-side VAST response, you may never notice that midpoint or complete beacons were authored for a player that never runs in the SSAI path. The &lt;a href="https://vastlint.org/docs/vast-tracking-events/midpoint/" rel="noopener noreferrer"&gt;midpoint event doc&lt;/a&gt; is a reminder that those URLs are pixels on the wire whether the stitcher or the device fires them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What CSAI and browser QA actually test
&lt;/h2&gt;

&lt;p&gt;Client-side insertion is the path most trafficking teams know. You paste a VAST URL into a tag tester, the player selects a &lt;code&gt;MediaFile&lt;/code&gt;, and someone watches quartiles tick in the network panel. Wrappers get peeled until an InLine appears. Click-through and verification resources load in a real DOM.&lt;/p&gt;

&lt;p&gt;That workflow is valuable. It is also incomplete for any inventory that monetizes through SSAI on CTV. The tag the browser resolves may be a wrapper chain whose final InLine is optimized for desktop bitrates. The mezzanine the stitcher needed might live only on an upstream hop your QA never fetched, or never existed because the creative was built for CSAI-only supply. &lt;a href="https://vastlint.org/blog/how-to-validate-omid-in-vast-tags/" rel="noopener noreferrer"&gt;Verification markup&lt;/a&gt; can load in a web player while the stitched stream carries no measurable verification resource at all, because the transcode step stripped interactive files the living-room device never executes.&lt;/p&gt;

&lt;p&gt;Teams migrating off deprecated APIs feel this split directly. A &lt;a href="https://vastlint.org/guides/vast-vpaid-migration/" rel="noopener noreferrer"&gt;VPAID migration guide&lt;/a&gt; matters because the interactive file that survives browser QA is exactly what SSAI cannot run. Passing validation on the client tag is not evidence the CTV break will accept the creative.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the two paths disagree without looking "broken"
&lt;/h2&gt;

&lt;p&gt;Same trafficking ID, two environments, four common divergences:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;CSAI / browser QA&lt;/th&gt;
&lt;th&gt;SSAI ingest&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primary media&lt;/td&gt;
&lt;td&gt;First playable &lt;code&gt;MediaFile&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Mezzanine or approved master&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interactive API&lt;/td&gt;
&lt;td&gt;VPAID or SIMID may load&lt;/td&gt;
&lt;td&gt;Script APIs rejected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duration&lt;/td&gt;
&lt;td&gt;Player tolerance&lt;/td&gt;
&lt;td&gt;Pod slot and ad server clock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identity&lt;/td&gt;
&lt;td&gt;Optional in older tags&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;UniversalAdId&lt;/code&gt; for reconciliation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of these rows fail XML schema by themselves. That is why the problem shows up as silent fill loss on CTV while reporting still shows the creative as "ready."&lt;/p&gt;

&lt;p&gt;New portfolio formats make the split sharper. Pause, overlay, and squeezeback buys expect NonLinear VAST shapes and AdCOM placement signals that a linear-only CSAI tag never exercised. &lt;a href="https://vastlint.org/blog/prepare-vast-4-4-ctv-formats/" rel="noopener noreferrer"&gt;Preparing for VAST 4.4 CTV formats&lt;/a&gt; is about proving partner acceptance on the path you will actually use, not only proving a desktop wrapper parses. When you QA portfolio NonLinear delivery, the &lt;a href="https://iab-tech-lab-vast-tester.vastlint.org/" rel="noopener noreferrer"&gt;IAB-style VAST tester&lt;/a&gt; (an independent fork, not an IAB Tech Lab product) exercises SIMID studio and sample creatives alongside host transport logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How you catch it
&lt;/h2&gt;

&lt;p&gt;An LLM or MCP task can emit VAST that looks plausible and still miss mezzanine, duration drift, or HTTPS on the master URL. The wire contract does not move because the tool call returned OK. You still need a deterministic check on the creative the buy claimed.&lt;/p&gt;

&lt;p&gt;Start with the live tag URL in the &lt;a href="https://vastlint.org/tester/" rel="noopener noreferrer"&gt;VAST tag tester&lt;/a&gt;: fetch, preview, and confirm tracking and media URLs resolve the way a player would. Then run the same entry point through the &lt;a href="https://vastlint.org/inspect/" rel="noopener noreferrer"&gt;VAST inspector&lt;/a&gt; so wrapper hops surface the InLine the stitcher would see, not just the first response your browser cached.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;vastlint&lt;/code&gt; (&lt;code&gt;cargo install vastlint&lt;/code&gt;, &lt;code&gt;npm install vastlint&lt;/code&gt;, or the MCP server) applies versioned rules to the XML itself: mezzanine expectations on SSAI-bound tags, deprecated API frameworks, duration and media consistency. It is independent of IAB Tech Lab and of AgenticAdvertising.org; it is the package that owns the VAST payload after your agent or trafficking UI emits it.&lt;/p&gt;

&lt;p&gt;When verification vendors QA the tag separately from the video team, align on &lt;a href="https://vastlint.org/blog/verification-manager-checks-the-vast-tag/" rel="noopener noreferrer"&gt;what the verification manager actually checks&lt;/a&gt; versus what the stitcher transcodes. Measurement OK in a browser is not SSAI readiness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://vastlint.org/guides/vast-vpaid-migration/" rel="noopener noreferrer"&gt;VPAID migration for VAST 4.1+&lt;/a&gt; when interactive files still ride the tag SSAI will reject.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://vastlint.org/blog/how-to-validate-omid-in-vast-tags/" rel="noopener noreferrer"&gt;Validate OMID inside VAST AdVerifications&lt;/a&gt; before you assume CTV measurement survives transcode.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://vastlint.org/blog/prepare-vast-4-4-ctv-formats/" rel="noopener noreferrer"&gt;Prepare VAST 4.4 CTV formats&lt;/a&gt; before you commit portfolio traffic to partners that only tested linear CSAI tags.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fill on CTV is not a single tag. It is the ingest path you never opened in the browser.&lt;/p&gt;

</description>
      <category>vast</category>
      <category>ctv</category>
      <category>advertising</category>
      <category>ssai</category>
    </item>
    <item>
      <title>Persistent creative IDs make video campaign reconciliation easier to repeat.</title>
      <dc:creator>Aleksander Sekowski</dc:creator>
      <pubDate>Tue, 03 Nov 2026 17:00:00 +0000</pubDate>
      <link>https://dev.to/aleksuix/persistent-creative-ids-make-video-campaign-reconciliation-easier-to-repeat-49ep</link>
      <guid>https://dev.to/aleksuix/persistent-creative-ids-make-video-campaign-reconciliation-easier-to-repeat-49ep</guid>
      <description>&lt;p&gt;Originally published on &lt;a href="https://vastlint.org/blog/universaladid-campaign-reconciliation-cost/" rel="noopener noreferrer"&gt;VASTLint&lt;/a&gt;. Part 5 of 8 in this series on video standards and business outcomes.&lt;/p&gt;

&lt;p&gt;An agency receives four reports for one video campaign. Each system has assigned the creative a different local identifier. Someone opens a spreadsheet, matches names, checks durations and asks whether two similar filenames refer to the same version. The campaign's delivery may be healthy while the reporting process still depends on a person remembering how the assets were renamed.&lt;/p&gt;

&lt;p&gt;IAB Tech Lab's UniversalAdId and Ad Creative ID Framework, or ACIF, address the problem of identifying creative across a supply chain. Following that approach can support more dependable reconciliation and asset management. The business benefit comes when participants preserve and use the identity, rather than merely including an identifier-shaped value in one document.&lt;/p&gt;

&lt;h2&gt;
  
  
  A shared identifier needs to survive each handoff.
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://iabtechlab.com/standards/acif/" rel="noopener noreferrer"&gt;ACIF overview&lt;/a&gt; describes persistent creative identification and registration as a basis for workflows including reconciliation, frequency management and competitive separation. A VAST UniversalAdId supplies the registry context and creative identifier. These capabilities give systems something consistent to join on when their own campaign and creative keys differ.&lt;/p&gt;

&lt;p&gt;That join only works if the parties preserve the same identity and agree what it identifies. A local ad-server key can remain useful inside its own system. A file checksum can identify exact bytes. Neither necessarily represents the registered creative in the way a cross-platform workflow expects. Retain those different identifiers as separate fields and describe the mapping explicitly.&lt;/p&gt;

&lt;p&gt;For a reporting analyst at an agency, the practical improvement is a less ambiguous path from a delivery row back to the intended creative. The analyst can trace the registry and identifier through the submission and receiving reports. A missing value or conflicting mapping becomes an exception to investigate rather than a reason to manually reconstruct every creative relationship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identity errors can send work in the wrong direction.
&lt;/h2&gt;

&lt;p&gt;Consider an illustrative campaign with a national video and several local variants. If two different variants incorrectly share an identity, a report can collapse distinctions that the campaign team needs. If the same creative acquires unrelated identities along the path, the report can split one asset into several apparent creatives. Both outcomes can make an otherwise simple question expensive to answer.&lt;/p&gt;

&lt;p&gt;The question might be which version ran in a region, whether an old offer is still active or which asset should be replaced. Before changing delivery, compare the identifier with the actual source asset and the campaign's version record. An identity field provides a reference. It does not guarantee that the media file behind a URL is the intended version.&lt;/p&gt;

&lt;p&gt;This is why identity controls belong in the handoff process. Decide who requests or supplies the registered ID, who maps it to local systems and who confirms that a creative change requires an updated record. Preserve the mapping with the approved asset. A spreadsheet maintained by an unidentified person is a fragile dependency even when every cell currently happens to be correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequency control needs more than a creative identifier.
&lt;/h2&gt;

&lt;p&gt;A persistent ID can help a system recognize the creative it is considering. It does not independently reveal who has already seen the ad, how often a household should see it or whether two devices belong to the same household. Frequency policy requires additional permitted signals, state and decisioning. Creative identity is one input to that process.&lt;/p&gt;

&lt;p&gt;The same qualification applies to competitive separation. Identifying the ad consistently can help retrieve or associate relevant metadata. The decision still depends on the metadata, the rules and the system enforcing them. A valid UniversalAdId does not automatically prevent competing brands from appearing in the same break, and it does not establish that the brand classification is correct.&lt;/p&gt;

&lt;p&gt;When evaluating an integration, test the actual decision. Use a known creative mapping and an agreed expected outcome, then inspect the result in the destination system. If the system cannot show which identifier informed the decision, describe that gap. A clean document validation result alone cannot demonstrate that a downstream frequency or competitive-separation policy ran as intended.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first useful business metric is the unmatched share.
&lt;/h2&gt;

&lt;p&gt;Start by measuring how many relevant reporting rows can be linked automatically to an approved creative record. Define the population and the meaning of a successful match. Include conflicting matches and incorrectly merged records in the quality review. A higher match rate is not an improvement if it comes from treating unrelated assets as the same creative.&lt;/p&gt;

&lt;p&gt;Track the time spent resolving unmatched records and how often resolution requires another company. This can reveal a process improvement sooner than a campaign-level revenue metric. The useful outcome may be a report prepared earlier, fewer assets mislabeled or less effort answering a buyer's question. State which result was observed instead of combining them into a general claim of higher ROI.&lt;/p&gt;

&lt;p&gt;For illustration, assume a team processes 600 exception rows in a reporting period and a verified identity change removes 400 of them. At an assumed two minutes per row, the potential reduction is about 13.3 hours. Multiply by a measured labor rate only if a monetary estimate helps the decision. The example does not establish actual savings or a typical industry workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Correct mappings can reduce the cost of changing a campaign.
&lt;/h2&gt;

&lt;p&gt;A campaign adjustment often touches several systems at once. The operations team needs to know which local creative records correspond to the asset that must be paused, replaced or reviewed. A preserved cross-platform identity can make that search more reliable. It also gives a second reviewer a way to check that the intended creative was changed everywhere in scope.&lt;/p&gt;

&lt;p&gt;Measure the work required for a defined change, including the number of systems and the records inspected. Keep the result tied to the specific workflow. If a team reduces manual lookup steps but still needs publisher confirmation, it has improved part of the task. That is useful evidence without claiming that campaign updates are now automatic across the entire market.&lt;/p&gt;

&lt;p&gt;Revenue consequences require an additional record. If an old offer remained live, document its actual delivery and the commercial response. If faster identification enabled a replacement before a fixed deadline, retain the relevant times and what was delivered afterward. The presence of an identifier defect does not prove that the wrong asset ran or that any budget was wasted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate the declaration, then verify the identity behind it.
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://vastlint.org/blog/how-to-validate-universaladid-in-vast-tags/" rel="noopener noreferrer"&gt;VASTLint's UniversalAdId guide&lt;/a&gt; explains the declaration checks in VAST documents. Use the &lt;a href="https://vastlint.org/validate/" rel="noopener noreferrer"&gt;validator&lt;/a&gt; to inspect supplied values and their placement. A result can help identify an empty field, an invalid structure or another document issue before the tag is accepted into a campaign workflow.&lt;/p&gt;

&lt;p&gt;A document check is not a registry lookup, proof of ownership or confirmation that the ID belongs to the referenced video. Those questions need the relevant registration and asset records. The &lt;a href="https://github.com/InteractiveAdvertisingBureau/vast/blob/master/VAST%20CTV%20Addendum%202024.md" rel="noopener noreferrer"&gt;VAST CTV Addendum 2024&lt;/a&gt; also describes ways to carry newer capabilities through legacy VAST versions. Confirm which representation the actual receiver supports.&lt;/p&gt;

&lt;p&gt;Keep a test in which a known identity survives the path from submission to the receiving report. Add a case that deliberately introduces a conflicting mapping and confirm that the workflow exposes it. Retain the original documents and report rows privately where necessary. The evidence should show that the intended identity reached the system that needs to act on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The business asset is a maintained creative record.
&lt;/h2&gt;

&lt;p&gt;Assign an owner to the mapping and give changes a review process. Preserve the approved identity, source asset, local system keys and relevant campaign version together. This creates a record another operator can use when the original campaign manager is unavailable. It also makes recurring discrepancies easier to identify without relying on naming conventions that partners may change.&lt;/p&gt;

&lt;p&gt;Review the exceptions after introducing persistent identification. Count unresolved mappings, time spent on reconciliation and errors found in completed reports. If the workload changes, normalize the comparison to a relevant campaign or row population. Report whether the gain came from a technical mapping change, a cleanup project or a different mix of campaigns.&lt;/p&gt;

&lt;p&gt;Persistent creative IDs provide a common reference that businesses can build on. The operational value appears when reports join correctly, changes reach the right assets and teams can explain exceptions with less reconstruction. Those outcomes are measurable. Broader claims about reduced waste or better campaign performance need the downstream evidence that connects the identity improvement to what actually happened.&lt;/p&gt;

</description>
      <category>adtech</category>
      <category>webdev</category>
      <category>testing</category>
      <category>performance</category>
    </item>
    <item>
      <title>11 Curated Deal Mistakes That Clear as Open Auction and Kill Fill</title>
      <dc:creator>Aleksander Sekowski</dc:creator>
      <pubDate>Tue, 03 Nov 2026 00:01:52 +0000</pubDate>
      <link>https://dev.to/aleksuix/11-curated-deal-mistakes-that-clear-as-open-auction-and-kill-fill-1op9</link>
      <guid>https://dev.to/aleksuix/11-curated-deal-mistakes-that-clear-as-open-auction-and-kill-fill-1op9</guid>
      <description>&lt;p&gt;IAB Tech Lab marked the &lt;a href="https://ppc.land/private-marketplace/" rel="noopener noreferrer"&gt;Deals API&lt;/a&gt; at version 1.0 in February 2026, and trade coverage picked it up again in late October. The API finally names the curator, the origin seller, and the curation fee type. None of that rides on the live bid request. On the wire you still get &lt;code&gt;imp.pmp.deals&lt;/code&gt; with a deal &lt;code&gt;id&lt;/code&gt;, a floor, and maybe &lt;code&gt;wseat&lt;/code&gt;. The audience filter, the inventory bundle, and the data fee you negotiated live upstream, or in a spreadsheet, or in the DSP deal UI. When that upstream layer slips, the auction can clear as open traffic while finance still books a curated CPM. Fill drops because you are bidding the wrong auction, and money leaks because you are paying for packaging that never reached the impression.&lt;/p&gt;

&lt;p&gt;Here are eleven ways that happens in production.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;private_auction&lt;/code&gt; left at 0 on a deal-only buy.&lt;/strong&gt; OpenRTB 2.6 defaults &lt;code&gt;pmp.private_auction&lt;/code&gt; to 0, which means outside bidders may compete. A curated package sold as exclusive should carry &lt;code&gt;private_auction: 1&lt;/code&gt; so only listed deals qualify. If the SSP omits the flag, your deal bid can win, but so can anyone else on the open exchange at a lower price. You thought you bought a walled garden; the request says otherwise.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deal Sync metadata promises a segment the bid request cannot prove.&lt;/strong&gt; The Deals API and Deal Sync objects can describe segments, inventory lists, and curator roles. The bid request has no field that says "this impression passed filter X." You validate the deal id in the DSP, not the filter graph. When the curator loosens targeting to recover volume, the first signal is often a win without the audience you sold to the client.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;dealid&lt;/code&gt; missing on the bid even though &lt;code&gt;imp.pmp.deals&lt;/code&gt; listed your deal.&lt;/strong&gt; The response must echo the deal id when you transact against an in-request deal. Without it, reporting attributes spend to open auction, pacing rules misfire, and loss analytics blame creative filters instead of deal mismatch. Loss reason 213 (not allowed in PMP deal) is real, but so is the quieter case: you win, and finance cannot tie the impression back to the curated line item.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multiple deals in the array when the buyer activated one.&lt;/strong&gt; Some exchanges broadcast every PMP they could serve on an impression. Your seat bids deal A; the request also advertises deals B and C for other buyers. That is legal OpenRTB, but it confuses deal-level QA if you sample one id and assume the path is exclusive. You optimize against the wrong floor and wonder why margin disappeared.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;wseat&lt;/code&gt; or &lt;code&gt;wadomain&lt;/code&gt; stripped between curator and exchange.&lt;/strong&gt; Deal objects can restrict which buyer seats or advertiser domains may bid. Curators sometimes normalize requests for latency and drop optional arrays. Your bid goes in from an allowed seat, loses to a cheaper open bidder, and the seller swears the deal was "activated." The payload tells a different story.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Impression-level &lt;code&gt;bidfloor&lt;/code&gt; undercuts the per-deal floor you synced.&lt;/strong&gt; Floors can live on &lt;code&gt;imp.bidfloor&lt;/code&gt; and again on each &lt;code&gt;pmp.deals[].bidfloor&lt;/code&gt;. A curated package often sets a higher deal floor to cover data and curation fees. If only the impression floor survives the hop, open auction bids above the low floor and below your economic deal floor steal wins you cannot serve profitably.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Duration floors on video deals ignored after curation repackaging.&lt;/strong&gt; OpenRTB 2.6 dated snapshots added &lt;code&gt;durfloors&lt;/code&gt; and related video deal pricing. A curator bundles long-form CTV with a 30-second creative requirement. The repackaged request drops &lt;code&gt;durfloors&lt;/code&gt; because the downstream exchange runs an older profile. Your bidder passes schema checks; the player rejects the creative length.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Curator fee embedded in UI, invisible on the request.&lt;/strong&gt; TripleLift’s October 2026 Uber audience packages and Multilocal’s FouAnalytics curated PMPs both describe bundled data plus inventory. Neither OpenRTB nor the winning bid exposes how much of the CPM is inventory versus data versus curation margin. When fill softens, buyers cannot tell whether to renegotiate the package or fix targeting.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Supply-side targeting miscounted as "curation."&lt;/strong&gt; OpenX’s 2026 reporting split audience and content targeting from inventory-only curation. Deals sold as curated bundles may be executed as supply-side targeting packages with different fee and auction rules. Your operational runbook says PMP; the SSP’s internal taxonomy says something else. Spend lands on the wrong report row.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deal id rotation without DSP sync.&lt;/strong&gt; Curators rotate ids after package changes. The Deals API row updates; your DSP still bids last month’s id. The exchange treats the bid as non-deal or maps it to a generic PMP with weaker filters. Wins look fine in aggregate until you audit domain and app bundle mix.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Protobuf JSON &lt;code&gt;privateAuction: false&lt;/code&gt; on a gRPC hop.&lt;/strong&gt; On gRPC bid streams the same flag serializes as boolean camelCase. A spec-JSON validator says &lt;code&gt;private_auction: 1&lt;/code&gt;; a proto hop silently reads open auction if the field name or type drifts. Curated exclusivity dies in transport, not in the sales contract.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How you catch it
&lt;/h2&gt;

&lt;p&gt;An LLM wiring AdCP, ARTF, or AAMP can emit an OpenRTB request that parses clean and still misrepresents the curated buy. The RPC or MCP task can return OK while &lt;code&gt;private_auction&lt;/code&gt;, deal floors, and &lt;code&gt;dealid&lt;/code&gt; are wrong for what finance sold.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://rtblint.org" rel="noopener noreferrer"&gt;RTBlint&lt;/a&gt; is the deterministic check on that object: &lt;code&gt;imp.pmp.deals&lt;/code&gt;, per-deal floors, seat restrictions, and the bid response &lt;code&gt;dealid&lt;/code&gt;. It is independent of IAB Tech Lab and of AgenticAdvertising.org; the spec does not require it, but someone has to diff the live request against the deal terms you think you bought.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;rtblint validate&lt;/code&gt; on sampled bid requests from the curated line item, or &lt;code&gt;npm install rtblint-core&lt;/code&gt; in CI on fixtures your SSP provides. For ARTF paths that mutate deals, validate the patched request before it hits the exchange, not only the agent’s success bit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://rtblint.org/answers/what-is-curation-in-programmatic/" rel="noopener noreferrer"&gt;What is curation in programmatic?&lt;/a&gt; walks how a curated package collapses to an ordinary deal id on the wire.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://rtblint.org/docs/openrtb/floors/" rel="noopener noreferrer"&gt;Bid floors in OpenRTB&lt;/a&gt; covers impression floors versus &lt;code&gt;pmp.deals[].bidfloor&lt;/code&gt; and video duration floors.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://rtblint.org/blog/pmp-vs-preferred-deal-vs-programmatic-guaranteed/" rel="noopener noreferrer"&gt;PMP vs preferred deal vs programmatic guaranteed&lt;/a&gt; separates auction types so you know when open competition is allowed by design.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://rtblint.org/blog/openx-ctv-sst-share-sorts-deals-not-bid-fields/" rel="noopener noreferrer"&gt;OpenX CTV supply-side targeting vs curation&lt;/a&gt; shows why the word "curation" on a rate card may not match IAB’s taxonomy.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://rtblint.org/blog/dark-pooling-authorized-seller-id/" rel="noopener noreferrer"&gt;Dark pooling and authorized seller id&lt;/a&gt; is a supply-path angle when the curated deal’s seller chain does not match the inventory you thought you bought.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>openrtb</category>
      <category>advertising</category>
      <category>adtech</category>
      <category>programmatic</category>
    </item>
    <item>
      <title>An OpenRTB bid floor keeps its own currency.</title>
      <dc:creator>Aleksander Sekowski</dc:creator>
      <pubDate>Mon, 02 Nov 2026 19:00:00 +0000</pubDate>
      <link>https://dev.to/aleksuix/an-openrtb-bid-floor-keeps-its-own-currency-1if1</link>
      <guid>https://dev.to/aleksuix/an-openrtb-bid-floor-keeps-its-own-currency-1if1</guid>
      <description>&lt;p&gt;A bid request can allow EUR responses while an impression's floor is still denominated in USD. If an integration reads the response currency list and silently applies it to the floor, its arithmetic can look correct while comparing different units.&lt;/p&gt;

&lt;p&gt;The problem is easy to miss because both values are ordinary JSON numbers. Their meaning depends on the currency attached to each field.&lt;/p&gt;

&lt;p&gt;This walkthrough uses OpenRTB 2.6's June 2026 specification snapshot. The examples focus on currency handling and are not complete bid requests or production pricing logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Follow the field that owns the currency
&lt;/h2&gt;

&lt;p&gt;The request's &lt;code&gt;cur&lt;/code&gt; field describes allowed bid response currencies. The impression's &lt;code&gt;bidfloorcur&lt;/code&gt; describes the currency of that impression's floor. In the referenced snapshot, the latter defaults to USD when omitted.&lt;/p&gt;

&lt;p&gt;Those are separate contracts. Setting &lt;code&gt;cur&lt;/code&gt; to &lt;code&gt;["EUR"]&lt;/code&gt; does not rewrite an omitted &lt;code&gt;bidfloorcur&lt;/code&gt; to EUR.&lt;/p&gt;

&lt;p&gt;Consider this fragment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cur"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"imp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"video-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"bidfloor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the impression floor, the omitted currency resolves to USD under the cited specification. A bid price expressed in EUR needs an explicit conversion before a numerical floor comparison makes sense.&lt;/p&gt;

&lt;p&gt;There is another scope to review when deals are involved. A deal has its own floor fields. Inspect the applicable deal and exchange contract rather than applying an impression-level assumption everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve the amount and currency together
&lt;/h2&gt;

&lt;p&gt;Avoid passing a bare number called &lt;code&gt;floor&lt;/code&gt; through several functions. Use a structure that carries the amount, currency and source. That makes it harder for a later step to lose the unit or confuse an explicit value with a default.&lt;/p&gt;

&lt;p&gt;The following complete Node.js example performs a narrow comparison. The exchange rate is synthetic and is supplied by the caller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;assert&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:assert/strict&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;impressionFloor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;imp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bidfloor&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;imp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bidfloor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;imp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bidfloorcur&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;imp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bidfloorcur&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid floor amount&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EUR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unsupported currency in this example&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;currencySource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;imp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bidfloorcur&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;explicit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;convertFloor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;usable conversion rate missing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EUR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;imp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;video-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bidfloor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;3.2&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;floor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;impressionFloor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;imp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USD:EUR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;floorInEUR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;convertFloor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EUR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currencySource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;floorInEUR&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;2.88&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;floorInEUR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;throws&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;convertFloor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EUR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}));&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;throws&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;impressionFloor&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;bidfloor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;3.20&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;throws&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;impressionFloor&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;bidfloor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;impressionFloor&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;bidfloor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two comparisons deliberately produce different answers. With this invented rate, a EUR 3.00 bid exceeds the converted EUR 2.88 floor. Comparing the same bid directly with the unconverted USD 3.20 number would reject it.&lt;/p&gt;

&lt;p&gt;The example supports only two currencies so that unsupported inputs fail visibly. A production implementation needs a maintained currency set, a rate source and an explicit rounding policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the direction and age of the rate
&lt;/h2&gt;

&lt;p&gt;A rate named &lt;code&gt;0.9&lt;/code&gt; is incomplete. State that it means EUR per USD, record when it became effective and identify the source. Multiplying by an inverse rate can produce plausible values without immediately revealing the error.&lt;/p&gt;

&lt;p&gt;Attach that metadata to the pricing decision or a retrievable rate snapshot. A later replay should use the rate policy that applied to the original decision, rather than whichever rate happens to be current when a developer investigates it.&lt;/p&gt;

&lt;p&gt;Do not treat a missing rate as one-to-one conversion. Decide whether the application should skip the impression, stop the calculation or route the case for review. That decision is business policy and should appear in the diagnostic result.&lt;/p&gt;

&lt;p&gt;Likewise, do not round a floor for display and reuse the displayed number in auction logic. Define the precision and comparison behavior required by your integration. Binary floating point, exchange rounding rules and boundary prices need deliberate treatment beyond this small example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test defaults and explicit values separately
&lt;/h2&gt;

&lt;p&gt;An omitted currency and an explicit USD currency can lead to the same effective value while carrying different diagnostic meaning. Preserve which path was taken so you can find producers that rely on defaults unexpectedly.&lt;/p&gt;

&lt;p&gt;Include fixtures for a missing floor, a zero floor, an explicit non-USD currency and a malformed numeric string. Zero must survive as zero. A truthiness check such as &lt;code&gt;amount || fallback&lt;/code&gt; can replace a legitimate value.&lt;/p&gt;

&lt;p&gt;Then add a request with multiple impressions and different floor currencies. This catches helpers that compute one request-wide currency and apply it to every impression.&lt;/p&gt;

&lt;p&gt;For deal traffic, keep dedicated cases with the deal identifier and applicable floor fields. The test should establish which floor the integration selected before it tests the comparison arithmetic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debug a rejected bid with the normalized record
&lt;/h2&gt;

&lt;p&gt;When a bid appears to clear a floor but is rejected, collect the matched impression or deal, raw floor amount, effective floor currency, response currency, conversion snapshot and final comparison value.&lt;/p&gt;

&lt;p&gt;Check the units as well as the currency. OpenRTB pricing uses CPM conventions in these fields. An amount interpreted as a per-impression charge introduces a separate scale error that currency conversion cannot fix.&lt;/p&gt;

&lt;p&gt;Finally, distinguish your local eligibility calculation from the exchange's auction result. Clearing this one floor check does not establish that the bid is otherwise valid or that it should win.&lt;/p&gt;

&lt;p&gt;I maintain RTBlint. Its &lt;a href="https://rtblint.org/guides/common-openrtb-mistakes/?utm_source=dev.to&amp;amp;utm_medium=referral&amp;amp;utm_campaign=reference-series-2026&amp;amp;utm_content=openrtb-floor-currency-test"&gt;OpenRTB mistakes reference&lt;/a&gt; is useful for reviewing the surrounding request and response contract. Keep the currency decision in your own replay fixture so that a pricing investigation can reproduce the exact comparison.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/InteractiveAdvertisingBureau/openrtb2.x/blob/2.6-202606/2.6.md" rel="noopener noreferrer"&gt;OpenRTB 2.6-202606, BidRequest and Imp&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Technical sources checked October 25, 2026. Drafted with AI; examples are synthetic unless explicitly stated otherwise.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>openrtb</category>
      <category>json</category>
      <category>testing</category>
      <category>adtech</category>
    </item>
    <item>
      <title>The AdCP OpenRTB and VAST Mapping Cheat Sheet Every Agentic Bid Ops Engineer Needs</title>
      <dc:creator>Aleksander Sekowski</dc:creator>
      <pubDate>Mon, 02 Nov 2026 00:39:03 +0000</pubDate>
      <link>https://dev.to/aleksuix/the-adcp-openrtb-and-vast-mapping-cheat-sheet-every-agentic-bid-ops-engineer-needs-2g82</link>
      <guid>https://dev.to/aleksuix/the-adcp-openrtb-and-vast-mapping-cheat-sheet-every-agentic-bid-ops-engineer-needs-2g82</guid>
      <description>&lt;p&gt;On October 22, 2026 &lt;a href="https://agenticadvertising.org/perspectives/october-town-hall-protocol-advances-new-ways-to-build-and-introducing-our-board-mvis1aq7" rel="noopener noreferrer"&gt;AgenticAdvertising.org closed its October town hall&lt;/a&gt; with AdCP 3.2 in production and agent-to-agent TV buys moving from demos to repeat campaigns. Fox and Simulmedia alone reported more than 300 spot-level orders executed over A2A this quarter. Every one of those stories ends the same way in engineering: a media buy object, then an ad server or exchange that speaks OpenRTB, then a player that still reads VAST. The MCP task can return OK while the wire objects drift.&lt;/p&gt;

&lt;p&gt;This sheet is for engineers wiring buyer agents, sell-side execution, or Prebid-style real-time modules: the people who need to know which AdCP field is supposed to show up as which integer, deal id, or XML child before traffic scales.&lt;/p&gt;

&lt;h2&gt;
  
  
  AdCP negotiation fields and where they must land
&lt;/h2&gt;

&lt;p&gt;AdCP names constraints in agent tasks (&lt;code&gt;get_products&lt;/code&gt;, &lt;code&gt;create_media_buy&lt;/code&gt;, &lt;code&gt;sync_creatives&lt;/code&gt;). Programmatic execution still uses OpenRTB on the auction hop and VAST on the creative hop. The specification defines several enums by reference to OpenRTB 2.6 and AdCOM; treat the table as the contract your emit layer must satisfy.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;AdCP surface&lt;/th&gt;
&lt;th&gt;Wire target&lt;/th&gt;
&lt;th&gt;What the buyer expects&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;video_placement_types&lt;/code&gt; (e.g. &lt;code&gt;instream&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;imp.video.plcmt&lt;/code&gt; (1–4)&lt;/td&gt;
&lt;td&gt;Same placement class the product filter selected; not deprecated &lt;code&gt;video.placement&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;audio_distribution_types&lt;/code&gt; (e.g. &lt;code&gt;podcast&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;imp.audio.feed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AdCOM feed type on the impression that clears the auction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Media buy / package deal reference&lt;/td&gt;
&lt;td&gt;&lt;code&gt;imp.pmp.deals[].id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bid &lt;code&gt;dealid&lt;/code&gt; must name a deal actually offered on that impression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Negotiated price and auction type&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;deals[].bidfloor&lt;/code&gt;, &lt;code&gt;bidfloorcur&lt;/code&gt;, &lt;code&gt;at&lt;/code&gt;, &lt;code&gt;guar&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Deal floor beats impression floor; PG vs open auction must match the buy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Creative format from &lt;code&gt;sync_creatives&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;imp.video&lt;/code&gt; / &lt;code&gt;imp.audio&lt;/code&gt; mimes, duration, protocols&lt;/td&gt;
&lt;td&gt;Creative accepted upstream must fit the emitted impression or loss reasons 203/204 apply&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duration-priced video&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;imp.video.durfloors&lt;/code&gt;, &lt;code&gt;mincpmpersec&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Pod pricing fields require pod context on the impression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VAST asset from creative tasks&lt;/td&gt;
&lt;td&gt;VAST &lt;code&gt;InLine&lt;/code&gt; / &lt;code&gt;Wrapper&lt;/code&gt;, &lt;code&gt;Duration&lt;/code&gt;, &lt;code&gt;MediaFile&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Agentic brief does not replace a servable tag the player can fetch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;adagents.json&lt;/code&gt; authorization&lt;/td&gt;
&lt;td&gt;Paired with &lt;code&gt;source.schain&lt;/code&gt; at serve time&lt;/td&gt;
&lt;td&gt;Agent allowed to sell ≠ chain complete on this impression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AXE segment response (&lt;code&gt;axei&lt;/code&gt;, &lt;code&gt;axex&lt;/code&gt;, &lt;code&gt;axem&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;OpenRTB user/data and substituted creative URLs&lt;/td&gt;
&lt;td&gt;Malformed AXE context fails segment lookup quietly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One cell is the example people paste wrong daily: &lt;strong&gt;&lt;code&gt;instream&lt;/code&gt; negotiated on the buy but emitted as &lt;code&gt;video.placement&lt;/code&gt; instead of &lt;code&gt;video.plcmt&lt;/code&gt;&lt;/strong&gt;. AdCP names align with &lt;code&gt;plcmt&lt;/code&gt;; the older field uses a different enum. Schema validators that only know XML or a loose JSON schema will not catch the semantic swap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rows that pass agent tests and still break fill
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;video_placement_types: ["instream"]&lt;/code&gt; with only &lt;code&gt;placement: 1&lt;/code&gt; on the bid request.&lt;/strong&gt; The integer might look plausible. Buyers filtered on &lt;code&gt;plcmt&lt;/code&gt; semantics. Downstream reporting that reads AdCOM placement class sees a deprecated signal or none at all. The migration table lives in &lt;a href="https://rtblint.org/guides/placement-vs-plcmt/" rel="noopener noreferrer"&gt;placement versus plcmt&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;create_media_buy&lt;/code&gt; success with a deal id never copied into &lt;code&gt;imp.pmp.deals&lt;/code&gt;.&lt;/strong&gt; The agreement exists in the agent transcript. The auction request offers no private deal. Bids that cite the negotiated id die with loss reason 4. Nothing in AdCP task JSON proves the emit layer ran.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;sync_creatives&lt;/code&gt; OK while the VAST asset URL 404s or omits &lt;code&gt;Duration&lt;/code&gt;.&lt;/strong&gt; Creative governance in AdCP is a different document from the tag the SSAI stitcher or client player pulls. I run those URLs through the &lt;a href="https://vastlint.org/tester/" rel="noopener noreferrer"&gt;VAST tag tester&lt;/a&gt; and wrapper chains through the &lt;a href="https://vastlint.org/inspect/" rel="noopener noreferrer"&gt;VAST inspector&lt;/a&gt; because MCP success on the creative task does not fetch the tag.&lt;/p&gt;

&lt;h2&gt;
  
  
  How you catch it
&lt;/h2&gt;

&lt;p&gt;Split the check by payload owner. &lt;strong&gt;rtblint&lt;/strong&gt; (&lt;code&gt;cargo install rtblint&lt;/code&gt;, &lt;code&gt;npm install rtblint-core&lt;/code&gt;, MCP &lt;code&gt;rtblint-mcp&lt;/code&gt;) validates the OpenRTB bid request and the bid response against the request that produced it, including AdCP-oriented discovery via &lt;code&gt;get_adcp_capabilities&lt;/code&gt;. &lt;strong&gt;vastlint&lt;/strong&gt; (&lt;code&gt;cargo install vastlint&lt;/code&gt;, &lt;code&gt;npm install vastlint&lt;/code&gt;) validates the VAST or VMAP creative the buy claimed. An implementation that lets a model emit either object still needs that deterministic pass: tool success and RPC OK are not the wire contract. These packages are independent of IAB Tech Lab and of AAO; the specs do not require them.&lt;/p&gt;

&lt;p&gt;Practical loop: fixture one product type per placement class, validate the serialized bytes at the emit boundary, then validate the creative URL before launch. For agent stacks, call validation as an MCP tool in the same turn you assemble the request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Field-level walkthrough of the media-buy handoff: &lt;a href="https://rtblint.org/guides/adcp-to-openrtb/" rel="noopener noreferrer"&gt;AdCP to OpenRTB mapping guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;How AdCP sits above VAST and OpenRTB without replacing them: &lt;a href="https://vastlint.org/docs/adcp/" rel="noopener noreferrer"&gt;AdCP and VAST on vastlint.org&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;When the exchange runs an older dated snapshot than your agent: &lt;a href="https://rtblint.org/blog/openrtb-version-mismatch/" rel="noopener noreferrer"&gt;OpenRTB version mismatch&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;AdCOM asset subtype gaps on 2.6 impressions: &lt;a href="https://rtblint.org/docs/rule-reference/adcom-assetformat-subtype_required/" rel="noopener noreferrer"&gt;assetformat subtype required&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;MCP tools and capability discovery: &lt;a href="https://rtblint.org/docs/mcp/" rel="noopener noreferrer"&gt;rtblint MCP docs&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agentic buying moved the negotiation up a layer. Fill still reads the same objects it always did.&lt;/p&gt;

</description>
      <category>adcp</category>
      <category>openrtb</category>
      <category>vast</category>
      <category>advertising</category>
    </item>
    <item>
      <title>Creative Readiness Explained: How VAST Becomes Servable on SSAI and CTV</title>
      <dc:creator>Aleksander Sekowski</dc:creator>
      <pubDate>Sun, 01 Nov 2026 21:37:34 +0000</pubDate>
      <link>https://dev.to/aleksuix/creative-readiness-explained-how-vast-becomes-servable-on-ssai-and-ctv-2b75</link>
      <guid>https://dev.to/aleksuix/creative-readiness-explained-how-vast-becomes-servable-on-ssai-and-ctv-2b75</guid>
      <description>&lt;p&gt;On 26 October 2026, TF1 PUB said its streaming pause format AdPause would sell through programmatic pipes, not only direct deals (&lt;a href="https://videoweek.com/2026/10/26/pause-ads-go-programmatic-on-tf1/" rel="noopener noreferrer"&gt;VideoWeek&lt;/a&gt;). That is the same shift Netflix and other CTV sellers pushed earlier in the year: the buyer keeps using the same DSP, but the creative arrives as VAST chosen at auction time. The XML still has to survive a server-side insertion path and a TV-class player. Schema validation is the floor. &lt;strong&gt;Creative readiness&lt;/strong&gt; is whether the tag matches the environment that was bought.&lt;/p&gt;

&lt;p&gt;If you have only trafficked browser pre-roll, "valid VAST" feels like a green check in a QA tool. On SSAI and most CTV stacks, the stitcher may fetch your wrapper chain, pull a mezzanine master, transcode renditions, and only then hand the player a MediaFile. The player schedules pods from &lt;code&gt;&amp;lt;Duration&amp;gt;&lt;/code&gt;, refuses cleartext media when the app is HTTPS-only, and skips interactive APIs that never had a JavaScript runtime on the device. None of that is optional polish. It is the servability contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the stitcher reads that your browser tag never needed
&lt;/h2&gt;

&lt;p&gt;Server-side ad insertion exists so the manifest stays smooth. The ad decision might still be OpenRTB on the buy side, but the delivery hop is VAST: wrappers collapse to an InLine, media is prepared ahead of the break, and impressions often fire from infrastructure that looks like a data center rather than a laptop browser.&lt;/p&gt;

&lt;p&gt;VAST 4.x separates &lt;strong&gt;playback renditions&lt;/strong&gt; from &lt;strong&gt;source masters&lt;/strong&gt;. &lt;code&gt;&amp;lt;MediaFile&amp;gt;&lt;/code&gt; is what the player (or the stitcher's output manifest) is supposed to play: &lt;code&gt;delivery&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;width&lt;/code&gt;, &lt;code&gt;height&lt;/code&gt;, and a reachable HTTPS URL. &lt;code&gt;&amp;lt;Mezzanine&amp;gt;&lt;/code&gt; is the high-quality source the SSAI platform uses to generate those renditions. IAB's CTV programmatic guidance treats mezzanine as the asset that lets transcoding happen before the impression window closes. A tag that only lists a single progressive MP4 might be fine on desktop CSAI. On a major SSAI operator, the same tag can be rejected because there is no mezzanine to feed the transcoder, or because the mezzanine URL is HTTP on an HTTPS-only fetch policy.&lt;/p&gt;

&lt;p&gt;The gap shows up in attributes, not only in missing elements. A mezzanine node with a URL but without the width, height, or type metadata the operator documents is still a failed handoff: the stitcher cannot queue a job it cannot describe. The &lt;a href="https://vastlint.org/docs/vast-examples/mezzanine-missing-attrs/" rel="noopener noreferrer"&gt;mezzanine missing attributes example&lt;/a&gt; is the shape of that failure: legal XML, unusable master.&lt;/p&gt;

&lt;h2&gt;
  
  
  Duration, codec, identity, and transport
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Duration&lt;/strong&gt; is how pod logic knows whether the spot fits the break. SSAI systems allocate slot length from the declared &lt;code&gt;&amp;lt;Duration&amp;gt;&lt;/code&gt; in the InLine that will actually play. A wrapper that never resolves, or an InLine whose duration disagrees with the mezzanine by more than the operator tolerance, produces empty avails or mid-roll overruns. Players on CTV also use duration for quartile tracking even when the stitcher normalized the file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Codec and container&lt;/strong&gt; ride on &lt;code&gt;MediaFile@type&lt;/code&gt; and on what the device decode chain accepts. H.264 in an MP4 container is still the common denominator on living-room hardware, but operators publish allow lists. A creative that validates as &lt;code&gt;video/mp4&lt;/code&gt; while the bytes are an unsupported profile, or a mezzanine the transcoder cannot ingest, dies after the auction with a transcode error your DSP never sees.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTTPS&lt;/strong&gt; is not pedantry. Many players and stitchers refuse to fetch &lt;code&gt;http://&lt;/code&gt; media when the app context is secure. A single cleartext redirect in the chain fails the whole creative even when every XML element is present.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UniversalAdId&lt;/strong&gt; ties the served asset back to the record trafficking, finance, and brand safety expect. VAST 4.x places &lt;code&gt;&amp;lt;UniversalAdId&amp;gt;&lt;/code&gt; under &lt;code&gt;&amp;lt;Creative&amp;gt;&lt;/code&gt; with a registry such as &lt;code&gt;ad-id.org&lt;/code&gt;. Duplicate IDs across flights, missing registry, or an ID that does not match the CDN object name are readiness bugs: the spot might play once while downstream systems treat it as the wrong creative. The &lt;a href="https://vastlint.org/docs/universal-ad-id/" rel="noopener noreferrer"&gt;Universal Ad Id guide&lt;/a&gt; walks the element and registry pairing practitioners still mix up.&lt;/p&gt;

&lt;p&gt;Interactive APIs are part of readiness too. VPAID as an &lt;code&gt;apiFramework&lt;/code&gt; on a linear &lt;code&gt;&amp;lt;MediaFile&amp;gt;&lt;/code&gt; was deprecated in VAST 4.1 and has no runtime on SSAI or on most CTV devices. Tags that still declare VPAID validate against older samples and fail at serve time. If you are migrating off script-based units, the &lt;a href="https://vastlint.org/blog/vpaid-is-deprecated-stop-treating-it-like-a-fallback/" rel="noopener noreferrer"&gt;VPAID deprecation write-up&lt;/a&gt; is the policy context; readiness work is replacing the script path with SIMID or plain video plus &lt;code&gt;&amp;lt;TrackingEvents&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the buy signal meets the tag
&lt;/h2&gt;

&lt;p&gt;Programmatic CTV adds a second layer: the &lt;strong&gt;format signal&lt;/strong&gt; on the bid must match the VAST shape. Pause, overlay, and squeezeback placements use NonLinear or companion structures with AdCOM extensions for &lt;code&gt;plcmt&lt;/code&gt;, &lt;code&gt;pos&lt;/code&gt;, and &lt;code&gt;playbackmethod&lt;/code&gt;. A linear InLine trafficked into a pause deal validates as VAST and fails the player because the environment expected a static or NonLinear unit. TF1's note about differing seller specs is the business version of the same problem: one campaign ID cannot assume every publisher accepts the same NonLinear timing or asset type. Our &lt;a href="https://vastlint.org/blog/netflix-programmatic-pause-ads-nonlinear-vast/" rel="noopener noreferrer"&gt;Netflix programmatic pause ads and NonLinear VAST&lt;/a&gt; post is the sibling pattern for another publisher surface; the readiness lesson is identical: narrowest route wins, and the tag must encode that route.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://vastlint.org/blog/ctv-ad-portfolio-signaling-vast-validation/" rel="noopener noreferrer"&gt;CTV Ad Portfolio signaling and validation&lt;/a&gt; article connects portfolio metadata to the VAST payload: the signal tells the player which format was bought, but it does not transcode your mezzanine or fix HTTP media.&lt;/p&gt;

&lt;h2&gt;
  
  
  How you catch it before fill drops
&lt;/h2&gt;

&lt;p&gt;Start with a live fetch, not a pasted fragment from last year's template. The &lt;a href="https://vastlint.org/tester/" rel="noopener noreferrer"&gt;VAST tag tester&lt;/a&gt; pulls the tag URL, shows the resolved creative, surfaces tracking and click URLs, and lets you preview what the player would receive after redirects. Wrapper-heavy paths belong in the &lt;a href="https://vastlint.org/inspect/" rel="noopener noreferrer"&gt;VAST inspector&lt;/a&gt;, which walks each hop, shows depth, and pinpoints where the chain stops before an InLine.&lt;/p&gt;

&lt;p&gt;After preview, run &lt;strong&gt;&lt;code&gt;vastlint&lt;/code&gt;&lt;/strong&gt; (&lt;code&gt;cargo install vastlint&lt;/code&gt; or &lt;code&gt;npm install vastlint&lt;/code&gt;) on the same document. An LLM implementing AdCP, AAMP, or a trafficking assistant can emit VAST that looks plausible and still miss mezzanine, duration, or HTTPS rules; the RPC or the upload endpoint can return OK while the stitcher rejects the asset. &lt;strong&gt;vastlint&lt;/strong&gt; is the deterministic check on the creative the buy claimed. It is independent of IAB Tech Lab and of AgenticAdvertising.org; it is not an official conformance harness, and the spec does not require it. It is how you catch wire-level mistakes before they become zero-fill lines on CTV.&lt;/p&gt;

&lt;p&gt;When the placement is portfolio interactive (SIMID on &lt;code&gt;InteractiveCreativeFile&lt;/code&gt;, sample IAB creatives, host transport logs), use the &lt;a href="https://iab-tech-lab-vast-tester.vastlint.org/" rel="noopener noreferrer"&gt;IAB-style VAST tester&lt;/a&gt; for handshake debugging. That workbench is an independent fork, not an IAB Tech Lab product; linear mezzanine readiness still starts with the tag tester and inspector above.&lt;/p&gt;

&lt;p&gt;For trafficking into Google Ad Manager or other SSAI-backed video, the &lt;a href="https://vastlint.org/guides/vast-google-ad-manager/" rel="noopener noreferrer"&gt;VAST and Google Ad Manager guide&lt;/a&gt; maps common GAM expectations to the elements above. Rewarded and other non-standard breaks add player constraints on top; the &lt;a href="https://vastlint.org/guides/vast-rewarded/" rel="noopener noreferrer"&gt;rewarded VAST guide&lt;/a&gt; is the place to align skip and completion semantics with duration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;Treat readiness as a checklist per destination: mezzanine present and described for SSAI, MediaFile attributes complete for the player, HTTPS end to end, UniversalAdId aligned with trafficking, duration matching the break, and no deprecated interactive APIs on living-room inventory. When programmatic opens a new CTV format, copy the narrowest creative spec across DSP routes before you widen assets. Validation green in a desktop tool is not evidence the stitcher queued your master; fetch, inspect, and lint the same URL the player will use on the night the campaign goes live.&lt;/p&gt;

</description>
      <category>vast</category>
      <category>ctv</category>
      <category>adtech</category>
      <category>advertising</category>
    </item>
    <item>
      <title>VAST errors 301, 302 and 303 need different debugging steps.</title>
      <dc:creator>Aleksander Sekowski</dc:creator>
      <pubDate>Sat, 31 Oct 2026 18:00:00 +0000</pubDate>
      <link>https://dev.to/aleksuix/vast-errors-301-302-and-303-need-different-debugging-steps-gg5</link>
      <guid>https://dev.to/aleksuix/vast-errors-301-302-and-303-need-different-debugging-steps-gg5</guid>
      <description>&lt;p&gt;A blank video slot is an observation. It is not a diagnosis. If the creative travels through VAST Wrappers, the useful starting point is the document sequence the player actually attempted to resolve.&lt;/p&gt;

&lt;p&gt;VAST 4.3 distinguishes several wrapper-related failures. Error 301 concerns a wrapper URI timeout or unavailability. Error 302 concerns the player's wrapper limit. Error 303 concerns no ads returned after one or more wrappers. Those categories lead to different experiments.&lt;/p&gt;

&lt;p&gt;The investigation below uses synthetic responses and a controlled player. The trace format is application-owned, not a VAST schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capture the chain the player used
&lt;/h2&gt;

&lt;p&gt;A top-level tag can return different content depending on the request context. Save the URL template, the relevant expanded request values and the returned documents for the test run. Remove credentials from the copy attached to a public ticket.&lt;/p&gt;

&lt;p&gt;Record one row per attempted VAST fetch. Include the start time relative to the run, elapsed time, HTTP outcome, response type and whether resolution continued. Keep HTTP redirects distinguishable from VAST Wrapper transitions.&lt;/p&gt;

&lt;p&gt;A useful minimal record looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"synthetic-wrapper-run-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"player_build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test-build-8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"configured_wrapper_limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"configured_fetch_timeout_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hops"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Wrapper"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"elapsed_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Wrapper"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"elapsed_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"empty_VAST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"elapsed_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"observed_error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;303&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The configured values are illustrative. They are not universal player defaults. Put the actual values in the record because a result dependent on a timeout or depth limit cannot be reproduced without them.&lt;/p&gt;

&lt;p&gt;Also preserve the terminal response body. An empty VAST document, an HTML error page and an unreachable server are different inputs even when the user sees the same empty slot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Investigate 301 by locating the failed fetch
&lt;/h2&gt;

&lt;p&gt;Start with the request that did not yield the next usable VAST response. Did the player make it? Did DNS resolution, connection setup or response transfer fail? Did a server return a response after the player had already stopped waiting?&lt;/p&gt;

&lt;p&gt;Use the request timing and server logs together when available. A successful request in a server log does not prove that the player received it before its deadline. Conversely, a missing server log does not identify which network layer prevented the request.&lt;/p&gt;

&lt;p&gt;Reproduce the case with a deterministic endpoint that deliberately responds beyond the configured timeout. Keep the document sequence unchanged. If you also change wrapper depth or creative content, you will have several possible explanations for a changed result.&lt;/p&gt;

&lt;p&gt;Then move the response delay just inside the test budget. Observe whether resolution continues and which document the player processes next. The result should establish a timing boundary for that player configuration, not a promise about every production device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Investigate 302 with a low-latency chain
&lt;/h2&gt;

&lt;p&gt;A wrapper limit is about the chain the player is willing to follow. To test it, serve a deterministic sequence of quickly returned Wrapper documents and increase the depth one transition at a time.&lt;/p&gt;

&lt;p&gt;Record how the player counts wrappers. Does the configured value represent the maximum followed transitions or another documented convention? Preserve the observed terminal hop rather than assuming every player uses the same counting rule.&lt;/p&gt;

&lt;p&gt;Keep latency low enough that the test reaches the depth boundary. A long chain of slow endpoints can fail on time before it exercises the configured wrapper limit. Calling that a depth regression would send the investigation in the wrong direction.&lt;/p&gt;

&lt;p&gt;Also check for a loop. A chain that alternates between the same serving endpoints may eventually hit a limit, but the practical repair is to remove the loop. Log normalized diagnostic identities carefully: removing query parameters can conceal the values that distinguish two legitimate requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Investigate 303 by inspecting the terminal document
&lt;/h2&gt;

&lt;p&gt;For this scenario, preserve a chain that reaches a well-formed empty VAST response after a wrapper. Verify the response bytes and the context in which the player requested them.&lt;/p&gt;

&lt;p&gt;An empty response can represent no available ad. Ask whether the serving system intended that outcome for the supplied targeting, consent and inventory context. A tag pasted into a different environment may not reproduce the same eligibility decision.&lt;/p&gt;

&lt;p&gt;Do not repair an empty response by manufacturing an Inline ad or changing the error code in a dashboard. The useful result is an explanation for why the downstream system returned no ad and whether that was expected.&lt;/p&gt;

&lt;p&gt;Keep the player observation with the terminal document. Error handling depends on serving context, wrapper options and implementation behavior. Avoid labeling every empty VAST response as 303 without checking how it was reached.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a small regression matrix
&lt;/h2&gt;

&lt;p&gt;Use a single baseline chain that resolves to a known test Inline. Then create three separate fixtures:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Delay one wrapper endpoint beyond the configured fetch budget.&lt;/li&gt;
&lt;li&gt;Extend the chain beyond the configured wrapper limit while keeping responses fast.&lt;/li&gt;
&lt;li&gt;Replace the terminal response with an empty VAST document after a wrapper.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For each run, save the fixture revision, player build, relevant settings, hop trace and emitted error. State the expected category and compare it with the observation. If they differ, keep the failed run rather than editing the expected result to match.&lt;/p&gt;

&lt;p&gt;After a repair, rerun the baseline and the affected failure case. Also verify that the fix did not merely skip a wrapper carrying required tracking or verification resources.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write a ticket someone else can reproduce
&lt;/h2&gt;

&lt;p&gt;A useful report names the failing hop, includes sanitized request context and attaches the terminal response or transport failure. It distinguishes the player's emitted code from your diagnosis.&lt;/p&gt;

&lt;p&gt;For example, observed 301 at hop three is evidence. Supplier outage is a hypothesis until logs or a controlled reproduction support it. Keeping those separate makes the ticket easier for another team to act on.&lt;/p&gt;

&lt;p&gt;I maintain VASTlint. The &lt;a href="https://vastlint.org/guides/vast-wrapper-chains/?utm_source=dev.to&amp;amp;utm_medium=referral&amp;amp;utm_campaign=reference-series-2026&amp;amp;utm_content=vast-wrapper-error-triage"&gt;wrapper-chain guide&lt;/a&gt; provides a reference for inspecting the document sequence. Pair that inspection with the player trace: a clean fetch from a server-side tool does not reproduce the device's network path, timeout settings or runtime behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://iabtechlab.com/wp-content/uploads/2022/09/VAST_4.3.pdf" rel="noopener noreferrer"&gt;VAST 4.3, error reporting and wrapper handling&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Technical sources checked October 25, 2026. Drafted with AI; examples are synthetic unless explicitly stated otherwise.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vast</category>
      <category>debugging</category>
      <category>video</category>
      <category>adtech</category>
    </item>
  </channel>
</rss>
