# Trillboards OpenRTB 2.6 Extension Schema Reference

`version 2026-03-v1`


Schema reference for Trillboards' proprietary OpenRTB 2.6 bid request extensions.
This is NOT a bid endpoint specification — it documents the SIGNALS injected
into every bid request sent to DSPs so engineers can build bidding algorithms
that leverage Trillboards' unique audience intelligence.

## How to Use This Document

When Trillboards sends a bid request to your DSP, it includes standard OpenRTB 2.6
fields plus the proprietary extensions documented here. Your bidding algorithm can
use these signals to bid higher on high-value inventory.

## Extension Namespaces

| Namespace | What It Contains | Freshness |
|-----------|-----------------|-----------|
| `ext.trillboards` | Real-time audience signals from FEIN edge AI | 10-30 seconds |
| `ext.trb.vas` | Verified Attention Seconds (historical quality) | 7-day rolling |
| `ext.trb.intent` | Purchase intent from speech analysis | 10-30 seconds |
| `ext.trb.weather` | Contextual weather conditions | 60-minute cache |
| `ext.trb.foot_traffic` | Venue occupancy and traffic | 10-30 seconds |
| `ext.trillboards_contract` | Signal quality assurance | Per-request |
| `user.data[]` | IAB Audience Taxonomy 1.1 segments + seller-defined | 10-30 seconds |

## Signal Sources

- **FEIN Edge AI**: On-device ML Kit (face detection, gaze tracking) + YAMNet TFLite (audio classification) on the tablet, which sends Trillboards a record for each detected person and sends selected camera frames to a cloud model.
- **VAS Engine**: Verified Attention Seconds computed from historical FEIN measurements, stored in PostgreSQL attention_ledger.
- **Intent Graph**: Speech-derived purchase intent from YAMNet audio scene classification + keyword detection on the tablet. The tablet also sends Trillboards the speech transcript, which a cloud model reads for purchase intent and brands mentioned.
- **Weather Service**: WeatherAPI.com data cached in Redis with 60-minute TTL.
- **Census ACS**: US Census American Community Survey neighborhood demographics by geohash.

## Data behind the signals

- Bid requests carry the screen's IP address, its location (its live GPS position where the screen reports one) and its live audience measurements.
- The tablet keeps a record for each person its camera detects: estimated age range, gender, dominant emotion, dwell time and the time spent looking at the screen. On tablets running the full sensing profile it also computes a face-identity template, which Trillboards uses to recognise the same person across screens and days.
- Selected camera frames go to a cloud model (Google Vertex AI) that estimates ethnicity, age band, gender, social composition, purchase-intent stage, activity, attire, carried items, lifestyle segments and mood. Trillboards keeps those frames for 400 days; the frames captured between 5 May and 21 June 2026 are kept as a model-evaluation set with no scheduled deletion.
- Audio: the tablet classifies ambient sound and transcribes speech on the device, and Trillboards stores the transcript. No audio recording is stored or transmitted.
- Radio: the tablet sends Trillboards the Bluetooth, Wi-Fi and local-network identifiers nearby phones broadcast. Trillboards stores them and links device keys to the person seen with them and to mobile advertising IDs.
- Trillboards sells and shares this audience measurement and location data with its data partners, including 375ai and Intuizi. Each category of data partner and what it receives: https://trillboards.com/legal/data-partners



---

## Schemas

30 schemas. 
Every one is derived from `docs/openapi/openrtb-extensions.yaml` at request time.


[`AudienceAttentionPrediction`](#audienceattentionprediction) · [`AudienceSocialAttention`](#audiencesocialattention) · [`BidRequestExt`](#bidrequestext) · [`CompletedImpressions`](#completedimpressions) · [`ContractExt`](#contractext) · [`DOOHBidRequest`](#doohbidrequest) · [`DOOHObject`](#doohobject) · [`DemandPartners`](#demandpartners) · [`Device`](#device) · [`EarnerScreens`](#earnerscreens) · [`EventExt`](#eventext) · [`FootTrafficExt`](#foottrafficext) · [`IABSegmentReference`](#iabsegmentreference) · [`Impression`](#impression) · [`IntentExt`](#intentext) · [`OpenrtbRevenueScreenHourly`](#openrtbrevenuescreenhourly) · [`PartnerReconciliationReports`](#partnerreconciliationreports) · [`PartnerVariants`](#partnervariants) · [`PartnerZoneCpmDaily`](#partnerzonecpmdaily) · [`Regs`](#regs) · [`Segment`](#segment) · [`SellerDefinedSegmentReference`](#sellerdefinedsegmentreference) · [`Source`](#source) · [`TrbNamespace`](#trbnamespace) · [`TrillboardsAudienceExt`](#trillboardsaudienceext) · [`User`](#user) · [`UserData`](#userdata) · [`VASExt`](#vasext) · [`VenueExt`](#venueext) · [`WeatherExt`](#weatherext)


### AudienceAttentionPrediction


Backend-derived predictive attention signal + buyer-grade attentive-seconds. 1:1 with audience_metrics. Never null by contract; placeholder on row 1.


| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `predicted_attention_next_30s` | `string` — one of `high`, `medium`, `low`, `none` | yes | predicted_attention_next_30s enum {high,medium,low,none}. 'none' is below 'low'; fires when rolling history converges to zero or on a trend down from 'low'. |
| `predicted_disengagement_imminent` | `boolean` | yes | predicted_disengagement_imminent needs elevated dwell AND a falling trend, so it is unreachable whenever the history vector is empty. A 0% fire rate is a HISTORY-PLUMBING alarm, not a quiet fleet. |
| `prediction_evidence` | `string` | yes |  |
| `attentive_seconds_observed` | `number` | yes | attentive_seconds_observed = SUM(per_face[].gaze_seconds >= 1.0), OpenOOH 2024 4.2. NEVER NULL by DDL, so 0.000 means 'no viewer met threshold' ONLY when per_face_observations[] was present. NUMERIC(6,3). |
| `attentive_face_count` | `integer` | yes | attentive_face_count SMALLINT: distinct viewers meeting the >=1s threshold; the buyer-side CPMA denominator. NEVER NULL. |
| `attentive_seconds_predicted_15s` | `number` | yes | attentive_seconds_predicted_15s: expected next-15s attentive seconds from per-screen history x attention-weight; bootstrap = linear extrapolation. No global tau. NUMERIC(6,3). |
| `next_state_attention_forecast` | `number`, nullable |  |  |
| `next_state_confidence` | `number`, nullable |  |  |
| `next_state_method` | `string`, nullable |  |  |
| `next_state_horizon_minutes` | `integer`, nullable |  |  |
| `next_state_sample_size` | `integer`, nullable |  |  |
| `next_state_forecast_at` | `string`, nullable |  |  |
| `next_state_attention_realized` | `number`, nullable |  |  |
| `next_state_abs_error` | `number`, nullable |  |  |
| `next_state_graded_at` | `string`, nullable |  |  |
| `next_state_persist_abs_error` | `number`, nullable |  |  |
| `next_state_xscreen_abs_error` | `number`, nullable |  |  |
| `next_state_xscreen_sample_size` | `integer`, nullable |  |  |
| `next_state_xscreen_method` | `string`, nullable |  |  |
| `next_state_xsv_abs_error` | `number`, nullable |  |  |
| `next_state_xsv_sample_size` | `integer`, nullable |  |  |


### AudienceSocialAttention


Buyer-grade social amplification signal (5 typed columns). Cloud Vertex Gemini producer. 1 row per audience observation when cascade is observable.


| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `social_amplification_factor` | `number` | yes | NUMERIC(4,3). |
| `cascade_depth` | `integer` | yes |  |
| `viral_attention_score` | `number` | yes | viral_attention_score is the headline number Viant/T-Vision license. Composite of social_amplification_factor × cascade_depth × emotional engagement. NUMERIC(4,3). |
| `primary_attention_anchor_face_index` | `integer`, nullable |  | All 5 columns NOT NULL except primary_attention_anchor_face_index (only legit NULL: no anchor or solo scene). |
| `amplification_evidence` | `string` | yes |  |
| `data_projection_version` | `integer` | yes |  |
| `data_projection_xid` | `string` | yes |  |


### BidRequestExt


Top-level bid request extensions.

## ⚠️ Both Trillboards namespaces are STRIPPED on curated (registered-audience) deals

When a bid request is served under a curation plan with
`registeredAudienceOnly` — the shape behind our curated PMP / registered
audience deals — the server **deletes `ext.trillboards` and `ext.trb`
outright** before the request leaves us
(`services/programmatic/bidstreamSignalCuration.js`). If `ext` is left with
no other keys, `ext` itself is removed.

This is deliberate: on a curated deal the audience is carried by the
registered segment in `user.data`, and the raw sensing signals are withheld.

**The consequence for a bidder is the trap worth stating plainly:** every
signal described on this page disappears on exactly the deals we most want
you buying. A bidder whose valuation depends on `ext.trb.vas`,
`ext.trillboards.attention_score`, or any other field here will price
curated inventory as though the screen had no audience at all.

Build for this explicitly:

- Treat **both** namespaces as absent-by-default and gate every read on
  their presence. Do not assume a partially-populated `ext`.
- Do not infer "no audience" or a zero-valued audience from their absence.
- On a curated deal, read the audience from `user.data` (the registered
  segment) instead. That is where the targeting signal lives for these deals.
- The stripping is all-or-nothing per request: you will never see
  `ext.trb` survive while `ext.trillboards` is removed, or vice versa.



| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `protocol` | `string` |  |  |
| `trillboards` | [`TrillboardsAudienceExt`](#trillboardsaudienceext) |  |  |
| `trb` | [`TrbNamespace`](#trbnamespace) |  |  |
| `trillboards_contract` | [`ContractExt`](#contractext) |  |  |


### CompletedImpressions


Completed-play observations from VAST complete and proof-of-play paths. Deduped by issued request/impression key for live revenue and daily earnings.


| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `integer`, nullable |  | JustBaat dedupes issued request keys; Adipolo/VidVerto/others use rendered impression_id with request fallback. Readers/rollups dedupe overlapping vast_event, proof_of_play and approved partner_sdk. |
| `completed_at` | `string` | yes |  |
| `screen_mongo_id` | `string` | yes |  |
| `device_id` | `string`, nullable |  |  |
| `ad_id` | `string` | yes |  |
| `creative_id` | `string`, nullable |  |  |
| `impression_id` | `string`, nullable |  |  |
| `source_provider` | `string` | yes | ImpressionController drops rows with unknown source_provider (PR #2924); earnings use partner billing policy, not raw counts. Exclude pi_e2e_*, admin-allocation and is_demo_data from revenue. |
| `variant_name` | `string`, nullable |  |  |
| `actual_cpm` | `number`, nullable |  | NUMERIC(18,6). |
| `actual_gross_revenue` | `number`, nullable |  | NUMERIC(18,6). |
| `pricing_source` | `string` | yes | PG is the canonical completion write; completed_impressions_dist is its CH reporting mirror. pricing_source: 'placement_purchase'=ad_price/goal per play; '*_overdelivered'/'*_zero'=$0; 'config_defa... |
| `currency` | `string` | yes |  |
| `ingestion_path` | `string` | yes |  |
| `request_id` | `string`, nullable |  | owned_direct = signed src or verified SDK. VAST impid, request_id and SDK suffix share one nonce/PG row; completion, exact debit and eligible billing commit together. First authorized write owns CH... |
| `bid_request_id` | `string`, nullable |  |  |
| `play_id` | `string`, nullable |  |  |
| `duration_seconds` | `number`, nullable |  | NUMERIC(10,2). |
| `givt_passed` | `boolean`, nullable |  |  |
| `signature` | `string`, nullable |  |  |
| `display_mode` | `string`, nullable |  |  |
| `display_area_ratio` | `number`, nullable |  | NUMERIC(5,4). |
| `metadata` | `object` | yes | SDK missing-price is server-pending until signed native pricing; directbillable_ is not authority. Client debit/mirror/pending claims are stripped. Legacy finalization: recorded/native nonnull zero... |
| `created_at` | `string` | yes | chEventParityCron: exact-key repair; created_at/oldest authorized floor; original completed_at CH key; same writer/DLQ before ACK; no delivery debit. All leaf pending indexes must be valid; failed/... |
| `screen_id` | `integer`, nullable |  |  |
| `demand_partner_id` | `integer`, nullable |  |  |
| `variant_id` | `integer`, nullable |  |  |
| `content_sha256` | `string`, nullable |  | content_sha256 is DECLARED to propagate from vast_requests via request_id but lands on ~1% of rows: PG vast_requests is empty (vastRequestsWriteBuffer defaultDisabled since 2026-06-26), so that lin... |
| `serving_tenant_id` | `integer`, nullable |  |  |
| `impression_multiplier` | `number` | yes | NUMERIC(12,6). |
| `quantity_source` | `string`, nullable |  |  |
| `quantity_source_type` | `integer`, nullable |  |  |
| `quantity_vendor` | `string`, nullable |  |  |
| `quantity_measured_at` | `string`, nullable |  |  |
| `earnings_source` | `string`, nullable |  |  |
| `earnings_deal_id` | `string`, nullable |  |  |
| `earnings_deal_type` | `string`, nullable |  |  |
| `advertiser_domain` | `string`, nullable |  |  |
| `buyer_seat` | `string`, nullable |  |  |
| `zone_id` | `string`, nullable |  |  |
| `raw_client_timestamp` | `string`, nullable |  |  |
| `normalized_event_timestamp` | `string`, nullable |  |  |
| `event_clock_skew_seconds` | `number`, nullable |  |  |
| `event_timestamp_clamped` | `boolean`, nullable |  |  |
| `billable_key` | `string`, nullable |  |  |


### ContractExt


Signal quality assurance contract. Tells DSPs how complete and
reliable the signals in this bid request are. If valid=false,
some required fields are missing (but the request is still valid OpenRTB).



| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `version` | `string` |  | Contract schema version |
| `valid` | `boolean` |  | Whether all required fields passed validation |
| `required_field_coverage_pct` | `number` |  | Percentage of required fields that are present and valid |
| `error_count` | `integer` |  | Number of validation errors (missing required fields) |
| `warning_count` | `integer` |  | Number of validation warnings (recommended fields missing) |
| `errors` | array of `object` |  |  |
| `warnings` | array of `object` |  |  |


```json
{
  "version": "2026-03-v1",
  "valid": true,
  "required_field_coverage_pct": 100,
  "error_count": 0,
  "warning_count": 0,
  "errors": [],
  "warnings": []
}
```


### DOOHBidRequest


Full OpenRTB 2.6 DOOH bid request with all Trillboards extensions


| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `string` |  | Unique bid request ID |
| `imp` | array of [`Impression`](#impression) |  |  |
| `dooh` | [`DOOHObject`](#doohobject) |  |  |
| `device` | [`Device`](#device) |  |  |
| `user` | [`User`](#user) |  |  |
| `source` | [`Source`](#source) |  |  |
| `regs` | [`Regs`](#regs) |  |  |
| `at` | `integer` — one of `1` |  | Auction type. Always 1 (first price) |
| `tmax` | `integer` |  | Maximum response time in milliseconds |
| `cur` | array of `string` |  |  |
| `ext` | [`BidRequestExt`](#bidrequestext) |  |  |


```json
{
  "id": "691fced1-040c-4354-b861-ef2496f44f29",
  "imp": [
    {
      "id": "imp_507f1f77bcf86cd799439011",
      "tagid": "507f1f77bcf86cd799439011",
      "bidfloor": 2,
      "bidfloorcur": "USD",
      "secure": 1,
      "dt": 1788279423607,
      "qty": {
        "multiplier": 2,
        "sourcetype": 2
      },
      "banner": {
        "w": 1920,
        "h": 1080,
        "format": [
          {
            "w": 1920,
            "h": 1080
          }
        ],
        "pos": 1
      },
      "video": {
        "mimes": [
          "video/mp4",
          "video/webm"
        ],
        "minduration": 5,
        "maxduration": 30,
        "protocols": [
          2,
          3,
          5,
          6
        ],
        "w": 1920,
        "h": 1080,
        "startdelay": 0,
        "linearity": 1,
        "skip": 0,
        "placement": 4
      }
    }
  ],
  "dooh": {
    "id": "507f1f77bcf86cd799439011",
    "name": "Test Screen",
    "venuetype": [
      2,
      202
    ],
    "venuetypetax": 5,
    "publisher": {
      "id": "trillboards",
      "name": "Trillboards",
      "domain": "trillboards.com",
      "cat": [
        "IAB24"
      ]
    },
    "domain": "trillboards.com",
    "keywords": "Retail,Shopping Mall,New York,NY",
    "content": {
      "genre": "Entertainment",
      "cattax": 1,
      "cat": [
        "IAB1"
      ],
      "prodq": 1,
      "context": 1,
      "contentrating": "TV-PG",
      "livestream": 1,
      "language": "EN",
      "channel": {
        "name": "Entertainment"
      }
    }
  },
  "device": {
    "devicetype": 8,
    "connectiontype": 2,
    "w": 1920,
    "h": 1080,
    "geo": {
      "lat": 40.7128,
      "lon": -74.006,
      "type": 1,
      "country": "US",
      "region": "NY",
      "city": "New York",
      "zip": "10001"
    }
  },
  "source": {
    "fd": 0,
    "tid": "691fced1-040c-4354-b861-ef2496f44f29",
    "schain": {
      "complete": 1,
      "nodes": [
        {
          "asi": "trillboards.com",
          "sid": "trillboards",
          "rid": "691fced1-040c-4354-b861-ef2496f44f29",
          "name": "Trillboards",
          "domain": "trillboards.com",
          "hp": 1
        }
      ],
      "ver": "1.0"
    }
  },
  "at": 1,
  "tmax": 500,
  "cur": [
    "USD"
  ],
  "test": 0,
  "regs": {
    "coppa": 0,
    "gdpr": 0
  },
  "ext": {
    "protocol": "OpenRTB 2.6",
    "trillboards_contract": {
      "version": "2026-03-v1",
      "valid": true,
      "required_field_coverage_pct": 100,
      "error_count": 0,
      "warning_count": 0,
      "errors": [],
      "warnings": []
    },
    "trillboards": {
      "attention_score": 0.87,
      "audience_count": 8,
      "crowd_density": 12,
      "ad_receptivity": 0.75,
      "emotional_engagement": 0.82,
      "mood": "happy",
      "estimated_occupancy": 25,
      "avg_dwell_ms": 4500,
      "group_composition": {
        "solo": 3
      },
      "screen_engagement": "active",
      "audience_reaction": "positive",
      "signals_age_ms": 12480
    },
    "trb": {
      "weather": {
        "temp_f": 85,
        "feels_like_f": 88,
        "humidity": 75,
        "condition": "Partly cloudy",
        "uv_index": 8,
        "wind_mph": 12,
        "is_raining": false,
        "temp_segment": "hot"
      },
      "foot_traffic": {
        "current_audience": 25,
        "estimated_occupancy": 50,
        "avg_dwell_seconds": 6,
        "traffic_tier": "very_high",
        "estimated_hourly_impressions": 15000
      }
    }
  }
}
```


### DOOHObject


OpenRTB 2.6 DOOH object (top-level, replaces site/app)


| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `string` |  |  |
| `name` | `string` |  |  |
| `venuetype` | array of `integer` |  | OpenOOH venue-type ids, as INTEGERS, projected down to taxonomy **1.1** — the version `venuetypetax` declares. Corrected 2026-09-01 against `openrtbRequestBuilder.js`, which has always emitted integers. This declared `string` items and "v1.2 IDs"; both were wrong. Our tables implement OpenOOH 1.2, but AdCOM's taxonomy enumeration stops at 5 (= OpenOOH 1.1), so 1.2 cannot be signalled on the wire at all and the ids are projected. The projection is lossless in MEANING, only in specificity — see the WIRE PROJECTION block in `config/openoohTaxonomy.js`. The array is OMITTED rather than sent empty when classification yields nothing: `venuetype: []` would assert "we classified this screen as belonging to no venue type", which is a different and false claim. |
| `venuetypetax` | `integer` — one of `5` |  | AdCOM venue-taxonomy identifier. **5 = OpenOOH 1.1**, and 5 is what we send. Corrected 2026-09-01. This declared `enum: [1]` described as "1 = OpenOOH Venue Taxonomy". AdCOM value 1 is OpenOOH **1.0**, so the published contract named the oldest taxonomy in the list while the ids alongside it were not that taxonomy. The builder itself carried the same bug and was fixed; this document was not updated with it, so a DSP resolving our `venuetype` ids against OpenOOH 1.0 would have mapped them against the wrong version. |
| `publisher` | `object` |  |  |
| `domain` | `string` |  |  |
| `keywords` | `string` |  |  |


### DemandPartners


Canonical INT-PK identity table for demand-side partners (DSPs/SSPs that Trillboards routes ad requests to). Wave 2 of pristine-state refactor. Replaces partner_key TEXT joins across the billing path.


| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `demand_partner_id` | `string`, nullable |  | demand_partner_id INT is the canonical FK going forward; partner_key TEXT is kept as the wire-format alternate-key FOREVER (external partner-API auth + admin UI rely on it). Never propose dropping ... |
| `partner_key` | `string` | yes |  |
| `public_alias` | `string` | yes | public_alias UNIQUE: this is the obfuscated external alias partner-API consumers see. partnerAlias.js will read from this table after Wave 2 cutover. |
| `display_name` | `string` | yes |  |
| `partner_type` | `string` | yes |  |
| `archived` | `boolean` | yes |  |
| `created_at` | `string` | yes |  |
| `updated_at` | `string` | yes |  |


### Device


| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `devicetype` | `integer` — one of `8` |  | 8 = Digital Out of Home (DOOH) per OpenRTB 2.6 |
| `ua` | `string` |  |  |
| `make` | `string` |  |  |
| `model` | `string` |  |  |
| `os` | `string` |  |  |
| `osv` | `string` |  |  |
| `connectiontype` | `integer` |  |  |
| `w` | `integer` |  |  |
| `h` | `integer` |  |  |
| `ifa` | `string` |  | OS-sanctioned resettable advertising ID in the clear; omitted when unavailable or limited |
| `ifa_type` | `string` — one of `aaid`, `rida`, `afai`, `idfa`, `tifa`, `msai`, `dpid`, `ppid`, … (10 total) |  | Legacy compatibility copy; canonical DSP-compatible placement is device.ext.ifa_type |
| `ext` | `object` |  |  |
| `geo` | `object` |  |  |
| `eids` | array of `object` |  | Extended device identifiers (IAB recommended for DOOH) |


### EarnerScreens


User-owned ad screens. Each row is a logical screen that can display ads. Links user to device. PG primary with Mongo legacy mirror.


| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `taxonomy_spec_version` | `string`, nullable |  |  |
| `taxonomy_migrated_at` | `string`, nullable |  |  |
| `auction_mode` | `string`, nullable |  |  |
| `auction_type` | `string`, nullable |  |  |
| `auction_enabled_at` | `string`, nullable |  |  |
| `auction_cohort` | `string`, nullable |  |  |
| `sync_is_master` | `boolean`, nullable |  |  |
| `sync_master_screen_id` | `string`, nullable |  |  |
| `sync_enabled` | `boolean`, nullable |  |  |
| `sync_last_sync_at` | `string`, nullable |  |  |
| `sync_group_name` | `string`, nullable |  |  |
| `registration_source` | `string`, nullable |  |  |
| `last_offline_transition_at` | `string`, nullable |  |  |
| `is_standby` | `boolean` | yes |  |
| `blocked_iab_categories` | array of `string` |  |  |
| `blocked_advertisers` | array of `string` |  |  |
| `pref_allow_tobacco` | `boolean`, nullable |  |  |
| `pref_allow_religious` | `boolean`, nullable |  |  |
| `pref_auto_approve_known_brands` | `boolean`, nullable |  |  |
| `pref_blocked_subjects` | array of `string` |  |  |
| `pref_allowed_subjects` | array of `string` |  |  |
| `pref_blocked_keywords` | array of `string` |  |  |
| `pref_allowed_keywords` | array of `string` |  |  |
| `geocode_source` | `string`, nullable |  |  |
| `venue_parent` | `string`, nullable |  |  |
| `venue_child` | `string`, nullable |  |  |
| `venue_grandchild` | `string`, nullable |  |  |
| `environment_type` | `string`, nullable |  |  |
| `placement_type` | `string`, nullable |  |  |
| `venue_state` | `string`, nullable |  |  |
| `venue_city` | `string`, nullable |  |  |
| `display_mode` | `string`, nullable |  |  |
| `ad_interval_seconds` | `integer`, nullable |  |  |
| `sound_enabled` | `boolean`, nullable |  |  |
| `advertisement_sizing` | `string`, nullable |  |  |
| `avg_dwell_time_seconds` | `integer`, nullable |  |  |
| `peak_hours` | array of `string` |  |  |
| `delivery_mode` | `string` | yes |  |
| `loop_plan_id` | `integer`, nullable |  |  |
| `ntp_sync_required` | `boolean` | yes |  |
| `venue_classification_source` | `string`, nullable |  | enrichment_status: PG GENERATED from (venue_classification_source, enrichment_failed_at); a real classification DOMINATES a stale failure stamp (success wins). Never written directly. Values: pendi... |
| `device_platform_override` | `string`, nullable |  |  |
| `hb_vast_tag_url` | `string`, nullable |  |  |
| `hb_min_interval_seconds` | `integer`, nullable |  |  |
| `hb_last_sync_at` | `string`, nullable |  |  |
| `waterfall_partners` | array of `string` | yes | waterfall_partners is the ONLY pin store (order + whitelist; screen_partner_settings was never built). Serving reads it only with a screen_header_bidding_config row and ignores it once waterfall_op... |
| `waterfall_total_timeout_ms` | `integer`, nullable |  |  |
| `waterfall_optimized_at` | `string`, nullable |  |  |
| `programmatic_enabled` | `boolean` | yes |  |
| `prog_currency` | `string` | yes |  |
| `preferred_deal_ids` | array of `string` | yes |  |
| `last_programmatic_sync_at` | `string`, nullable |  |  |
| `mediation_priority` | `integer`, nullable |  |  |
| `hive_stack_screen_id` | `string`, nullable |  |  |
| `hive_stack_site_id` | `string`, nullable |  |  |
| `hive_stack_unit_id` | `string`, nullable |  |  |
| `hive_stack_unit_uuid` | `string`, nullable |  |  |
| `hive_stack_synced_at` | `string`, nullable |  |  |
| `hive_stack_sync_status` | `string`, nullable |  |  |
| `taggify_screen_id` | `string`, nullable |  |  |
| `gam_ad_unit_id` | `string`, nullable |  |  |
| `gam_ad_unit_code` | `string`, nullable |  |  |
| `gam_synced_at` | `string`, nullable |  |  |
| `gam_sync_status` | `string`, nullable |  |  |
| `branding_overlay_enabled` | `boolean` | yes |  |
| `branding_overlay_logo_url` | `string` | yes |  |
| `branding_overlay_logo_transformed_url` | `string` | yes |  |
| `branding_overlay_position` | `string` | yes |  |
| `branding_overlay_size_ratio` | `number` | yes | NUMERIC(5,4). |
| `branding_overlay_padding_ratio` | `number` | yes | NUMERIC(5,4). |
| `branding_overlay_opacity` | `number` | yes | NUMERIC(5,4). |
| `branding_overlay_hide_on_ads` | `boolean` | yes |  |
| `region` | `string` | yes |  |
| `physical_size_set_by` | `string`, nullable |  |  |
| `physical_size_set_at` | `string`, nullable |  |  |
| `offline_alert_sent_at` | `string`, nullable |  |  |
| `back_online_alert_sent_at` | `string`, nullable |  |  |
| `enrichment_failed_at` | `string`, nullable |  |  |
| `operating_hours` | `object`, nullable |  |  |
| `slot_duration_seconds` | `integer`, nullable |  |  |
| `state_iso_3166_2` | `string`, nullable |  |  |
| `geocoding_raw_response` | `object`, nullable |  |  |
| `geocoded_at` | `string`, nullable |  |  |
| `photo_flagged` | `boolean` | yes |  |
| `photo_flagged_at` | `string`, nullable |  |  |
| `photo_flagged_by_mongo_id` | `string`, nullable |  |  |
| `photo_flag_reason` | `string`, nullable |  |  |
| `photo_flag_notes` | `string`, nullable |  |  |
| `photo_notification_sent_at` | `string`, nullable |  |  |
| `photo_notification_count` | `integer` | yes |  |
| `photo_quality_sent_at` | `string`, nullable |  |  |
| `l_bar_position` | `string`, nullable |  |  |
| `block_group_geoid` | `string`, nullable |  |  |
| `dma_code` | `string`, nullable |  |  |
| `dma_name` | `string`, nullable |  |  |
| `place_name` | `string`, nullable |  |  |
| `neighborhood` | `string`, nullable |  |  |
| `branding_overlay_placement` | `string` | yes |  |
| `archetype_photo_url` | `string`, nullable |  |  |
| `archetype_photo_key` | `string`, nullable |  |  |
| `archetype_photo_synced_at` | `string`, nullable |  |  |
| `offline_push_alert_sent_at` | `string`, nullable |  |  |
| `is_demo_data` | `boolean` | yes |  |
| `deleted_at` | `string`, nullable |  | deleted_at IS NULL required — soft-delete nulls earnerscreen_device_id for partial unique index |
| `partner_mongo_id` | `string`, nullable |  |  |
| `photos` | `object`, nullable |  |  |
| `photo_review_status` | `string`, nullable |  |  |
| `venue_category` | `string`, nullable |  |  |
| `venue_subcategory` | `string`, nullable |  |  |
| `venue_child_id` | `string`, nullable |  |  |
| `places_rating` | `number`, nullable |  |  |
| `places_review_count` | `integer`, nullable |  |  |
| `commercial_value_score` | `number`, nullable |  |  |
| `foot_traffic_multiplier` | `number`, nullable |  |  |
| `floor_price_cpm` | `number`, nullable |  | NUMERIC(12,6). |
| `distributor_id` | `integer`, nullable |  |  |
| `billing_mode` | `string` | yes |  |
| `ads_disabled_reasons` | array of `string` | yes |  |
| `cms_payment_status` | `string` | yes |  |
| `ssp_enabled` | `boolean`, nullable |  |  |
| `gam_ad_unit_path` | `string`, nullable |  |  |
| `l_bar_ads_enabled` | `boolean`, nullable |  |  |
| `width_inches` | `number`, nullable |  | Physical width_inches/height_inches/diagonal_inches are inches. Pixels live on earner_screen_devices; the raw screen has no resolution_width/resolution_height columns. |
| `height_inches` | `number`, nullable |  |  |
| `diagonal_inches` | `number`, nullable |  |  |
| `dynamic_floor_pricing` | `boolean`, nullable |  |  |
| `prog_allocation_pct` | `number`, nullable |  |  |
| `hb_provider` | `string`, nullable |  |  |
| `hb_timeout_ms` | `integer`, nullable |  |  |
| `hb_enabled` | `boolean`, nullable |  |  |
| `hb_allocation` | `number`, nullable |  |  |
| `hb_request_mode` | `string`, nullable |  |  |
| `hb_force_request` | `boolean`, nullable |  |  |
| `pref_blocked_categories` | array of `string` |  |  |
| `pref_allowed_categories` | array of `string` |  |  |
| `content_filtering_enabled` | `boolean`, nullable |  |  |
| `pref_level` | `string`, nullable |  |  |
| `pref_max_age_rating` | `string`, nullable |  |  |
| `mobile_distance_threshold_m` | `integer`, nullable |  |  |
| `mobile_update_interval_s` | `integer`, nullable |  |  |
| `mobile_vehicle_type` | `string`, nullable |  |  |
| `latitude` | `number`, nullable |  |  |
| `longitude` | `number`, nullable |  |  |
| `brand_substitution_mode` | `string` | yes |  |
| `brand_substitution_consent_at` | `string`, nullable |  |  |
| `brand_substitution_consent_by` | `string`, nullable |  |  |
| `serving_tenant_id` | `integer`, nullable |  |  |
| `enrichment_attempts` | `integer` | yes |  |
| `last_enrichment_attempt_at` | `string`, nullable |  |  |
| `media_capability` | `string` — one of `image_video`, `image_only`, `video_only` |  |  |
| `screen_placement` | `string`, nullable |  |  |


### EventExt


Large-event proximity for the screen. The whole `event` sub-namespace is
present ONLY when a nearby event was detected — its absence means "no
nearby event", so `nearby` is always `true` when the object exists.



| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `nearby` | `boolean` |  | Always `true` when this object is present. |
| `count` | `integer` |  | Number of nearby events detected |
| `max_capacity` | `integer` |  | Attendee capacity of the largest nearby event |
| `category` | `string` |  | Dominant category across the nearby events |
| `audience_spike` | `boolean` |  | Whether an audience spike is expected from these events |


### FootTrafficExt


Real-time venue foot traffic metrics derived from FEIN audience sensing.
Higher traffic = more impressions = higher value for advertisers.



| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `current_audience` | `integer` |  | Current number of people detected (face count, rounded) |
| `estimated_occupancy` | `integer` |  | Estimated total venue occupancy (extrapolated) |
| `avg_dwell_seconds` | `integer` |  | Average dwell time in seconds |
| `traffic_tier` | `string` — one of `very_low`, `low`, `medium`, `high`, `very_high` |  | Traffic classification for easy targeting: - very_high: >= 50 people - high: 20-49 people - medium: 8-19 people - low: 3-7 people - very_low: < 3 people |
| `estimated_hourly_impressions` | `integer` |  | Projected impressions per hour based on face turnover rate |


```json
{
  "current_audience": 25,
  "estimated_occupancy": 50,
  "avg_dwell_seconds": 6,
  "traffic_tier": "very_high",
  "estimated_hourly_impressions": 15000
}
```


### IABSegmentReference


Reference table of IAB Audience Taxonomy 1.1 segment IDs used by Trillboards.

### Income Segments
| ID | Name |
|-----|------|
| 1001 | High Income |
| 1002 | Upper-Middle Income |
| 1003 | Middle Income |
| 1004 | Lower-Middle Income |
| 1005 | Low Income |

### Lifestyle Segments
| ID | Name |
|-----|------|
| 2001 | Business Professional |
| 2002 | Student |
| 2003 | Fitness Enthusiast |
| 2004 | Luxury Shopper |
| 2005 | Tech Savvy |
| 2006 | Outdoor Active |
| 2007 | Foodie |
| 2008 | Health Conscious |
| 2009 | Parent |
| 2010 | Commuter |
| 2011 | Tourist |
| 2012 | Nightlife |
| 2013 | Pet Owner |
| 2014 | Gamer |
| 2015 | Reader |
| 2016 | Eco-Conscious |
| 2017 | Fashion Forward |
| 2018 | DIY/Home Improvement |
| 2019 | Music Lover |
| 2020 | Sports Fan |

### Purchase Intent Segments
| ID | Name |
|-----|------|
| 3001 | High Purchase Intent |
| 3002 | Medium Purchase Intent |
| 3003 | Low Purchase Intent |



Type: `object`


### Impression


| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `string` |  |  |
| `tagid` | `string` |  | Screen MongoDB ID |
| `bidfloor` | `number` |  | Floor price in CPM |
| `bidfloorcur` | `string` |  |  |
| `secure` | `integer` |  |  |
| `dt` | `integer` |  | Estimated fulfillment timestamp (epoch seconds) |
| `qty` | `object` |  | DOOH impression quantity/multiplier (OpenRTB 2.6) |
| `banner` | `object` |  |  |
| `video` | `object` |  |  |


### IntentExt


Purchase intent signals derived from ambient audio classification.
YAMNet TFLite classifies audio SCENES (not speech content) on-device.
Combined with visual shopping signals (bag detection) and demographic
context to compute a composite intent score.

Higher intent_score = audience is more likely to make a purchase.
DSPs can bid higher when purchase_stage is READY_TO_BUY or COMPARING.



| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `score` | `number` |  | Composite purchase intent score (0-1). Weighted combination of verbal intent, brand mentions, price inquiry, shopping activity, bag detection, income match, emotion, and engagement. |
| `purchase_stage` | `string` — one of `READY_TO_BUY`, `COMPARING`, `CONSIDERING`, `BROWSING`, `NONE` |  | Classified purchase funnel stage based on behavioral signals. - READY_TO_BUY: High intent + brand mentions + price inquiry (score >= 0.7) - COMPARING: Active comparison shopping (score 0.5-0.7) - CONSIDERING: Interest shown but no purchase signals (score 0.3-0.5) - BROWSING: Casual browsing, low intent (score 0.1-0.3) - NONE: No purchase intent detected (score < 0.1) |
| `categories` | array of `object` |  | Detected product/service interest categories |
| `brand_mentions` | `boolean` |  | Whether brand names were detected in ambient audio |
| `brand_count` | `integer` |  | Number of distinct brand mentions detected |
| `audience_size` | `integer` |  | Current audience size (face count) when intent was measured |
| `should_trigger` | `boolean` |  | Whether intent score is high enough to trigger premium ad swap |


### OpenrtbRevenueScreenHourly


Revenue per screen per hour. Aggregates gross revenue from completed impressions. Refreshed from CH via clickhouseRollupSource. PG only.


| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `hour_bucket` | `string` | yes |  |
| `screen_mongo_id` | `string` | yes |  |
| `impressions` | `integer` | yes |  |
| `priced_impressions` | `integer` | yes |  |
| `estimated_impressions` | `integer` | yes |  |
| `actual_gross_revenue` | `number` | yes |  |
| `estimated_gross_revenue` | `number` | yes |  |
| `gross_revenue` | `number` | yes |  |
| `updated_at` | `string` | yes |  |


### PartnerReconciliationReports


Partner-reported impression and revenue counts for reconciliation against deduped completed_impressions. Ingested from VidVerto SSP API and Adipolo API / JustBaat CSV reports. PG only.


| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `their_requests` | `integer`, nullable |  |  |
| `our_requests` | `integer`, nullable |  |  |
| `request_variance` | `number`, nullable |  | NUMERIC(10,4). |
| `demand_partner_id` | `integer`, nullable |  | Wave 2 B added demand_partner_id INT + variant_id INT FKs. Legacy partner_key TEXT kept as wire-format alternate-key. Backfill via follow-up Wave 2 B-backfill PR. |
| `variant_id` | `integer`, nullable |  |  |
| `id` | `integer`, nullable |  | VidVerto synced daily via vidvertoRevenueSync before programmatic earnings aggregation. |
| `provider_name` | `string` | yes |  |
| `report_date` | `string` | yes |  |
| `report_type` | `string` — one of `partner_api`, `manual_upload`, `email_extract`, `dashboard_snapshot` | yes |  |
| `their_impressions` | `integer`, nullable |  |  |
| `their_revenue` | `number`, nullable |  | NUMERIC(14,6). |
| `their_ecpm` | `number`, nullable |  | NUMERIC(10,4). |
| `their_fill_rate` | `number`, nullable |  | NUMERIC(6,4). |
| `our_impressions` | `integer`, nullable |  |  |
| `our_revenue` | `number`, nullable |  | NUMERIC(14,6). |
| `our_ecpm` | `number`, nullable |  | NUMERIC(10,4). |
| `impression_variance` | `number`, nullable |  | NUMERIC(8,4). |
| `revenue_variance` | `number`, nullable |  | NUMERIC(8,4). |
| `status` | `string` — one of `pending`, `matched`, `flagged`, `disputed`, `resolved`, `accepted` | yes |  |
| `resolution_notes` | `string`, nullable |  |  |
| `adjustment_id` | `integer`, nullable |  |  |
| `raw_data` | `object`, nullable |  | Adipolo publisher revenue requires a completed vast_requests lifecycle plus latest screens_live source='owned'. ID shape/event alone is insufficient; shared, external, and unjoined bundles are excl... |
| `imported_by` | `string`, nullable |  |  |
| `reviewed_by` | `string`, nullable |  |  |
| `reviewed_at` | `string`, nullable |  |  |
| `created_at` | `string` | yes |  |
| `updated_at` | `string` | yes |  |
| `applied_report_hash` | `string`, nullable |  | applied_report_hash/applied_at (mig 449) gate the hourly pricing re-apply: syncs skip the CI re-stamp + PED rebuild when the applied tuple is unchanged. Written ONLY by partnerPricingApplyGate.reco... |
| `applied_at` | `string`, nullable |  |  |


### PartnerVariants


Canonical INT-PK identity for partner variants (e.g. adipolo_lower_floor_tablet). Wave 2 of pristine-state refactor. variant_name TEXT remains as the wire-format alternate-key indefinitely.


| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `variant_id` | `string`, nullable |  | feeds_into lists tables that will gain a nullable variant_id INT FK in Wave 2 Stream B. Each is a separate PR — this PR mints the dimension only. |
| `demand_partner_id` | `integer` | yes | Backfill is 1:N from demand_partner_config.variant_names[]. Rows with NULL demand_partner_id at apply time are silently skipped — deploy-controller previous-tick guard ensures Wave 2 A1 has populat... |
| `variant_name` | `string` | yes | variant_name TEXT is the wire-format alternate-key (kept forever) — internal logs and partner integrations rely on it. Never propose dropping variant_name. |
| `public_alias` | `string` | yes | public_alias is the obfuscation alias for partner-API responses (matches partnerAlias.encodeAlias pattern: 'var_' \|\| sha256(variant_name)[0..5]). |
| `display_name` | `string`, nullable |  | vast_tag_variants JSONB on demand_partner_config still co-exists during transition. Backfill UPDATE pulls display_name from the JSONB if present (no-op today; forward-compatible). |
| `archived` | `boolean` | yes |  |
| `created_at` | `string` | yes |  |
| `updated_at` | `string` | yes |  |
| `daily_impression_cap` | `integer`, nullable |  |  |
| `daily_request_cap` | `integer`, nullable |  |  |
| `daily_budget_cap` | `number`, nullable |  | NUMERIC(10,2). |
| `request_pressure_enabled` | `boolean`, nullable |  | request_pressure_enabled=false fails open for pressure shaping only. It must not be treated as demand-source enablement. |
| `request_pressure_base_interval_seconds` | `integer`, nullable |  |  |
| `request_pressure_probe_interval_seconds` | `integer`, nullable |  |  |
| `request_pressure_defer_seconds` | `integer`, nullable |  |  |
| `request_pressure_probe_modulo` | `integer`, nullable |  |  |
| `request_pressure_probe_target_per_minute` | `number`, nullable |  | NUMERIC(10,4). |
| `request_pressure_min_pair_requests` | `integer`, nullable |  |  |
| `request_pressure_min_source_requests` | `integer`, nullable |  |  |
| `request_pressure_completion_rate_floor` | `number`, nullable |  | NUMERIC(12,8). |
| `request_pressure_confidence_level` | `number`, nullable |  | NUMERIC(6,3). |
| `request_pressure_max_multiplier` | `integer`, nullable |  |  |


### PartnerZoneCpmDaily


Per-zone per-day CPM rates from partner APIs. Used for revenue estimation and settled partner-truth allocation when partner_reconciliation_reports does not provide a newer manual/API row. PG only.


| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `demand_partner_id` | `integer`, nullable |  | Wave 2 B added demand_partner_id INT + variant_id INT FKs. Legacy partner_key TEXT kept as wire-format alternate-key. Backfill via follow-up Wave 2 B-backfill PR. |
| `variant_id` | `integer`, nullable |  |  |
| `id` | `string`, nullable |  |  |
| `partner_key` | `string` | yes |  |
| `zone_id` | `string` | yes |  |
| `zone_label` | `string`, nullable |  |  |
| `period_date` | `string` | yes |  |
| `ecpm` | `number` | yes | NUMERIC(10,4). |
| `gross_revenue` | `number`, nullable |  | NUMERIC(14,6). |
| `impressions` | `integer`, nullable |  |  |
| `requests` | `integer`, nullable |  |  |
| `fill_rate` | `number`, nullable |  | NUMERIC(6,4). |
| `source` | `string` | yes | Daily earnings aggregation may use these rows to settle gross revenue, but completed_impressions remains the event-count source for programmatic_earnings_daily.impressions_count. |
| `fetched_at` | `string` | yes |  |
| `created_at` | `string` | yes |  |
| `updated_at` | `string` | yes |  |


### Regs


| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `coppa` | `integer` |  |  |
| `gdpr` | `integer` |  | Set to 0 by the Trillboards request builder. On demand rails with wire hygiene enabled, the field carries the value from the authenticated publisher or device privacy context and is omitted when that context has none. |


### Segment


| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `string` |  | Segment identifier (IAB taxonomy ID or seller-defined) |
| `name` | `string` |  | Human-readable segment name |
| `value` | `string` |  |  |


### SellerDefinedSegmentReference


Seller-defined context segments in the `trillboards-seller-defined` data object.
These are included in `user.data[]` with `ext.segtax` omitted (seller-defined taxonomy).

### Venue Segments (trb_vn_*)
| ID | Name | Matches |
|-----|------|---------|
| trb_vn_001 | venue:fitness_gym | gym, fitness, workout, crossfit |
| trb_vn_002 | venue:restaurant_casual | casual restaurant, diner |
| trb_vn_003 | venue:restaurant_quick_service | QSR, fast food, takeout |
| trb_vn_004 | venue:bar_nightlife | bar, nightlife, club, pub, lounge |
| trb_vn_005 | venue:salon_beauty | salon, beauty, spa |
| trb_vn_006 | venue:barber_shop | barber |
| trb_vn_007 | venue:retail_general | retail, shopping, mall |
| trb_vn_008 | venue:retail_convenience | convenience, gas station |
| trb_vn_009 | venue:office_lobby | office, cowork, business |
| trb_vn_010 | venue:healthcare_waiting_room | clinic, medical, hospital, pharmacy |
| trb_vn_011 | venue:education_campus | campus, college, university, school |
| trb_vn_012 | venue:hotel_hospitality | hotel, hospitality, resort |
| trb_vn_013 | venue:transit_hub | transit, station, airport, terminal |
| trb_vn_014 | venue:entertainment | cinema, theater, arcade, event |
| trb_vn_015 | venue:residential_common | residential, apartment, condo, lobby |

### Geo Segments (trb_geo_*)
| ID | Name |
|-----|------|
| trb_geo_001 | geo:country_us |
| trb_geo_002 | geo:state_ca |
| trb_geo_003 | geo:state_ny |
| trb_geo_004 | geo:state_tx |
| trb_geo_005 | geo:city_tier_1 |

### Traffic Tier Segments (trb_trf_*)
| ID | Name | Threshold |
|-----|------|-----------|
| trb_trf_001 | traffic:very_low | < 3 people |
| trb_trf_002 | traffic:low | 3-7 people |
| trb_trf_003 | traffic:medium | 8-19 people |
| trb_trf_004 | traffic:high | 20-49 people |
| trb_trf_005 | traffic:very_high | >= 50 people |

### Dwell Time Segments (trb_dwl_*)
| ID | Name | Threshold |
|-----|------|-----------|
| trb_dwl_001 | dwell:very_short | < 3 seconds |
| trb_dwl_002 | dwell:short | 3-7 seconds |
| trb_dwl_003 | dwell:medium | 7-15 seconds |
| trb_dwl_004 | dwell:long | > 15 seconds |

### Daypart Segments (trb_dpt_*)
| ID | Name | Hours (UTC) |
|-----|------|-------------|
| trb_dpt_001 | daypart:morning | 05:00-10:59 |
| trb_dpt_002 | daypart:midday | 11:00-13:59 |
| trb_dpt_003 | daypart:afternoon | 14:00-17:59 |
| trb_dpt_004 | daypart:evening | 18:00-22:59 |
| trb_dpt_005 | daypart:late_night | 23:00-04:59 |

### Quality Flags (trb_qly_*)
| ID | Name | Meaning |
|-----|------|---------|
| trb_qly_001 | quality:geo_verified | Screen has GPS coordinates |
| trb_qly_002 | quality:edge_sensed_audience | Real-time FEIN audience data present |
| trb_qly_003 | quality:floor_defined | Publisher floor price is set |



Type: `object`


### Source


| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `fd` | `integer` |  | 1 = exchange is final decision maker |
| `tid` | `string` |  | Transaction/request ID |
| `schain` | `object` |  | Supply chain transparency |


### TrbNamespace


Trillboards extension sub-namespaces. Every sub-namespace is OPTIONAL and
is omitted entirely when the underlying signal is unavailable for that
screen — treat an absent key as "no signal", never as a zero.

This object is pinned to the request builder by
`tests/unit/openrtbExtNamespaceSpecParity.test.js`, which derives the
emitted key set from the source and fails on drift in either direction.



| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `vas` | [`VASExt`](#vasext) |  |  |
| `intent` | [`IntentExt`](#intentext) |  |  |
| `weather` | [`WeatherExt`](#weatherext) |  |  |
| `foot_traffic` | [`FootTrafficExt`](#foottrafficext) |  |  |
| `venue` | [`VenueExt`](#venueext) |  |  |
| `event` | [`EventExt`](#eventext) |  |  |


### TrillboardsAudienceExt


Real-time audience intelligence from FEIN edge AI sensors.
Updated every 10-30 seconds. These signals are MEASURED, not estimated.
Unlike CTV viewability which uses probabilistic models, Trillboards uses
on-device computer vision (ML Kit) and audio classification (YAMNet TFLite)
to provide deterministic audience data.



| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `attention_score` | `number` |  | Fraction of detected faces actively looking at the screen. 0.0 = nobody looking, 1.0 = everyone looking. Measured via gaze detection, NOT self-reported or probabilistic. |
| `audience_count` | `number` |  | Average number of faces detected during current measurement window. **Parse `audience_count`, not `live_face_count`.** A previous revision of this spec documented this field as `live_face_count` and stated that `audience_count` "was never actually emitted by the server". That was exactly backwards, and it was a confusion of two different surfaces: - **This surface** — the outbound bid request's `ext.trillboards` —   has always emitted `audience_count`. - The **inventory discovery** response (`GET /openrtb/v2/adslots`)   emits `live_face_count` inside its `fein` object. Both names are live; they belong to different documents. `live_face_count` never appears in a bid request. |
| `crowd_density` | `integer` |  | Peak face count observed in current window (max concurrent viewers) |
| `ad_receptivity` | `number` |  | Audio-derived venue receptivity score. Quiet/focused environments score higher. Based on YAMNet ambient sound classification. |
| `emotional_engagement` | `number` |  | Composite emotional engagement score from facial expression analysis. Higher = more engaged (positive or negative reaction to content). Computed from each detected person's facial expression, classified on the device. |
| `mood` | `string` — one of `happy`, `neutral`, `focused`, `calm`, `energetic`, `social`, `surprised`, `sad` |  | Dominant venue emotional state derived from facial expression analysis. **Parse `mood`, not `dominant_emotion`.** Same surface confusion as `audience_count` above: the bid request emits `mood`; the `GET /openrtb/v2/adslots` discovery response emits `dominant_emotion`. |
| `estimated_occupancy` | `integer` |  | Estimated venue occupancy (extrapolated from visible faces + venue capacity) |
| `avg_dwell_ms` | `number` |  | Average time (milliseconds) a face stays in the camera's field of view |
| `group_composition` | `object` |  | Audience composition breakdown by demographic bucket. Derived from each detected person's estimated age range and gender. |
| `screen_engagement` | `string` |  | Categorical engagement label passed through from the edge reading (e.g. `active`), NOT a score. Corrected 2026-09-01. This declared `type: number` with `minimum: 0`, `maximum: 1` and `example: 0.65`, but `_buildTrillboardsExt` assigns `audience.screenEngagement` unchanged and that value is a string everywhere it is produced. A bidder doing arithmetic on this field was getting `NaN`. The combined 0-1 metrics are `emotional_engagement` and `ad_receptivity`. |
| `audience_reaction` | `string` — one of `positive`, `neutral`, `negative`, `mixed` |  | Dominant audience reaction to current content |
| `signals_age_ms` | `integer` |  | Milliseconds since last FEIN measurement update. Lower = fresher signals. Typical: 10,000-30,000ms. If > 120,000ms, signals may be stale (screen offline or camera occluded). |
| `reliability_score` | `number` |  | Screen reliability score from unified telemetry. Factors: uptime, heartbeat regularity, GPS accuracy, FEIN measurement frequency. |
| `audience_quality` | `number` |  | Audience data quality score. High = rich FEIN signals with high confidence. Low = limited or stale audience data. |
| `screen_quality_composite` | `number` |  | Composite quality score combining reliability + audience quality + geo accuracy |
| `speech_contexts` | array of `string` |  | Shopping contexts inferred from on-device speech classification. The bid request carries the context labels; Trillboards stores the speech transcripts they come from. Omitted entirely when no context was inferred. |
| `purchase_journey_stage` | `string` |  | Where the observed audience sits in a purchase journey, derived from speech context. Omitted when no stage was inferred. |
| `purchase_journey_urgency` | `string` |  | Urgency signal accompanying `purchase_journey_stage`. Omitted when no stage was inferred. |
| `canonical_brand_matches` | array of `string` |  | De-duplicated canonical brand names matched from speech evidence. Presence of a competitor brand is a conquesting opportunity. Omitted when no brand was matched. |
| `signal_derived` | `object` |  | Camera-less derived audience signals, carried alongside the IAB and Trillboards segment sets. Present only when derived signals exist for the screen AND the request is not a clean curated-wire request. |
| `agent_endpoint` | `string` |  | AI-agent discovery: the Trillboards MCP endpoint, so a DSP's agent can negotiate directly from the bid stream. Constant for all requests. |
| `adcp_discovery` | `string` |  | AI-agent discovery: the AdCP `adagents.json` document. Constant for all requests. |


```json
{
  "attention_score": 0.87,
  "audience_count": 8,
  "crowd_density": 12,
  "ad_receptivity": 0.75,
  "emotional_engagement": 0.82,
  "mood": "happy",
  "estimated_occupancy": 25,
  "avg_dwell_ms": 4500,
  "group_composition": {
    "solo": 3
  },
  "screen_engagement": "active",
  "audience_reaction": "positive",
  "signals_age_ms": 12480
}
```


### User


User object carrying the screen's audience segments. The
trillboards-audience segments are read from the per-person records the
screen keeps for the people its camera detects. Trillboards recognises
the same person across screens and days and links people to device
keys and mobile advertising IDs.



| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | array of [`UserData`](#userdata) |  |  |


### UserData


IAB Audience Taxonomy 1.1 segments and seller-defined context segments.
Three data sources may be present in user.data[]:

1. **trillboards-audience**: Real-time audience segments from FEIN edge AI
2. **trillboards-census**: US Census ACS neighborhood demographics
3. **trillboards-seller-defined**: Seller-defined context segments (venue, traffic, dwell, daypart)



| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `string` — one of `trillboards-audience`, `trillboards-census`, `trillboards-seller-defined` |  | Data provider identifier |
| `name` | `string` |  | Human-readable data source name |
| `segment` | array of [`Segment`](#segment) |  |  |
| `ext` | `object` |  |  |


### VASExt


Verified Attention Seconds — historical attention quality metrics
for this screen. Based on 7-day rolling data from the attention_ledger.

VAS = face_count * attention_score * dwell_seconds. It captures the TOTAL
attention delivered, not just whether someone was present. A screen with
3 attentive viewers for 10 seconds scores higher than 10 distracted
viewers for 1 second.



| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `pricing_model` | `string` — one of `vas` |  | Always "vas" — indicates attention-based pricing is available |
| `predicted_vas` | `number` |  | Predicted VAS for the next impression based on historical patterns |
| `vas_7d` | `number` |  | Average VAS (Viewable Attention Score) over the past 7 days. (Field renamed 2026-05 from `historical_vas_avg` to align with the buyer-facing field name on /adslots — partners migrating from a stale spec should rename their parser key.) |
| `median_vas` | `number` |  | Median VAS (more robust to outliers than mean) |
| `p90_vas` | `number` |  | 90th percentile VAS — the quality floor for top 10% of measurements |
| `vas_floor` | `number` |  | Minimum VAS threshold for quality-gated pricing |
| `vas_measurements_7d` | `integer` |  | Number of VAS measurements in the past 7 days (data density) |
| `avg_face_count` | `number` |  | Average face count across measurements |
| `avg_attention_score` | `number` |  | Average attention score across measurements |
| `last_measurement` | `string` |  | Timestamp of most recent VAS measurement |


### VenueExt


Venue quality and neighborhood context for the screen's location.

Every field is omitted unless it carries positive value — a zero or
`unknown` is dropped rather than broadcast, because a zero on the bid
stream reads as "worthless" and suppresses bids. An absent field means
"not known", not "scored zero".



| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `price_level` | `integer` |  | Venue price tier (0 = cheapest, 4 = most expensive) |
| `rating` | `number` |  | Venue rating on a 5-point scale |
| `quality_score` | `number` |  | Composite venue quality score. Omitted when not positive. |
| `neighborhood_class` | `string` |  | Neighborhood classification for the surrounding area. Omitted when unknown (the literal `unknown` is never sent). |
| `amenity_density` | `number` |  | Density of nearby amenities (retail, dining, transit) around the screen. Omitted when not positive. |


### WeatherExt


Contextual weather data for the screen's location.
Cached in Redis with 60-minute TTL from WeatherAPI.com.
DSPs use weather for contextual targeting (e.g., ice cream ads when hot,
umbrella ads when raining).



| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `temp_f` | `number` |  | Current temperature in Fahrenheit |
| `feels_like_f` | `number` |  | Feels-like temperature accounting for humidity and wind chill |
| `humidity` | `number` |  | Relative humidity percentage (0-100) |
| `condition` | `string` |  | Weather condition description |
| `uv_index` | `number` |  | UV index (0-11+) |
| `wind_mph` | `number` |  | Wind speed in miles per hour |
| `is_raining` | `boolean` |  | Whether it is currently raining at the screen location |
| `temp_segment` | `string` — one of `hot`, `warm`, `cool`, `cold` |  | Temperature classification for easy targeting: - hot: >= 85F - warm: 65-84F - cool: 45-64F - cold: < 45F |


```json
{
  "temp_f": 85,
  "feels_like_f": 88,
  "humidity": 75,
  "condition": "Partly cloudy",
  "uv_index": 8,
  "wind_mph": 12,
  "is_raining": false,
  "temp_segment": "hot"
}
```
