A Price Watcher That Cannot Buy, and the Buyer Beside It

Two Python tools, one deliberate line between them.

A twenty-second reel: a ticket page showing one price opens to reveal the nineteen in its payload, the watcher fires a new-lowest alert to Telegram, a vertical line divides price-watcher from ticket-autobuy with the one config key that crosses it, the buyer walks a checkout to a completed order, and the counts settle at 424 tests, 7 nights and 1 real order

Built a price watcher for arbitrary websites and, beside it, a tool that reserves a listing when the price drops and hands a human the code to pay. The watcher is standard-library only and structurally cannot buy; the buyer holds the browser and the risk. They talk through one key in a config file that the watcher parses and ignores. It ran unattended against a live third-party site for eleven days, and the part worth reading is the three failures it hid from me while reporting success.

Role
Sole author and operator — architecture, the adapter contract, the alert-rule semantics, the headless checkout automation, both test suites, and the failure analysis that followed the system into production
Timeframe
Sept 2026 – Sept 2026
Stack
PythonOpen Source
Evidence
  • Source
  • Product stack

Read the watcherSource code

I wanted one ticket. What I built instead was a pair of tools that hold a line between watching a market and acting on it — and then spent eleven days learning, from my own running system, how many ways a program can fail while telling you it is fine.

A personal project with no client and no deadline but a real one: a seven-night concert whose secondary market moved every minute. The generic price trackers do not cover a site like this, because they only integrate with marketplaces they have deals with. So the question was never "can I scrape it" — it was what shape a thing should take when it has to run unattended, every minute, against a site that owes me nothing.

The reconnaissance killed three plans before any code

The first plan was to intercept the page's data request. There is no such request — the site is a Next.js front end over a Bubble backend, and every price is server-rendered into the first response, reachable by a bare curl with no browser and no cookies. The second plan assumed a page like that carries no structured data; it carries full schema.org Event JSON-LD. The third plan was to skip writing anything and point an existing tool at it.

That third one is the one worth telling. changedetection.io has a restock mode, and its price extractor is a JSONPath expression matching a field named exactly `price`. This page publishes `lowPrice` and `highPrice`. I verified it by running the real expression against the real payload with a control leg alongside it — a synthetic object with a plain `price` field returned a value, and the real page returned nothing. Because availability still resolves, the tool never raises an extraction error. It produces a watch that looks healthy and silently ignores every price threshold you set on it, and the only way you would ever find out is by never receiving an alert.

The line is the design

price-watcher keeps two invariants. It depends on nothing outside the Python standard library — no virtualenv, no install, nothing to keep current — and it never buys: no checkout, no stored card, no login, no queue automation. Those are not v1 limitations waiting to be lifted. They are what make it safe to leave running every minute for free, because a process with no session and no credential has nothing to leak and nothing to spend.

Everything the watcher refuses lives in the second tool. ticket-autobuy holds the browser, the authenticated session and the risk. The two communicate through exactly one thing: a `buy` block inside a target file, which the watcher loads, validates and then ignores. Nothing else crosses. The buyer reads the price history the watcher just wrote, so checking the market costs the site zero additional requests, and the network is touched only at the moment of a purchase.

  • It reserves; it never pays. The roughly ten-minute hold on an unpaid Pix is the human gate, measured on a real order rather than assumed.
  • It never logs in by itself. A session is established by a person in a real browser, because automating that would mean storing a password — and a scripted login is the least ordinary-looking thing this tool could do at the moment it most wants to look ordinary.
  • Every alert rule is edge-triggered: it fires on a transition, never on a state, so a job running every sixty seconds cannot re-alert forever and train you to ignore it.

Three failures that reported success

All three were in code I wrote, all three were found while the system was live, and none of them looked like a failure from outside.

A single outlier killed an alert rule permanently

One listing appeared at R$ 66,00 on a night trading around R$ 220,00, stayed for a single poll, and vanished. It took the all-time-low record — and the all-time-low rule has no expiry, so the rule was now permanently dead on the night it had been built for, and the rolling-window rule was muted alongside it. Both rules were armed. Both looked healthy. Neither could ever have spoken again, and nothing anywhere would have said so.

The fix was an anomaly floor doing two jobs from one number: alert immediately on anything below it, and exclude anything below it from every baseline. The part that matters is that the exclusion is applied when history is read, not when it is written. That means arming the floor repairs a history that is already poisoned, and the log itself stays a faithful record of what the site actually served — a history that quietly omits the bad row cannot be audited. Replayed over roughly 1,100 recorded runs across seven targets, the rule fires exactly once: on the real R$ 66,00.

A fail-closed disarm stayed silent for twenty-five hours

The buyer hit an ambiguous state, did the correct thing — refused to act and disarmed the night — and then said nothing about it for twenty-five hours. Its only trace was one line on stderr. The log file gained zero bytes across roughly 1,500 scheduled runs while the session keepalive reported healthy every twenty minutes, and sixty-seven polls inside the buying window sat at or under the ceiling, including four consecutive minutes at R$ 220,00 with 178 available.

Worse, the disarm was wrong, and the tool had the evidence to know it. The routine that reads the orders page returned an empty result for four different causes, one of which was "the page rendered and there are no pending orders" — a case its own source comments as meaning no order exists. Forty-five seconds of successful checking was being graded as ignorance. The repair was to make silence impossible: a closed night now announces itself, an unresolved one nags on a throttle, and refusing to act requires two independent signals rather than one ambiguous empty value. A test had been asserting the silence.

A timeout that could never fire where firing was allowed

A sixty-second budget was supposed to abort a run that took too long. Measured, the work it was meant to bound took about thirty seconds, and the remaining forty-seven sat entirely after the point of no return — after the click that creates an order. So the budget could only ever have triggered where aborting was already impossible, and nothing but a print statement ever read it. It was replaced by a forty-five-second budget enforced on the segment before the click, and only that segment. In the same pass the measured walk went from 29.65 to roughly 17–19 seconds.

424
tests green, offline — 228 watcher, 196 buyer
7
nights watched live, on two cron cadences
5.49s
to extract the Pix code, after two 47-second failures
18.37s
checkout walk, against a 45-second budget

What it cost, and what it is worth

The honest summary is that the system worked and the operation did not. On two of the seven nights I bought tickets manually, at prices above R$ 300, because the automation never completed a purchase in time — and the night everything was finally aimed at came and went unattended. The end-to-end proof arrived on the last evening, on a target armed specifically to test it: a real order, a real Pix code in 5.49 seconds, deliberately left unpaid and verified lapsed thirteen minutes later. Then the whole thing was shut down — zero active scheduled jobs, all eight targets disarmed.

Evidence

424 tests green offline, seven nights read end to end, and one order completed against the live site — deliberately left unpaid, because the tool's whole design is that a human pays. It also missed the job I built it for: I ended up buying two tickets manually, at prices above R$ 300, because the automation never completed a purchase in time. Both numbers belong in the same paragraph. What I would actually put in front of a client is neither — it is the three sections above, where a system I wrote reported success while doing nothing, and instrumentation rather than guesswork is what found it.