RUEN
Start a project

Engineering case

Order handling on trading platforms

Two bots for sellers on different platforms: incoming orders and messages, automatic delivery, relisting, holding the session. One processing engine, two entirely different transports.

payment send retries
0
transfers per transaction
4
transports to the platform
2

Two bots for sellers on different trading platforms: they keep the account online, catch orders, messages and reviews, deliver goods, put sold items back on sale and answer buyers. Everything is operated from a chat; the console is only needed to start the process.

One caveat first, without which the numbers read dishonestly: both are built on an open-source bot framework for the platform — the singleton core, the event loop, the handler registry, the configs. Our contribution is everything above that: the payment layer, the relisting logic, the handling of the platform’s refusals. The second bot is the same framework rewritten for a platform with an entirely different interface: requests to its own API instead of parsing pages.

The wallet library runs in a separate process because it dies uncatchably

Transfers go to a long-lived child process: the parent writes requests to it, reads answers on a separate thread and hands results to whoever is waiting.

The obvious fix is to wrap the transfer in exception handling and retry. It does not work: the wallet library has a native part, and it terminates the process with a signal that cannot be caught. One wallet failure would kill the entire bot — along with automatic delivery, the auto-responder and the panel, so the seller would lose everything over one failed operation.

“The money left” and “an answer arrived” are two different facts, and the first wins

The worker reports its phases separately. Once it confirms the transaction reached the network, the result counts as successful even if a timeout arrives afterwards — the transaction identifier is fetched later in a separate request.

The obvious logic, “no successful answer, refund it”, means paying twice here: the transfer is already irreversible, while a refund on the platform also hands the buyer back the order money. There is exactly one exception, handled separately: the transaction entered a block and the fee was charged, but the payment itself did not go out — then a refund is issued despite the confirmation.

A payment send is never retried — and the code says so outright

Every read request is wrapped in retries with growing pauses: network nodes often refuse. The send itself is never retried, with a comment next to it explaining why.

A repeat call signs a new packet with the same sequence number. If the first one already reached the network, both may go through — that is a double charge. The line between a safe retry and a dangerous one is not intuitive here, so it is written down rather than left to memory.

Restarting the worker is postponed while transfers are in flight

The process is meant to be refreshed after a number of operations or on a timer — the native part degrades. But if requests are still in flight at that moment, the refresh is deferred.

Killing the process when a transaction has left but its confirmation has not arrived is exactly the earlier scenario: the coins are gone, the bot considers the transfer failed and issues a refund. Routine hygiene must not create the very case the other rules defend against.

On saving, the platform can return both a link to the created listing and a list of remarks such as “the title was truncated”. The operation counts as successful when the link is there.

Previously an exception was raised on any remark, and the bot logged “error, skipping” for a listing that had already been created. On the next pass the same listing was created again — that is, strict error checking was producing duplicates.

The platform’s limit refusals are split into four distinct classes

The refusal text is classified: an account-wide limit, a limit on one section, temporary rate limiting, a demand to verify identity. Each gets its own reaction.

To the platform all four look like similar text, but the reactions are opposite: an account-wide limit means synchronisation must stop, because trying other sections is pointless, while a section limit means the opposite — move on to the next one.

Read position is synthesised from time, because the second platform has no ordering

Message identifiers there are random strings. A sortable key is assembled from the message time and a stable hash of the identifier, and the “read up to here” mark is kept against it.

Without a numeric order there is no way to say “this message is newer than the one handled”. Keeping a set of seen identifiers means growing it forever and still being unable to sort.

The read mark only moves forward — that is a cure for an endless loop

The new value is taken as the maximum of what was found and what is already known, rather than assigned directly.

The platform periodically returns a truncated conversation history. The maximum of such a page turns out older than what was already handled, and with direct assignment the bot re-sent notifications about old messages — around and around.

A deal’s status change is reported only when it moves forward

Statuses carry ranks, and an event is emitted only if the rank increased; a terminal status is not demoted.

For the first minutes after a change the platform flickers because of eventual consistency: the deal list returns the new state, then the old one again. A naive “the status changed” comparison turns this into a stream of false notifications for the seller.

Try to restore the original first; make a copy only if the platform refuses

After a sale the bot first tries to publish the same listing. A copy is created only when the platform refuses.

A copy is simpler and always works, which makes it tempting. But it loses the views, the position in the listing and the item’s history, and clutters the seller’s profile with duplicates. The harder path was chosen because the cheap one destroys the very thing it is all for.

A pass has a spending cap

A separate pass picks up expired listings, which usually have no free publication option at all. The pass has a cap on what it may spend.

A shop can hold several hundred expired listings. A pass without a cap would drain the seller’s entire balance in one go — silently, at night, on a schedule.

Python · SQLite · GraphQL · BeautifulSoup · Telegram Bot API