CheapCharts API Skill
A free, public-API price tracker for digital movies and TV shows across iTunes (Apple TV), Amazon Prime Video, Vudu, and Google Play. No authentication. Parallel calls are safe - the bundled
deals.pyscript uses 8 concurrent DetailData workers.
Repo: https://github.com/tracerman/cheapcharts-skill
API Base URL: https://buster.cheapcharts.de/v1/gptapi/
ATL check: https://buster.cheapcharts.de/v1/DetailData.php (unofficial internal endpoint - Pitfall #17)
When to Use
- Movie/TV prices, deals, or discounts on digital stores (iTunes/Apple TV, Amazon, Vudu, Google Play)
- Charts (what's popular), recommendations by genre, cross-store price comparison
- Price history / "has it ever been cheaper?" / all-time-low questions
- User mentions CheapCharts directly
Don't use for: physical media (Blu-ray/DVD), streaming subscription catalogs (Netflix/Disney+), movie reviews (web search instead), video games (no public API - see EXTRAS.md).
Adaptive Interaction Contract
CheapCharts is a direct-query skill, not a menu wizard. Route the user's requested outcome into exactly one of three user-visible lanes:
| Lane | User job | Result |
|---|---|---|
| Browse | Find a set of current offers within a market, window, or filters | Deal rows or an explicit Browse state |
| Inspect | Ask a factual question about one title, offer, ATL status, or history | Factual answer only; never an unsolicited verdict |
| Decide | Ask whether to buy one resolved offer now | Buy / Wait / Skip receipt or an explicit non-decision state |
Discovery, capability checks, and conversation/session control are internal classifications, not additional lanes. Execute a specific opening request immediately when its meaning can be preserved. On a bare invocation, show this natural orientation sentence at most once per conversation: “I can show current movie or TV deals, inspect a title's price history, or help decide Buy / Wait / Skip—what are you looking for?” A reply routes directly. Do not ask for optional preferences before returning useful work.
Use meaning, not keywords alone. A set or filtered feed enters Browse. A factual title question such as “Has Heat ever been cheaper?” enters Inspect. Purchase advice such as “Should I buy Heat now?” enters Decide. Evaluative but non-advisory wording such as “Is Heat cheap?” defaults to factual Inspect and may offer a decision receipt as a next action.
Conversation scope and title branches
Maintain bounded conversation context; never treat it as a stored profile:
| Frame | Holds | Carry rule |
|---|---|---|
| Environment | Store and country | Visible defaults. A title-local comparison does not switch the Browse environment unless the user says “switch,” “from now on,” or equivalent. |
| Saved Browse frame | Lane, window, subtype, quality, price, savings, genre/rating, and sort | Compatible refinements inherit it. A title branch is pushed over it. |
| One-title branch | One canonical title/offer identity and title-local decision context | Branches never nest; selecting a second title replaces the first. |
Durability follows expressed scope. “My budget is $15” may carry to sibling title decisions with visible attribution; “show movies under $15” stays Browse-local; “for this one, I can wait a month” stays title-local. Ambiguous Decide shorthand updates only the active title and is labeled “for this title.” Sort clears on a lane or subtype pivot because it can behave as a hidden category filter; other filters carry only where their meaning and capability remain valid.
Entering Inspect or Decide from a rendered Browse row carries that snapshot's canonical title identity, market, format, and current offer. Human row numbers are bound to one rendered snapshot. After a newer snapshot makes “#3” stale or ambiguous, echo the likely title and ask for confirmation; structured consumers must use canonical identity, never position alone.
“Back” or “back to the deals” pops the title branch, restores the saved Browse criteria, refreshes live prices, and fully re-echoes the restored scope. Title-local constraints do not leak into the feed. “Start over” clears active frames and returns to the one-line orientation with visible market defaults.
One-question information-value gate
Ask at most one blocking question before a useful result or explicit non-result state, and only when materially different executions are plausible and a default would mislead. Ask for candidate selection before a decision on an ambiguous title. For an unsupported head constraint, offer one bounded remove-versus-pivot choice rather than silently broadening. Ask before a pivot that would lose the core job, requested subtype, or every meaningful user-set constraint. Missing optional decision preferences never justify a questionnaire; use visible neutral defaults.
Capability and degradation tiers
Validate capability before fetching, filtering, retrying, or widening:
- Native: execute and present ordinary results.
- Composable: combine documented calls or client-side filtering, label how the candidate set was formed, and avoid exhaustive-catalog claims. Movie-bundles-only and complete-series-only remain composable until completeness is verified.
- Degraded: only on a user-initiated pivot where the new lane and at least one meaningful user-set constraint survive. Lead with every unhonored dimension, label the preview
degraded-results, show its actual scope, and offer bounded corrections. - Unsupported or unreliable: refuse or ask the bounded choice. Never replace rental prices with purchase prices; never claim TV genre or seasons+4K was honored.
Defaults, derived values, or clearly soft context may be dropped only with visible provenance. An explicit current-request constraint or the request's core job must never disappear silently.
Empty and error behavior
Empty means a valid effective query matched nothing; error means data could not be obtained. Keep them distinct.
- Explicit Browse today empty: headline that nothing was recorded as changed today and note CheapCharts source lag. Optionally show one structurally separate Nearby: last 3 days supplement with its own full scope. Keep the active frame on today and never widen twice.
- Vague “latest” or “recent” empty: widen once when useful, label both requested and fallback windows, and preserve both scopes.
- Exact title/date empty: answer the exact null and offer history or a wider window; do not widen silently.
- Other constrained Browse empty: preserve the scope and offer the single most useful relaxation.
- Unresolved title: return
not_foundor candidatedisambiguation, never a fabricated match. - Incomplete decision evidence: return
insufficient_evidencewith known facts and missing requirements, never a low-substance verdict. - Fetch or schema failure: return
errorwith a useful retry or next action, distinct from empty.
Applied scope and source truth
Derive human and structured output from one canonical applied scope. Every result gets a compact human scope line. On ordinary refinements, provenance-tag non-obvious inherited, default-filled, or dropped dimensions; fully tag pivots, resets, composable/degraded execution, restored Browse frames, and returns after dormancy.
Structured envelopes must contain a complete self-contained applied_scope. Each dimension records provenance such as user_set, inherited, default, or dropped_unsupported; fallback windows, substitutions, dropped filters, and retries must also be represented. Product states are results, degraded-results, decision, disambiguation, empty, insufficient_evidence, not_found, unsupported, and error. Existing raw batch --json remains a list (or [] when empty). Use additive --scoped-json for the Browse envelope and --decide TITLE --json for the discriminated one-title decision envelope.
Always label the actual data source. Deals are current deals/price drops. Charts are CheapCharts chart rankings, Topseller is CheapCharts cross-store top sellers, and Recommendations is CheapCharts recommendations. Never present Charts, Topseller, Recommendations, Search results, or a wider fallback as if they were the requested deal feed.
Decide receipts
A decision requires a confidently resolved offer, its current price for the relevant format tier, and at least one trustworthy historical comparator: authoritative ATL status, a parseable historical floor, or a prior comparable price. Ratings and recurrence cadence are optional. If the minimum gate fails, preserve known facts in insufficient_evidence and name what is missing. This is a stateless one-title decision, not feed-wide ranking: do not store a profile, create alerts, or promise monitoring.
Apply supplied per-request budget ceiling, patience, required format, and new-purchase-versus-upgrade intent. Required format is a minimum capability (SD < HD < 4K): 4K satisfies HD or SD, and HD satisfies SD; it does not force the CLI to price a lower tier. Omitted constraints use visible neutral defaults; do not invent taste preferences. Keep objective deal strength (price position, discount credibility, observed sale behavior) separate from personal fit (optional constraints), then combine them into the offer-specific action:
- Buy: buy this resolved offer now.
- Wait: defer for a plausibly better opportunity.
- Skip: this current offer fails the value or constraint threshold; it is not criticism of the title and does not mean “never buy.”
Show High, Medium, or Low confidence together with evidence coverage, conflicts, missing signals, and downgrade reasons. Confidence is not a numeric probability and is separate from deal strength. Show a broad recurrence window only when history is sufficiently deep and regular; otherwise give descriptive cadence guidance without a date estimate. Never promise an exact next-sale date.
Lead the human receipt with resolved title/offer, action, confidence, and decisive reason; then objective deal strength, personal-fit effect or neutral-default label, supported recurrence guidance, applied constraints, caveats, and useful actions. A follow-up constraint recalculates the same resolved title and highlights what changed. Keep the full timeline available as deeper evidence.
Contract examples
| Request | Required behavior |
|---|---|
Bare /cheapcharts |
Show the one-line orientation once; do not run a default feed or display a menu. |
| “Today's Apple TV movie deals under $10” | Browse immediately; echo today, Apple TV/iTunes US, movies, and the price cap. |
| “Has Heat ever been cheaper?” | Inspect factually; no Buy / Wait / Skip. |
| “Should I buy Heat now?” | Decide if the evidence gate passes; otherwise return the exact non-decision state. |
| “Only 4K” | Inherit compatible Browse dimensions and label non-obvious inherited scope. |
| “What about TV?” after a movie-only scope | Clear sort and name unsupported casualties; show a degraded preview only if meaningful scope survives, otherwise ask once. |
| “Should I buy #3?” | Resolve against the displayed snapshot; confirm the title if a newer snapshot has made the row stale. |
| “Back to the deals” | Restore saved Browse criteria, refresh prices, and drop title-local context. |
| “Start over” | Clear frames and return to orientation with visible defaults. |
| “What does Heat cost to rent?” | Return unsupported; never substitute purchase data. |
| Explicit today with no matches | Preserve today; optionally add one separate, labeled last-three-days supplement. |
The Bundled Script (use this first)
scripts/deals.py (stdlib-only, Python 3.9+) pulls Deals, enriches every candidate with the authoritative ATL flag from DetailData in parallel (8 workers, ~12s for 50 items), preserves the API's sort order, and emits a markdown table or JSON. It handles the pitfalls (status checks, DetailData vocabulary, genre validation) so you don't have to.
python scripts/deals.py # all current deals (iTunes US), ATL column
python scripts/deals.py --since 1 # only items whose price changed today
python scripts/deals.py --atl-only # only rows at their all-time low
python scripts/deals.py --title "Fight Club" # single-title ATL check
python scripts/deals.py --title "Fight Club" --history # + full price-history timeline (sale windows, floor)
python scripts/deals.py --type seasons # TV seasons
python scripts/deals.py --quality sdOnly # strict SD results, using the SD price/date/ATL tier
python scripts/deals.py --genre horror # movie-only genre filter; other types rejected (Pitfall #21)
python scripts/deals.py --max-price 4.99 --min-savings 3 --limit 30
python scripts/deals.py --sort greatestSavings # bundles dominate this sort (Pitfall #35)
python scripts/deals.py --exclude-bundles # individual movies only (they carry ratings)
python scripts/deals.py --store amazon --title "Heat" # non-iTunes: prefer --title (Pitfall #30)
python scripts/deals.py --json # machine-readable output for pipelines
python scripts/deals.py --scoped-json # additive Browse envelope with applied_scope
python scripts/deals.py --decide "Heat" # Buy / Wait / Skip receipt; --title stays factual
python scripts/deals.py --decide "Heat" --budget 10 --required-format 4k --intent upgrade
python scripts/deals.py --decide "Heat" --json # discriminated decision envelope
Exit codes: 0 deals found or a decision issued, 1 legitimate empty/non-decision state, 2 API, usage, or response-schema error. In raw batch --json mode, empty results emit [] on stdout and diagnostics go to stderr; decision and scoped Browse modes instead emit their explicit state envelope. Failed DetailData lookups are counted and reported in the table header; if all fail, exit is 2.
Default sort is latestPricechange (freshest drops first). Output columns: Title (links to Apple TV) | Fmt | Now | Was | Save | IMDb | RT | Date | ATL | Buy | History.
Default Workflow
- "Today's drops" / "what changed today" ->
python scripts/deals.py --since 1. If empty, preserve today and optionally run--since 3only as a separately labeled Nearby supplement; CheapCharts lags Apple by hours-to-a-day (Pitfall #14). For vague “latest,” one labeled widening is allowed. - "What's at its all-time low?" ->
python scripts/deals.py --atl-only. - "Is [title] at its lowest ever?" ->
python scripts/deals.py --title "<name>". - Anything the script doesn't cover (charts, recommendations, cross-store, search) -> call the API directly per the decision table below; literal curl commands in RECIPES.md.
- Always check the response
statusfield first -status=errorfails silently otherwise (Pitfall #15).
Decision Table
| User asks... | Do this | Why |
|---|---|---|
| "Latest deals" / "today's drops" | deals.py --since 1 |
latestPricechange sort + DetailData date verification |
| "Deals under X" | deals.py --max-price X |
maxPrice filters server-side in the selected store/country currency |
| "Highly-rated deals" | Deals API with imdbRating/rottenTomatoesRating |
Both filter server-side on Deals (NOT Recommendations, Pitfall #23) |
| "4K / Dolby Vision / Atmos movie deals" | deals.py --quality 4k; filter Vision/Atmos client-side |
Movie quality=4k works; has4K=1 is ignored (#39, #16) |
| "Newest releases on sale" | Deals sort=releaseDate + strip placeholder dates |
Ascending only (#11); bundles carry fake 2030 dates (#12) |
| "What's popular / selling?" | Charts (label CheapCharts chart rankings) or Topseller (label CheapCharts cross-store top sellers) | Topseller is the only multi-store batch endpoint; neither source is a Deals feed |
| "How much is [title]?" | Search -> Prices only when priceFollowUpItemType=buymovies |
Search resolves the IMDb ID; rental follow-ups are unsupported (#38); iTunes = Apple TV (#19) |
| "Rental price of [title]?" | Explain that no public-API rental price is available | Deals/Charts reject rentalmovies; Prices silently returns purchase data (#38) |
| "Complete series deals" | Deals itemType=seasons, filter isBundle=1 client-side |
Season genre filter is broken (#21) |
| "Recommend a [genre] movie" | Recommendations with a specific genre; label CheapCharts recommendations | With genre=All it returns chart data (#18); never relabel it as deals |
| "Is [title] at its ATL?" / "lowest ever?" | deals.py --title or DetailData IsLowest flags |
Only DetailData exposes ATL; the flags beat parsing (#26) |
| "When was [title] on sale?" / "price history" / "when will it be on sale again?" | deals.py --title "<name>" --history |
Render the timeline and observed cadence; give a broad recurrence window only when history is deep and regular, otherwise descriptive guidance with no estimate or promise (EXTRAS.md) |
| "What just came off a sale?" | DetailData on candidates; report priceBefore < price rows |
Sale-ended rows are "next drop target" signal (#31) |
| Compare across all 4 stores | Search -> 4x Prices calls, or Topseller | Note Movies Anywhere implications (EXTRAS.md) |
| "Should I buy [title] now?" | Resolve the offer, fetch its quality-tier current price and trustworthy history, then use explicit decision mode | Purchase advice must produce a transparent one-title receipt or non-decision state; factual --title remains factual |
Critical Pitfalls (the ones that silently break workflows)
Full list of 40 with evidence and dates: references/PITFALLS.md.
- DetailData speaks a different vocabulary:
itemType=moviesorseasons, NOTbuymovies- the wrong value errors, and looks like "no data" if you skip the status check (#13). - Always check
statusbefore iteratingresults- errors return{"status":"error"}with an empty result shape that mimics "no deals" (#15). has4K=1is silently ignored on Deals/Charts - usequality=4kfor a new movie Deals request; filterhas4Kclient-side only for an already-fetched unfiltered response, and keep Vision/Atmos client-side (#16, #39).genreis broken for seasons everywhere;deals.pyrejects--genreunless--type buymovies. Unknown genre values on movies silently return EVERYTHING at the API level, so the script rejects those too (#21, #22).priceHdEvolutionvalues are absolute prices, NOT deltas - the sign is only the change direction; summing them produces garbage. For "at ATL now?" use theIsLowestflags; for timelines use--history, which parses it correctly (#26).- No batch DetailData - ATL enrichment is N+1 by design; use the parallel script (#28).
- Never fabricate store URLs -
productPageUrl/iTunesUrl/cheapChartsProductPageUrlare in the response; guessed Apple TV slugs 404 (#32). - Sort choice = category filter:
greatestSavingssurfaces bundles,latestPricechangesurfaces individual movies (#35). - Rental prices are unavailable through the public API: Deals/Charts reject
rentalmovies, while Prices silently returns purchases (#38). - Movie
quality=filters work on Deals: usequality=4kinstead of ignoredhas4K=1; seasons+4k is rejected, while supported SD modes use the SD DetailData tier (#39). - Currency comes from the priced response: Deals carries batch currency and DetailData carries single-title/history currency; Search currency is not reliable (#40).
Presentation Guidelines
Lead with the effective scope and use the canonical applied scope for both the human result and structured output. Name actual source and degradation before any rows.
Always include
cheapChartsProductPageUrl(price-history page) and the store buy link (productPageUrl/iTunesUrlfrom DetailData). Never guess URLs; if a field is missing, show plain text and say "store URL unavailable".Show savings (
priceBefore - price, and %) and skip rows where savings <= $0 unless reporting them as "sale ended".Always include an ATL column when data was enriched with DetailData:
ATL= at the historical floor,-= ordinary sale. Standard table:Title Genre Now Was Save IMDb ATL Changed Title Genre $X.XX $Y.YY $Z.ZZ (N%) N.N ATL YYYY-MM-DD "Sale ended" rows are reportable signal (Pitfall #31): when
priceBefore < price, add aStatuscolumn (on sale/sale ended/stable) instead of rendering empty cells - these are useful manual-watch candidates, not alerts created by the skill.Ratings come from Deals candidates, not DetailData - render IMDb/RT only for individual movies (
isMovieBundle == 0); bundles and seasons legitimately have none (#37).Filter noise: Search with
itemType=allreturns ebooks/audiobooks/albums too - filter bymediaTypeunless asked otherwise.Mention compounding savings when relevant: gift-card stacking and seasonal sale windows are in EXTRAS.md.
Keep monetary units honest: render the response currency;
--max-priceand--min-savingsare amounts in the selected store/country currency, with no conversion.
Verification Checklist
- Request was routed as Browse, Inspect, or Decide; no internal classification was exposed as a fourth lane
- Canonical applied scope and provenance agree between human and structured output
- Row references resolve against the correct rendered snapshot; stale positions are confirmed
- Capability was validated before fetch, retry, or widening; every dropped constraint is visible
- Charts, Topseller, and Recommendations are labeled by their actual source, never as Deals
- Exact empty/error semantics were preserved; a today supplement is separate and does not mutate the active frame
- Correct endpoint for the intent (Decision Table above)
- Response
status == "success"checked before iterating (Pitfall #15) -
itemTypevocabulary correct:buymoviesfor Deals/Charts/Prices,movies/seasonsfor DetailData (Pitfall #13) - Genre values from the enum only; omitted entirely for seasons (Pitfalls #21, #22)
- Rental requests are reported as unsupported, never inferred from purchase data (Pitfall #38)
- Price, prior price, date, and ATL all come from the requested quality tier (Pitfall #39)
- ATL claims based on
priceHdIsLowest/priceSdIsLowest, not parsed evolution strings (Pitfall #26) - Fake drops filtered (
priceBefore > price, savings > $0) and sale-ended rows reported as such (Pitfalls #9, #31) - Buy/history links taken verbatim from the response (Pitfall #32)
- For non-iTunes stores: single-title lookups preferred over batch (Pitfall #30)
-
cheapChartsProductPageUrlincluded in anything shown to the user - Decide issued a verdict only with resolved offer + current tier price + trustworthy historical comparator
- Decision receipt separates objective deal strength, personal fit/defaults, and confidence coverage
Files in This Skill
scripts/deals.py- the parallel deal/ATL finder (primary tool; unit-tested, CI-canaried)RECIPES.md- literal curl commands for every workflow + cron prompt templatesreferences/API.md- full endpoint/parameter/enum/field referencereferences/PITFALLS.md- all 40 empirically-verified API pitfallsreferences/EXTRAS.md- gift-card stacking, Movies Anywhere, seasonal sale calendar, CheapCharts Gamesexamples/- real output screenshots
Source
- API docs: https://www.cheapcharts.com/us/ai (llms.txt) - when llms.txt and this skill disagree, llms.txt wins, then verify empirically
- Website: https://www.cheapcharts.com
- The API is free and public, designed for AI agents. No auth headers needed.