Idea taken from TheNetsky/Microsoft-Rewards-Script, which builds search terms
from public feeds rather than a model. No code from it: that project is
GPL-3.0 and this one is MIT, so only the approach crosses over.
The LLM has exactly two call sites here, both producing a short string to type
into Bing. Everything the dependency costs, an Ollama account, cloud usage and
the provider work in #15, is paid for search strings. Three keyless sources
answer the same question:
Google Trends RSS queries people are actually typing right now
Wikipedia most-read topic seeds when trends is unavailable
Bing autosuggest expands a seed into related queries
Autosuggest is what makes the chaining work. Asking Bing what follows a term
returns queries Bing already expects, which is nearer to what the prompt in
llm_utils was reaching for than a model guessing unaided.
Selected with QUERY_SOURCE=trends. The default stays llm, so no existing setup
changes. stdlib only, no new dependencies.
Measured against the LLM on the same cards from a live account:
card llm trends
airport parking best rates airport parking reservations reserve airport parking best rates
checking vs savings compare checking vs savings accounts compare checking savings account options
cruise deals best cruise deals and destinations cruise deals destinations
Verified live with OLLAMA_HOST pointed at a dead port, so nothing could reach
a model: five queries generated from feeds and three typed into Bing, each
landing on a real results page.
Every source degrades to an empty list rather than raising, and both entry
points fall back, to the trimmed task description and to nouns.txt. A search
that does not happen costs points; a run that dies costs the rest of the day.
Fixes#26.
`requires-python` was `>=3.14`. On anything older poetry declines to create an
environment, and it says so on stderr while leaving stdout empty. The README
tells the user to run `iex (poetry env activate)`, `iex` only sees stdout, and
PowerShell reports
Invoke-Expression : Cannot bind argument to parameter 'Command' because it
is null.
which points at `iex` rather than at the install that never happened. The
first reply on the issue guessed the reporter was missing `iex`, which is a
built-in alias, so the error is actively misleading.
3.14 looks stricter than anything the code needs. Every file in src/ compiles
on 3.10, no 3.13 or 3.14 only syntax appears anywhere in the tree, and the
full task set was run end to end on 3.12.10 against a live account with all
six tasks completing. The dependency graph agrees: numpy is the highest floor
in the lock at >=3.12, so 3.12 is where the real constraint sits.
Verified after the change on 3.12.10: `poetry install` creates the
environment, `poetry env activate` emits a real activation command instead of
nothing, `iex (poetry env activate)` activates in PowerShell, and every
dependency plus every module under src/ imports.
The lock is regenerated rather than left stale, since `requires-python` feeds
its content hash. The only substantive change is a typing_extensions marker
for python_version < 3.13; the package set is unchanged at 203. Regenerated
with poetry 2.4.1 to match the version that wrote the existing file.
The README gains the version bump and a short note on the failure, so anyone
who hits it while running an older Python can recognise it.
searches_needed was computed once as (max - earned) // 5 and never re-checked. Two assumptions fail in practice: some markets award 3 points per search rather than 5, and the daily maximum itself is not stable, observed as 15, 30 and 60 on one account within a day with the counter resetting. The run therefore stopped around 18/30 and still reported success.
Search in rounds instead: measure, run a batch sized on the lower known rate, measure again, stop when the quota is full or a round gains nothing, and warn instead of claiming success when it is not filled.
Also give the ollama client a timeout and bound the empty-response retry, since both were unbounded and an unattended run hung for 14 minutes with 2.3 CPU-seconds. The bare while-not-response loop spins forever on empty responses.
scrollIntoView re-centered every element unconditionally and instantly, which is the page jump between the daily set and explore on bing. The pointer move now scrolls only when the element is actually outside the viewport, smoothly, and polls the rect until the async scroll settles before computing the path.
Visual search matches the full 'visual search streak' label. Plain 'visual search' can also hit a dashboard element that goes stale mid-interaction, and the guessed 'image search' needle is gone.
check_selectors.py walks every selector and prints what resolved, what is absent and what broke, along with browser, driver, page language and the earn section ids. Absent is a normal result for a task a variant does not ship. It completes no activities and claims nothing, so it is safe to run for a bug report.
complete_explore_on_bing_tasks now raises when the section is missing instead of returning quietly, which made complete_all_tasks print [OK] for a task that never ran.
The visible labels the lookups match on are collected in one Labels class. The selectors are market independent but still language dependent, and this makes that explicit and fixable in one place.
The pointer is moved to viewport coordinates, so an element below the fold produced a target outside the window and the driver raised MoveTargetOutOfBoundsException. Observed on the daily set opener during a scheduled run. Also clamps the bezier path to the viewport.
The daily set opener lives in the streaks section, not in a section of its own, so scoping the lookup to a dedicated section disabled the task on every layout. Match on 'daily set streak' instead, since 'daily set' also hits the level up entry.
Visual search keeps a positional fallback inside streaks because its label is not confirmed on layouts that ship it. Claim now prefers an exact label match so the 'Ready to claim' heading cannot win.
Absolute XPaths break outside en-US, where an extra exploreonbing section shifts every positional section index, so every task failed before it started.
Select by visible text, id suffix and visibility instead of position, take the visible copy of ids that are emitted twice for responsive layout, and raise NoSuchElementException for tasks a variant does not ship so the run skips them instead of aborting.