|
| 1 | +--- |
| 2 | +name: Pricing output contract |
| 3 | +description: Contract for `pricingInfoToStructured` and `pricingInfoToString` — what `fetch-actor-details` and `search-actors` return. |
| 4 | +--- |
| 5 | + |
| 6 | +# Pricing output contract |
| 7 | + |
| 8 | +Contract for `pricingInfoToStructured` and `pricingInfoToString` in `src/utils/pricing_info.ts`. |
| 9 | + |
| 10 | +## Goal |
| 11 | + |
| 12 | +Keep the contract stable. This branch diverged; realign it to: |
| 13 | + |
| 14 | +- `fetch-actor-details` → **complete** pricing, same as master. |
| 15 | +- `search-actors` → **simplified** pricing, same shape as master but `tieredPricing` filtered to the user's tier. Unknown/missing tier → fall back to `FREE`. |
| 16 | + |
| 17 | +**Structured data shape is identical in both modes.** Every field that appears in complete mode appears in simplified mode. The differences in simplified mode: |
| 18 | + |
| 19 | +- `tieredPricing` arrays contain exactly one entry (the resolved tier). |
| 20 | +- `PAY_PER_EVENT` events that were filtered from a tiered map also carry top-level `priceUsd` = resolved tier's price. Widget clients that skip the FREE tier in `tieredPricing` (see `src/web/src/utils/formatting.ts`) need this as a fallback so FREE-tier users see a concrete price instead of a generic "Pay per event" string. |
| 21 | +- Top-level `pricingNote` is set only when the Actor has **multiple tiers** *and* they resolve consistently. Single-tier Actors don't get a note (the "higher tiers may offer lower prices" promise is vacuous). |
| 22 | +- For `PAY_PER_EVENT` only: when `events.length > 5`, event `description` is omitted from simplified output and top-level `eventDescriptionsOmitted` / `eventDescriptionsNote` are set. |
| 23 | + |
| 24 | +Both modes include top-level `userTier` (the user's plan tier). |
| 25 | + |
| 26 | +`isFree` is not part of the output — consumers derive it from `model === 'FREE'`. |
| 27 | + |
| 28 | +## Two modes |
| 29 | + |
| 30 | +| Caller | Mode | `forTier` arg | `tieredPricing` array | `pricingNote` | |
| 31 | +|---|---|---|---|---| |
| 32 | +| `fetch-actor-details` | **complete** | always provided | full matrix, all tiers | absent | |
| 33 | +| `search-actors` | **simplified** | always provided | one entry — the resolved tier | present when the resolved tier is consistent across the Actor | |
| 34 | + |
| 35 | +## Rules |
| 36 | + |
| 37 | +1. Complete mode preserves the **full tiered matrix** from master (no tier data lost) and adds `userTier`. The **text** representation is reformatted from master for consistency with simplified mode — see *Text output notes* below. |
| 38 | +2. Simplified mode has the same field shape as complete mode; `tieredPricing` arrays are filtered to a single entry. Top-level `pricingNote` is added when the resolved tier is consistent across the Actor. |
| 39 | +3. Tier resolution (simplified mode only): (a) `forTier` match in the actor's map, (b) `FREE` fallback, (c) first entry. |
| 40 | +4. `userTier` is always the user's plan tier (the `forTier` value), even if we had to fall back to a different tier for prices in simplified mode. |
| 41 | +5. `pricingNote` appears in simplified mode only when the Actor has **multiple tiers** (for `PRICE_PER_DATASET_ITEM` / `FLAT_PRICE_PER_MONTH`: `tieredPricing` has more than one entry; for `PAY_PER_EVENT`: at least one event's tiered map has more than one entry) **and** tiers resolve consistently across the Actor. It names the **resolved** tier (not `userTier`, if they differ). Single-tier Actors never get a note. |
| 42 | +6. `pricingNote` is **omitted** in simplified mode when PAY_PER_EVENT events resolve to different tiers (e.g. event A offers GOLD, event B only offers FREE — no single actor-level label is truthful). Per-event `tieredPricing` arrays are still filtered correctly. |
| 43 | +7. Simplified `PAY_PER_EVENT` keeps event descriptions when `events.length <= 5`. |
| 44 | +8. Simplified `PAY_PER_EVENT` omits event descriptions when `events.length > 5`, keeps title + price, and sets: |
| 45 | + - `eventDescriptionsOmitted: true` |
| 46 | + - `eventDescriptionsNote: "Event descriptions were omitted because this actor has many pricing events. Use fetch-actor-details for full pricing details."` |
| 47 | +9. Single-tier actors (raw data already has only one tier) produce the same `tieredPricing` output in both modes — a 1-element array. |
| 48 | +10. `trialMinutes` is shown in both modes for `FLAT_PRICE_PER_MONTH`. |
| 49 | +11. `FREE` actors (`pricingModel === 'FREE'` or null input) return `{ "model": "FREE", "userTier": "<tier>" }` in both modes. |
| 50 | + |
| 51 | +## `pricingNote` wording |
| 52 | + |
| 53 | +``` |
| 54 | +Prices shown are for <TIER> tier. Higher tiers may offer lower prices — use fetch-actor-details to see the full pricing table. |
| 55 | +``` |
| 56 | + |
| 57 | +`<TIER>` ∈ `FREE`, `BRONZE`, `SILVER`, `GOLD`, `PLATINUM`, `DIAMOND` — the **resolved** tier. |
| 58 | + |
| 59 | +--- |
| 60 | + |
| 61 | +## Examples |
| 62 | + |
| 63 | +Using `compass/crawler-google-places` pricing (`PAY_PER_EVENT`). User on `GOLD` in all examples unless noted. |
| 64 | + |
| 65 | +### E1. `fetch-actor-details` — complete mode |
| 66 | + |
| 67 | +**Structured:** |
| 68 | +```json |
| 69 | +{ |
| 70 | + "model": "PAY_PER_EVENT", |
| 71 | + "userTier": "GOLD", |
| 72 | + "events": [ |
| 73 | + { |
| 74 | + "title": "Scraped place", "description": "...", |
| 75 | + "tieredPricing": [ |
| 76 | + { "tier": "FREE", "priceUsd": 0.004 }, |
| 77 | + { "tier": "BRONZE", "priceUsd": 0.004 }, |
| 78 | + { "tier": "SILVER", "priceUsd": 0.003 }, |
| 79 | + { "tier": "GOLD", "priceUsd": 0.0021 }, |
| 80 | + { "tier": "PLATINUM", "priceUsd": 0.00126 }, |
| 81 | + { "tier": "DIAMOND", "priceUsd": 0.00076 } |
| 82 | + ] |
| 83 | + }, |
| 84 | + { "title": "Actor start", "description": "...", "priceUsd": 0.00005 } |
| 85 | + ] |
| 86 | +} |
| 87 | +``` |
| 88 | + |
| 89 | +**Text:** |
| 90 | +``` |
| 91 | +This Actor is paid per event: |
| 92 | + - **Scraped place**: ... (FREE: $0.004, BRONZE: $0.004, SILVER: $0.003, GOLD: $0.0021, PLATINUM: $0.00126, DIAMOND: $0.00076 per event) |
| 93 | + - **Actor start**: ... ($0.00005 per event) |
| 94 | +``` |
| 95 | + |
| 96 | +### E2. `search-actors`, user on GOLD |
| 97 | + |
| 98 | +**Structured:** same shape as E1, `tieredPricing` filtered, `pricingNote` added. Simplified mode also mirrors the resolved price into top-level `priceUsd` on tiered events so the widget can render it for FREE-tier users (the widget's tier list skips FREE and falls back to `priceUsd`). |
| 99 | +```json |
| 100 | +{ |
| 101 | + "model": "PAY_PER_EVENT", |
| 102 | + "userTier": "GOLD", |
| 103 | + "events": [ |
| 104 | + { |
| 105 | + "title": "Scraped place", "description": "...", |
| 106 | + "priceUsd": 0.0021, |
| 107 | + "tieredPricing": [{ "tier": "GOLD", "priceUsd": 0.0021 }] |
| 108 | + }, |
| 109 | + { "title": "Actor start", "description": "...", "priceUsd": 0.00005 } |
| 110 | + ], |
| 111 | + "pricingNote": "Prices shown are for GOLD tier. Higher tiers may offer lower prices — use fetch-actor-details to see the full pricing table." |
| 112 | +} |
| 113 | +``` |
| 114 | + |
| 115 | +**Text:** |
| 116 | +``` |
| 117 | +This Actor is paid per event: |
| 118 | + - **Scraped place**: ... ($0.0021 per event) |
| 119 | + - **Actor start**: ... ($0.00005 per event) |
| 120 | +Prices shown are for GOLD tier. Higher tiers may offer lower prices — use fetch-actor-details to see the full pricing table. |
| 121 | +``` |
| 122 | + |
| 123 | +### E3. `search-actors`, user on DIAMOND but actor doesn't offer DIAMOND → fall back to FREE |
| 124 | + |
| 125 | +**Structured:** `userTier` is still `DIAMOND` (the user's plan). `tieredPricing` uses the FREE fallback. `pricingNote` names FREE (the **resolved** tier). Top-level `priceUsd` mirrors the resolved price (widget fallback). |
| 126 | +```json |
| 127 | +{ |
| 128 | + "model": "PAY_PER_EVENT", |
| 129 | + "userTier": "DIAMOND", |
| 130 | + "events": [ |
| 131 | + { |
| 132 | + "title": "Scraped place", "description": "...", |
| 133 | + "priceUsd": 0.004, |
| 134 | + "tieredPricing": [{ "tier": "FREE", "priceUsd": 0.004 }] |
| 135 | + }, |
| 136 | + { "title": "Actor start", "description": "...", "priceUsd": 0.00005 } |
| 137 | + ], |
| 138 | + "pricingNote": "Prices shown are for FREE tier. Higher tiers may offer lower prices — use fetch-actor-details to see the full pricing table." |
| 139 | +} |
| 140 | +``` |
| 141 | + |
| 142 | +### E4. Single-tier actor (only FREE bucket defined) |
| 143 | + |
| 144 | +**Complete mode** (fetch-actor-details) — `tieredPricing` is preserved as a 1-element array, no `pricingNote`: |
| 145 | +```json |
| 146 | +{ |
| 147 | + "model": "PAY_PER_EVENT", |
| 148 | + "userTier": "GOLD", |
| 149 | + "events": [ |
| 150 | + { |
| 151 | + "title": "Scraped place", "description": "...", |
| 152 | + "tieredPricing": [{ "tier": "FREE", "priceUsd": 0.004 }] |
| 153 | + } |
| 154 | + ] |
| 155 | +} |
| 156 | +``` |
| 157 | + |
| 158 | +**Simplified mode** (search-actors) — same shape plus `priceUsd` for widget fallback. **No `pricingNote`** because the Actor has only one tier: |
| 159 | +```json |
| 160 | +{ |
| 161 | + "model": "PAY_PER_EVENT", |
| 162 | + "userTier": "GOLD", |
| 163 | + "events": [ |
| 164 | + { |
| 165 | + "title": "Scraped place", "description": "...", |
| 166 | + "priceUsd": 0.004, |
| 167 | + "tieredPricing": [{ "tier": "FREE", "priceUsd": 0.004 }] |
| 168 | + } |
| 169 | + ] |
| 170 | +} |
| 171 | +``` |
| 172 | + |
| 173 | +**Text (both modes, identical):** |
| 174 | +``` |
| 175 | +This Actor is paid per event: |
| 176 | + - **Scraped place**: ... ($0.004 per event) |
| 177 | +``` |
| 178 | + |
| 179 | +### E5. `PRICE_PER_DATASET_ITEM`, `fetch-actor-details` |
| 180 | + |
| 181 | +**Structured:** |
| 182 | +```json |
| 183 | +{ |
| 184 | + "model": "PRICE_PER_DATASET_ITEM", |
| 185 | + "userTier": "GOLD", |
| 186 | + "pricePerUnit": 0.005, |
| 187 | + "unitName": "result", |
| 188 | + "tieredPricing": [ |
| 189 | + { "tier": "FREE", "pricePerUnit": 0.005 }, |
| 190 | + { "tier": "BRONZE", "pricePerUnit": 0.004 }, |
| 191 | + { "tier": "GOLD", "pricePerUnit": 0.002 } |
| 192 | + ] |
| 193 | +} |
| 194 | +``` |
| 195 | + |
| 196 | +**Text:** |
| 197 | +``` |
| 198 | +This Actor has tiered pricing per 1000 results: FREE: $5, BRONZE: $4, GOLD: $2. |
| 199 | +``` |
| 200 | + |
| 201 | +### E6. `PRICE_PER_DATASET_ITEM`, `search-actors`, user on GOLD |
| 202 | + |
| 203 | +**Structured:** |
| 204 | +```json |
| 205 | +{ |
| 206 | + "model": "PRICE_PER_DATASET_ITEM", |
| 207 | + "userTier": "GOLD", |
| 208 | + "pricePerUnit": 0.002, |
| 209 | + "unitName": "result", |
| 210 | + "tieredPricing": [{ "tier": "GOLD", "pricePerUnit": 0.002 }], |
| 211 | + "pricingNote": "Prices shown are for GOLD tier. Higher tiers may offer lower prices — use fetch-actor-details to see the full pricing table." |
| 212 | +} |
| 213 | +``` |
| 214 | + |
| 215 | +**Text:** |
| 216 | +``` |
| 217 | +This Actor costs $2 per 1000 results. Prices shown are for GOLD tier. Higher tiers may offer lower prices — use fetch-actor-details to see the full pricing table. |
| 218 | +``` |
| 219 | + |
| 220 | +### E7. `FLAT_PRICE_PER_MONTH`, `search-actors`, user on GOLD |
| 221 | + |
| 222 | +**Structured:** |
| 223 | +```json |
| 224 | +{ |
| 225 | + "model": "FLAT_PRICE_PER_MONTH", |
| 226 | + "userTier": "GOLD", |
| 227 | + "pricePerUnit": 20, |
| 228 | + "trialMinutes": 10080, |
| 229 | + "tieredPricing": [{ "tier": "GOLD", "pricePerUnit": 20 }], |
| 230 | + "pricingNote": "Prices shown are for GOLD tier. Higher tiers may offer lower prices — use fetch-actor-details to see the full pricing table." |
| 231 | +} |
| 232 | +``` |
| 233 | + |
| 234 | +**Text:** |
| 235 | +``` |
| 236 | +This Actor is rental and costs $20 per month, with a trial period of 7 days. Prices shown are for GOLD tier. Higher tiers may offer lower prices — use fetch-actor-details to see the full pricing table. |
| 237 | +``` |
| 238 | + |
| 239 | +### E8. FREE actor |
| 240 | + |
| 241 | +**Structured (both modes):** |
| 242 | +```json |
| 243 | +{ "model": "FREE", "userTier": "GOLD" } |
| 244 | +``` |
| 245 | + |
| 246 | +**Text (both modes):** |
| 247 | +``` |
| 248 | +This Actor is free to use. You are only charged for Apify platform usage. |
| 249 | +``` |
| 250 | + |
| 251 | +--- |
| 252 | + |
| 253 | +## Text output notes |
| 254 | + |
| 255 | +- Prices render as `$<n>` — no trailing `USD`, no forced decimals, no thousands separator (`1000`, not `1,000`). |
| 256 | +- Complete mode with multi-tier: tiers listed inline, comma-separated. |
| 257 | +- Simplified mode: single price per event/unit, no tier labels inline. `pricingNote` is appended on its own line whenever the resolved tier is consistent across the Actor (including single-tier Actors). |
| 258 | +- Simplified `PAY_PER_EVENT` with more than 5 events: omit event descriptions in both structured output and text, then append `eventDescriptionsNote` on its own line in text output. |
| 259 | + |
| 260 | +### Intentional text divergence from master |
| 261 | + |
| 262 | +The text representation in complete mode is **reformatted** from master. The structured data is lossless (full tier matrix preserved), but the human-readable text is tightened for consistency with simplified mode: |
| 263 | + |
| 264 | +- `PAY_PER_EVENT` preamble: `"This Actor is paid per event:"` (master had `"This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for the following events:"`). |
| 265 | +- Single-tier events drop the `(Tiered pricing: ...)` wrapper and render as flat `($X per event)`. |
| 266 | +- `PRICE_PER_DATASET_ITEM` drops the `(in this case named X)` custom-unit-name phrasing; unit name appears directly in the sentence (`"per 1000 pages"` vs `"per 1000 results (in this case named pages)"`). |
| 267 | +- `FLAT_PRICE_PER_MONTH` renders as `"costs $X per month"` instead of `"has a flat price of X USD per month"`. |
| 268 | + |
| 269 | +Rationale: older phrasings read as boilerplate to LLMs and consume tokens without adding information the structured fields don't already carry. If you need the master phrasing verbatim, read the structured data and format it yourself. |
0 commit comments