The asset. A price is a mutable current-state field; an observation is a fact: this source reported this value at this instant. Store observations and current price becomes a view. Store prices and you have thrown away the thing you were building.
| Field | Type | Notes |
|---|---|---|
| observation_id ·always | string | Monotonic within a listing. |
| listing_id ·always | string | What was read. |
| observed_at ·always | timestamp | When the source said it. |
| ingested_at ·always | timestamp | When we learned it. Differs, sometimes materially. |
| price_minor | int | null | As read. |
| availability | enum | As read, including unknown. |
| seller_name | string | null | Captures the buy-box holder at that instant. |
| source_channel ·always | enum | direct_api | merchant_feed | verified_agent | listing_view. |
| confidence ·always | float | Weight for this specific reading. |
{
"observation_id": "obs_88412",
"listing_id": "lst_2c81",
"observed_at": "2026-09-06T14:21:59Z",
"ingested_at": "2026-09-06T14:22:31Z",
"price_minor": 19800,
"availability": "in_stock",
"seller_name": "Best Buy",
"source_channel": "direct_api",
"confidence": 1.00
}An unchanged reading still counts as a poll but writes no row. Three rows over ninety days means the price changed twice, not that we looked three times — which is exactly why any statistic computed from the series must be time-weighted.
observed_at and ingested_at diverge whenever a source lags or a backfill lands. Anything time-sensitive needs both, and collapsing them loses information you cannot reconstruct.