First build. Watch a route and departure dates; get a book-now / wait verdict scored against the
fare history accumulated in the buyer's own key-value store.
- Series identity carries every price-changing input (route, date, cabin, passengers, stops,
carrier filter), so two different products can never share one history.
- Verdicts:
BASELINE, INSUFFICIENT_HISTORY, BOOK_NOW, GOOD_PRICE, TYPICAL, WAIT,
with a separate trend and a confidence that reflects how much history exists.
BOOK_NOW requires the price to be at the observed floor AND materially below the median AND
in the cheapest quarter. An earlier percentile-only rule fired on any dip below a flat median,
which made the strongest verdict meaningless on stable routes.
- Access is left to an upstream scraper run as a sub-run and billed to the buyer, so this Actor
competes on interpretation rather than on staying unblocked.
- Charging:
actor-start only after validation passes; fare-observation only when an itinerary
produced a price, a stored history entry and a verdict.
Solves the cold start. v0.1 returned BASELINE on the first run of any itinerary and
INSUFFICIENT_HISTORY on the next two, which meant a first-time buyer paid and got nothing they
could act on - the likeliest way this Actor dies on the shelf.
- A verdict now names the
basis it was computed from: history, route, upstream_typical
or none. confidence is capped by that basis, so a cross-section of dates read in one minute
can never report high and be mistaken for a month of watching.
- New
route basis: the same route across a spread of departure dates, all priced in the same
run. It answers "of the dates you could fly, is this one cheap", which is answerable on run one.
- New
routeBaselineDates input (default 6, max 12, 0 to disable). When a run's itineraries have
too little history to score, it prices that many extra departure dates - whole weeks either side
of the watched date, so day of week is held constant. Spending stops once the run has priced 7
dates in total, which is the width a date needs to be scored at medium given that a date is
never counted in its own baseline; listing 7 departDates makes the cross-section free. Nothing
is bought when the history already answers, when a datasetId was supplied, or when the budget
is 0. A one-date first run therefore costs 7 upstream searches instead of 1.
- Route-baseline dates come back as
rowType: "routeSample" rows with their prices, so the
cross-date median is checkable by hand. They are never stored to a series and never charged.
BOOK_NOW remains reachable only from history. A set of prices read in the same minute has
no time axis, so it cannot establish the floor that verdict asserts.
- The upstream's own typical-price figure is read when a scraper publishes one, as the weakest
basis and at
low confidence. It costs no extra search. Verified against a live
memo23/google-flights-scraper run: the default upstream publishes no such field, so on it the
ladder is history -> route -> none. The tier is kept because it costs nothing when absent.
- Fixed: the carrier was read from the first segment because
carrier-name was missing from the
field list. On a connecting itinerary flown by two airlines that named only the first leg. Found
by checking the extractor against a real upstream row rather than an invented one.
- New
tests/test_upstream_shape.py pins the default upstream's real output shape as a drift
canary, so a renamed column fails here instead of on a buyer's paid run.
maxItineraries default drops from 30 to 20. The upstream bills the buyer per result returned,
so this is the largest lever on what a run actually costs, and the old default contradicted this
Actor's own documented advice that raising it rarely changes the minimum. Measured on LAX-JFK
2026-11-02: results 10-29 contained nothing below the $205 minimum already present in the first
ten. Not lowered further because missing the true minimum would corrupt the series, which is a
worse failure than a slightly larger bill.
- README now states what a run costs on BOTH halves of the bill, with three worked examples. The
Store's pricing widget shows only this Actor's events, and on a cold-start run those are about a
twelfth of the all-in cost.
BASELINE and INSUFFICIENT_HISTORY are now the last resort rather than the first answer, and
their reason text says how to get a verdict today rather than in a fortnight.
- Orchestration is two passes (price every date, then score every date), because a date cannot be
compared with the others in its run until they exist. Every upstream fetch is cached in the
run's own key-value store, so a migration mid-run does not re-buy searches the buyer already
paid for - the protection the old push-as-you-go loop gave for free.
From the first real runs on the platform, and from what the publish dialog revealed.
-
The default input no longer contains a date. departDates had a hardcoded 2026-11-02
prefill. Apify auto-tests a published Actor against its default input every day and marks the
listing "under maintenance" after three consecutive days without a non-empty dataset, so that
literal was a timer: the day it slipped into the past this Actor's own validation would reject
it, the run would fail, and the listing would publicly flag itself as broken. With no dates
supplied the Actor now watches one departure DEFAULT_LEAD_DAYS (60) out, computed at run time.
This also makes origin and destination genuinely the only required fields, which is point 2
of the -API spec and was not previously true.
-
UPSTREAM_TIMEOUT drops 240s -> 90s. A search of the default upstream measures 25-28s. At
240s a single hung sub-run plus six healthy ones is 408 seconds, which blows the five-minute
auto-test ceiling on its own.
-
New wall-clock budget on the route-baseline phase (BASELINE_BUDGET_SECONDS, 175s). No new
baseline search is started past it. Sized so it never bites on a normal day - the last of six
probes starts around 168s - so the spread is not quietly reduced for everyone. On a slow day
probing stops, the cross-section narrows, and confidence drops to match, which the verdict layer
already reports honestly. routeBaselineStoppedEarly says so on the summary row.
-
Memory default drops 1024MB -> 256MB (min 128, max 2048). Two live runs peaked at 57MB
against a 1GB allocation while averaging 1.33% CPU: the run is bound by waiting on upstream
sub-runs, not by compute. Platform cost per run falls from $0.010 to about $0.0025. On a
cold-start run that moves our profit from $0.002 to $0.0095, because at 1GB the compute cost was
eating five sixths of what we keep. Buyers can still raise it; the ceiling is unchanged.
-
Recorded for the monetization setup, not a code change: the apify-actor-start synthetic event
is charged by Apify automatically and MUST NOT be charged from Actor code, so this Actor keeps
its own actor-start custom event and the synthetic one is removed in Console. The synthetic
apify-default-dataset-item event is removed too: it bills per row written to the default
dataset, and one watched itinerary writes eight rows (one observation, six route samples, one
summary), which would charge a buyer eight times for one itinerary and contradict the pricing
promise in the README.
Listing honesty. The Store description, the README and the fare-observation event description all
claimed something the measurement does not support, on a live listing.
- The claim "run one is not wasted: it prices a spread of nearby dates and tells you where yours
sits" was true about a third of the time. Measured 2026-09-06 across 34 controlled searches,
memo23/google-flights-scraper returns an empty dataset on ~39% of searches. A one-date first run
therefore yields a route verdict 34% of the time, a price with basis: none 27%, and no price at
all 39%.
- All three surfaces now state the measured rate with its date, as a figure, and promise a
labelled outcome rather than a verdict. Every row names its
basis and confidence or says it
has neither. The figures move with the upstream; the guarantee that the row tells you which case
you got does not. Improving the success rate edits one number rather than rewriting the claim.
medium confidence needs all six baseline dates to price, which is a ~3% event at the measured
upstream rate. The README says so instead of implying it is normal.
- The
fare-observation description no longer implies a verdict is always produced: it charges for
the observation, which is the thing that accumulates, and says the verdict may be BASELINE or
INSUFFICIENT_HISTORY.
Respects the buyer's own spending limit, which this Actor previously ignored entirely.
Actor.charge() returns a ChargeResult and it is now read rather than discarded.
ACTOR_MAX_TOTAL_CHARGE_USD, event_charge_limit_reached and chargeable_within_limit were
referenced nowhere in the source.
- The work is trimmed to what the limit can pay for BEFORE any upstream search starts. This is
the whole point: the expensive part of a run is not our compute, it is the sub-runs billed to the
BUYER's account, and their charge limit does not govern those. Previously a buyer with a low
limit would fund seven upstream searches, then be unable to be charged or delivered the rows
those searches paid for - paying the supplier, receiving nothing, and reasonably blaming us.
- If the limit is reached mid-run, it stops cleanly: everything charged has already been
pushed (rows are pushed before they are charged), no further sub-runs are started, and the
remaining itineraries get reason rows.
- The message names the buyer's own limit and never blames the upstream. It states the limit,
how many observations it allows, that the searches beyond it were not started so nothing was
spent on their account, and the one setting to change.
- Unknown is not zero: an unpriced Actor, a local run or an SDK that reports no budget leaves the
run unrestricted. Refusing to work because a limit could not be read would be a worse failure
than ignoring it.
- Paired Console setting, made the same day:
minimalMaxTotalChargeUsd set to $0.015, covering the
start event plus one fare observation, so the limit cannot be set below one useful run.
Switches the default upstream on measurement, and makes upstreamActor genuinely pluggable.
- Default upstream is now
automation-lab/google-flights-scraper. Measured against
memo23/google-flights-scraper, which had been the default: 30 of 30 searches returned results
versus memo23's 61%, at roughly a quarter of the price per search. Effective cost per result
that exists falls from about $0.041 to $0.0096.
- The switch was gated on a correctness question, not a coverage one. If two suppliers reported
different minimums for the same query, pre- and post-switch observations in a buyer's series
would not be comparable and the accumulated asset would be silently corrupted. Measured across
four routes: 6 comparisons where both suppliers answered, 6 exact agreements, 0 disagreements.
No history marker is needed.
- New
adapters.py. Upstreams disagree on field names (departDate/maxItems versus
departureDate/maxResults) and on which filters they support. An unrecognised upstream keeps
the historical field shape, so naming an unfamiliar scraper behaves exactly as before.
- A filter that the upstream cannot apply is never silently dropped.
maxStops is enforced by
this Actor when the upstream cannot, including rejecting rows whose stop count is unreadable -
guessing would put a possibly-violating fare into a series whose key promises otherwise.
airlines is refused outright at input validation when the chosen upstream cannot filter by
carrier, because carrier arrives as a display name while the input takes IATA codes.
Re-measures the listing figures against the new upstream. The switch understated the product, and
understating is a cost too.
- README figures re-measured end to end on the live Actor: 58 searches with 0 empty, and four
first runs on four fresh routes each producing a price, a route verdict, and
medium confidence
with all six baseline dates priced. The previous figures (34% route verdict, 27% basis: none,
39% no price) were correct for the old upstream's 61% success rate and are now wrong low.
- The older figure is kept in context rather than deleted, because it is the reason every row is
labelled: the numbers move with the upstream, the labelling does not.
- The Store description needed no change, which was the point of writing it without a rate in
it. It promises a labelled outcome, not a verdict, so a change in reliability does not falsify
it.
- The event descriptions needed no change either.
fare-observation says the verdict "may be"
BASELINE or INSUFFICIENT_HISTORY, which stays true and is simply rarer now; nothing in either
event description depended on the old success rate.