RUEN
Start a project

Engineering case

Listing prices in dollars

A small personal tool: the extension prints a dollar figure next to every rouble price using the central bank rate, and a bot watches for new listings matching saved searches.

dependencies in the extension
0
ways to find a price
2
proxies per request
3

Two independent things around one car marketplace. The extension prints a dollar figure next to every rouble price using the official rate — after the marketplace dropped dollar prices itself. The bot watches for new listings matching saved searches and sends a card with photos and specs.

The price on the page is found in two independent ways

The first walks text nodes, finds a rouble token and climbs up to the element whose text is exactly “number plus currency” — but no more than four ancestors. The second takes elements with classes like price or cost whose text is a bare number.

The obvious approach, one selector on the card’s class, survives until the marketplace’s next release: those classes are generated by its build. And an extension ships through store review — a fix reaches people days later. Two detectors guarantee nothing, but they break for different reasons: the first survives a full class rename, the second survives markup where the currency is not in the text at all.

The “no more than four ancestors” limit is not decoration either: without it the climb reaches the whole card’s container and the price sticks to an arbitrary spot.

A stale rate beats an error

If refreshing the rate fails, the old value is served; an error is raised only when there is no cache at all.

“Could not refresh, return an error” reads as more honest but makes the extension useless on any network hiccup: badges vanish from every price at once. Meanwhile the central bank rate changes once a working day, and yesterday’s figure differs by a fraction of a percent. To keep that from becoming a quiet lie, the rate’s date is shown in every badge’s tooltip and in the extension window, along with the time of the last refresh.

The page can fetch the rate itself, and that is not duplication

Normally a background process fetches the rate and stores it for everyone. But when there is no answer and no cache, the page makes the same request itself.

In one browser the network permission may not be granted: the extension is installed, the rate is missing — and the person simply sees no badges, with no explanation. A direct request from the page is usually blocked by browser policy, which is exactly why it was verified: the bank’s server allows it. It does not replace the main path; it switches on only when the main path is dead.

Listings come from the page’s own data, not from a browser in the background

The bot makes an ordinary request and pulls out the ready state object the marketplace itself renders the list from.

The two obvious options are worse. Parsing HTML with selectors means breaking on every markup release and then turning “from 1,250” back into a number. Running a headless browser means hundreds of megabytes on startup, while the service is capped at 600 MB for the whole process and lives on a cheap server. The state object is already typed: numbers as numbers, specs as fields. The trade-off is stated plainly — a dependency on an internal structure nobody promised to keep.

The first check of a search is silent

Right after a search is created the bot takes one page, marks everything as seen and exits without sending anything. Notifications start from the next run.

Otherwise a search for “under thirty thousand” dies the moment it is created: a dozen cards with photos immediately and a note saying “and three thousand more”. The tool exists for something else — showing what was not there a minute ago.

Paging stops on two conditions, and the second is about the send limit

The next page is fetched only if the tail holds no familiar listing and fewer fresh ones have accumulated than twice the send limit.

The first condition is obvious. On its own it is not enough: with a broad search, or after the bot has been down for a day, no page holds anything familiar, and paging runs to the cap — four requests to a marketplace that answers such behaviour with its own throttle, for data that will not be sent anyway. The margin is double rather than exact because some of it will be filtered out as paid promotion.

Listings flagged as paid promotion are never sent.

Deduplicating by id feels sufficient, but a promotion pushes a listing back to the top of the list. Without the filter the bot sees “something appeared at the top that was not there” and sends a month-old listing as news — again on every new payment.

A card that failed to send is not marked as seen

Sending runs with three attempts and different fallbacks: on a messenger throttle, a pause for exactly as long as it asked; on a photo error, the same thing as text; on broken formatting, without formatting. If every attempt fails, the loop breaks and the listing is not marked seen.

The order here is the reverse of the usual: confirmed delivery first, the mark afterwards. So anything unsent comes back on the next run. The fallbacks differ because the causes differ: a throttle is cured by waiting, a broken photo link only by dropping the photo, broken formatting only by dropping the formatting.

A proxy pool with growing cooldown — and the password stays out of the log

Up to three addresses are handed out per request in rotation; a failing one cools down for five, ten or fifteen minutes depending on the streak, and a success resets the counter. If all are banned, they are tried anyway. Every entry carries a short label of scheme, host and port — without the password, so it can be written to the log.

Each decision closes its own trap. A fixed cooldown lets a permanently dead address eat an attempt every five minutes forever. Without resetting the counter, one network glitch a month marks a working address as suspect for good. An empty candidate list is a guaranteed failure, while an address banned five minutes ago may already be alive: an attempt costs less than a refusal.

Manifest V3 · JavaScript · Node.js 18 · Telegraf · SQLite · undici · systemd