Personal project16 min read
CactusPlay - Game shop run by a vetted contractor network

Tech stack
Contents
- Infrastructure and Capabilities Walkthrough
- Challenge
- Discovery
- Options Considered
- Decision
- Implementation
- Anyone with Turkish payment details could fill orders
- Legal groundwork for taking payments
- The shop’s own mail engine
- The form before it was final
- Code activation guide for buyers
- Sign-in through Russian providers
- Paperwork before payments
- Instant delivery for Yandex.Market
- When the work outgrew one pair of hands
- The Grabber in Action
- Outcome
- Support: LLM Crawler Load
- Support: the front page that was not ours
Services
- →Co-founding and fullstack engineering
- →Node.js scrapers for PlayStation Store and Xbox Store
- →WooCommerce storefront with custom PHP extensions
- →Differential price sync pipeline and alert system
- →Payment gateway and digital delivery integration
- →Yandex.Market integration: offer mapping, price sync and instant code delivery
- →Five custom WordPress plugins: sync engine, pricing, digital codes, social sign-in, campaigns
- →Go cover-styling service and self-hosted FreeScout support desk
Deliverables
- ✓Game shop with thousands of live SKUs across PS and Xbox
- ✓Automated daily price sync, with zero manual updates
- ✓Price-change alert system for promotional opportunity detection
- ✓CIS payment gateway integrations with digital key delivery
- ✓End-to-end price chain: PS Store, site and then Yandex.Market, no manual step
- ✓Fully attributed Yandex.Market product cards
Infrastructure and Capabilities Walkthrough
A walkthrough of the running system: the WooCommerce storefront, the catalogue the grabber keeps filled, the admin side where prices and orders are handled, and how the pieces fit together. Commentary in Russian.
Challenge
CIS gamers buying PlayStation and Xbox titles faced a fragmented market: official store prices varied significantly by region, gift card resellers were scattered across forums, and there was no single place to compare and buy. Co-founded CactusPlay to fill that gap: a shop for games and in-game items priced from official sources, with fulfilment opened up to a network of contractors rather than kept in-house.
The core technical challenge: PlayStation Store and Xbox Store don't expose public APIs. Keeping thousands of SKUs priced accurately in near-real-time required building a scraping and synchronisation layer from scratch.
Discovery
Initial research mapped the full buyer journey: discover a game, compare price across regions, buy the cheapest legitimate key, redeem. The pain was at the compare and buy steps. Sellers existed but were fragmented; buyers had no trust signals. A shop with consistent pricing data and a clean checkout could own that middle of the funnel - provided it could fill orders faster than the founders alone ever could.
Options Considered
- Manual price entry by admins: rejected. Doesn't scale; prices change daily, sometimes hourly during sales.
- Third-party price aggregation API, none existed for CIS PlayStation/Xbox at the time.
- Custom Node.js scrapers + WordPress/WooCommerce storefront: chosen. Scrapers give real data ownership; WooCommerce gives a battle-tested commerce layer without building checkout from scratch.
Decision
Node.js scrapers running on a cron schedule hit PlayStation Store and Xbox Store endpoints, parse the response, and push price/availability updates to WooCommerce via its REST API. WordPress + WooCommerce handles the storefront, cart, and payment gateway integrations. Custom PHP extensions handle the contractor onboarding flows that WooCommerce did not support out of the box.

cactusplay.ru, scrollable snapshot of the live storefront
Implementation
Built reverse-engineered scrapers for both storefronts, handling session management, region switching, and anti-bot measures. Set up a differential update system: only changed prices trigger WooCommerce updates, keeping API load low. A price-change alert system notifies admins when a product drops below a threshold, enabling promotional timing decisions.
Integrated payment gateways supporting CIS payment methods. Custom PHP WooCommerce extensions handled digital delivery (key codes) and contractor payout logic. Deployed on a VPS with automated deployment via Git hooks.
Anyone with Turkish payment details could fill orders
This is the part that made the shop work at all. Our products are activated onto the buyer's account, which means somebody has to actually buy the title in the cheap region and put it there - and that somebody needs working Turkish payment details, which most people in the CIS did not have. Keeping fulfilment in-house capped the shop at whatever the founders could process in a day.
So the CRM was opened up. Any contractor could apply, pass a check, and from then on take orders from the queue, fulfil them and earn on each one. That turned the ceiling from a headcount problem into a throughput problem: the more vetted contractors on the CRM, the more orders the shop could absorb, without a single new hire on the payroll. Onboarding flows and payout logic are the custom WooCommerce extensions above - they exist because this model does.
Legal groundwork for taking payments
Accepting Russian bank cards was a paperwork problem before it was a plugin problem. No provider signs up a shop that has no legal shape, so the shape came first: the full document set (public offer, privacy policy, consent to personal data processing, refund and delivery terms), registration in the state register of personal data operators, and registered self-employed status to invoice against.
Only then did the payment stack go in: acquiring through Tochka bank's WooCommerce plugin, with LifePay CloudPrint beside it to issue fiscal receipts, which the acquiring plugin did not do on its own. What the buyer sees is the part that matters: paying straight from a Russian bank card, on the site, and getting the receipt the law requires.
The system behind the storefront
The shop is one face of a larger system. A Node.js grabber walks the official PlayStation Store, and the Xbox Store on the same abstraction, pulling 20,000+ positions every hour. It iterates games, DLC and services separately, renames categories to the shop's own taxonomy, and merges store tags, so the catalogue arrives structured rather than as a flat dump.
Five custom WordPress plugins carry the rest:
- slave-api. The synchronisation engine. It owns the platform abstraction (one interface over the PlayStation and Xbox APIs), the product handlers that change visibility or retire SKUs, CLI commands for full and partial syncs, and versioned upgrade scenarios so a catalogue rule can be rolled out across a live shop without a manual pass.
- product-manager. The storefront side of pricing. It queries the PlayStation Store for live prices across currencies, picks one through a pluggable price strategy (the shipped one takes the lowest legitimate regional price), caches the result in transients so a product page never waits on an upstream call, and injects the custom fields the flow needs into the product form, cart, checkout and order.
- product-code-manager. The digital delivery half. It generates and stores product codes, resolves and activates them, and exposes a JWT-authenticated API so an order placed anywhere in the system can be fulfilled without a person in the loop. Order handlers react to creation and status changes, which is what makes a code arrive immediately after a digital purchase instead of after a manual dispatch.
- aw-auth, sign-in. It puts VK, Yandex, Steam and Discord on the WordPress and WooCommerce login forms behind one provider contract, so each new network is a client, a service and a settings screen rather than a fork of the flow. Steam is verified against OpenID 2.0 signatures, the rest through an OAuth code exchange; a returning visitor is matched to their existing account and a new one is created only when no match exists. Because a callback can arrive twice, each code is claimed in a lock table with a hit counter. The second request waits for the first and follows its redirect instead of minting a duplicate account. Steam hands over no email address, so those users land on a details form for email and phone before checkout. Login is email-only by policy, addresses must sit on a .ru domain to satisfy Russian data residency rules, and the private page cache is purged on login so nobody meets a stale signed-out page.
- mail-manager. The shop's own campaign engine, so customer mail did not depend on an outside provider. Audiences are categories, campaigns attach to categories, and the pending queue is derived by join: a recipient who already has a delivery row for a campaign is never picked again, so an interrupted run resumes exactly where it stopped. A cron tick sends a fixed batch with a pause between messages, keeping the shop inside the SMTP rate limit instead of firing a burst that gets the domain flagged. Sending starts disabled after activation and an interceptor logs messages instead of delivering them, so a copy of the production database cannot email real customers; the admin table reports pending, sent and failed per campaign. Open and click columns and a tracking endpoint are scaffolded, delivery is the part that shipped.
Yandex.Market: one price chain, end to end
The marketplace integration closes the loop. Offers are mapped to Yandex.Market's schema through a dedicated mapper layer, with per-store configuration, and a price service keeps them current. The chain runs without a human at any step: a price changes in the official PlayStation Store, the grabber picks it up, the site updates and then the Yandex.Market offer updates. Codes are shipped fast enough that a digital purchase on the marketplace is fulfilled in the same session, and the product cards carry every attribute Yandex.Market accepts rather than the minimum required to publish.
One number, the whole catalogue repriced
Prices still needed a hand on the lever now and then: the rate moves, a partner changes terms, the margin has to be nudged. Rather than touching products, the multiplier over the base price sits as its own row in the database, RUB_MULTIPLIER with the p2p multiplier beside it. Change the value and the next sync rebuilds every price in the catalogue from it, on the site and in the Yandex.Market feed alike.

The price multipliers, one row each
A logo bought at a fair
The shop mark is not a drawing of a cactus, it is a particular one. I bought the pendant at a street fair in Recoleta, Buenos Aires, photographed it, shot it against a green screen and traced the cut-out into the mark that now sits on the site, on the covers and at the top of every marketplace card.

The pendant from Recoleta

Cut out in Photoshop
Cover styling service, built, not shipped
A separate Go service composed marketplace-ready covers: original art, a blurred bleed to fill the required aspect, a shop-branded header and edition badges. It was finished and produced the result below, but the integration into the catalogue pipeline was never completed. The covers went out unstyled.

The badges themselves were drawn for the marketplace listing, not borrowed from it. Each one reads a field of the product metadata and turns it into a label the buyer understands at a glance: the account region with its flag, the edition, the interface language, and the shop mark on top. The service then picked the set that matched the item and composed them over the cover.

Badge set driven by the product metadata
Category covers, generated as one set
The catalogue carried 33 categories and not one of them had artwork. Rather than commission them one at a time, the whole set was generated through Kie AI against a single prompt style keyed to the site: neon line art, green on near-black, one subject per tile. Installing them was a script rather than 33 trips through the media library - WP-CLI imports each file and binds it to its term as the category thumbnail, run in one pass inside the LiteSpeed container.

































All 33 category covers, one generated set
Support desk
Customer support runs on a self-hosted FreeScout at support.cactusplay.ru, taking web chat and Telegram into one inbox, with the customer's recent orders pulled in beside the conversation so an agent answers without switching systems.

Mail on hardware I control
Outside mail providers charge per mailbox and per volume, and the shop needed room to grow in both. A mailcow instance on our own server took the whole thing over: mail.cactusgames.ru sends over SMTP and receives over IMAP for every address, the support desk and the campaign engine authenticate against it, and delivery no longer sits behind someone else's pricing tier or terms. Below, FreeScout wired to it: sending on 5587 with StartTLS, fetching INBOX on 993 over SSL.

Sending through mail.cactusgames.ru

Fetching over IMAP
The shop’s own mail engine
mail-manager, the campaign engine described above, in use. It runs on CactusPlay and on the consulting sites, which is what keeps customer mail off an outside provider.
The form before it was final
Both the activation form and the guide sit on top of product-code-manager, on CactusPlay and on 1gros alike. This is an earlier, intermediate version of the form, recorded to walk the partner through the approach and check it was the right one before building it out.
Another shot of that early form, this time with the preview in it: before confirming, the buyer sees which product the code will give them. The JS bugs on screen were fixed later.
Code activation guide for buyers
A demo video put on the site itself, walking the buyer through activating their code. It runs on CactusPlay and on the 1gros consulting build. Onboarding people this way made the process clear enough that they stopped filing complaints with Yandex over it.
The site had a problem: after buying on Yandex.Market, users landed on the game activation form and got lost there, not understanding how to activate the game. So I set aside time, wrote a script and recorded this video. Buyers saw it right in the activation form, and it helped them not get lost, figure things out and finish the activation.
Sign-in through Russian providers
The aw-auth module, shipped separately and reused on CactusPlay and on the consulting sites. It covers OAuth sign-in through providers nobody had ready-made solutions for at the time - Yandex was, as far as I know, the first one available - plus VK and Discord. Built for the Russian market, with the data residency rules that come with it.

Yandex ID, the first provider wired in
Paperwork before payments
Selling digital goods to Russian buyers is a legal question before it is a technical one. The shop went through the whole document set rather than a template licence pasted into a footer: the licence agreement, the public offer, the refund policy, the terms covering how an activation reaches an account. The business was also registered as a personal data operator with the supervisory authority, with the storage and processing arranged to match what that registration commits you to. Ahead of that came a round of questions to an accountant, on how a resale of a foreign digital title is declared and how selling through a marketplace differs from selling through your own site.
With the paperwork in order, acquiring came within reach. A provider does not simply switch it on: the shop went through moderation first, and passing it is what the whole document set had been assembled for. Once it cleared, the current account went to Tochka, whose WooCommerce plugin takes the payment on the site, and LifePay cloudprint issues the fiscal receipts. Buyers in Russia pay in roubles by MIR, Visa or Mastercard, or through SBP, without leaving the checkout.

Paying in roubles by card or SBP
The same setup carried over to the partner shops. Below, the 1gros checkout: the subtotal in roubles, the card and SBP marks, and the line naming Tochka as the platform behind the payments.

Checkout on a partner shop
Instant delivery for Yandex.Market
Listing on the Yandex.Market marketplace came with a hard constraint: once someone buys, there are 15 minutes to deliver the digital goods. Our products are activated onto an account rather than handed over as a bare key, and that does not fit into 15 minutes.
The way through was a custom key activation form. On purchase our API issues the buyer a key that is really a deferred obligation, which satisfies the Yandex delivery window; the activation onto the account is then carried out on our own platform, closing the order on the Yandex side.
The same chain runs on the partner shops. Below, the 1gros storefront on Yandex.Market: the cards were built by the catalogue automation rather than filled in by hand, and every sale issues its own activation key, which the buyer then redeems on the site.

1gros on Yandex.Market, cards built by the automation
When the work outgrew one pair of hands
Not every module was worth building myself. To keep the pace up, self-contained pieces went out to freelance marketplaces like Kwork, which only works if the spec is tight enough to hand over without a conversation attached. Below is one of those specs: a FreeScout plugin that pulls Yandex.Market order chats into the same inbox as web chat and Telegram, with the endpoint for each operation named, lazy loading for long threads, attachments handled, and chat and arbitration messages marked apart.

Spec for one outsourced module
Order volume also outran what the existing crew could absorb, so stretches of the project were spent on recruiting rather than on code: writing the postings, screening, trialling people on real tickets. Plenty of it ended the way it does, with a good candidate turning the work down for reasons that have nothing to do with you.

A candidate stepping away
And when the hands were simply not there, the answer was to take the operations myself: answering buyers in the order chats, chasing the ones that stalled, fulfilling orders by hand. There was a lot of it. The inbox below is a normal afternoon, four Yandex.Market notifications in two hours on one shop alone, against a running count of unread mail in the thousands.

An order chat, worked by hand

A normal afternoon in the inbox
Coordination ran through service chats rather than a task board. Each shop had one, and a bot posted into it the moment the CRM registered an order - number, shop, status, timestamp - so whoever was free picked it up instead of everyone refreshing the admin panel.

The service chat, with the CRM bot posting new orders
Consulting for other shops
The grabber and the solutions around it went out as consulting too. I applied them on 1gros.ru, ps-store.ru, playstaionplus.ru and xbox-store.ru: syncing the catalogue database through my own parser, plugging in third-party shops, tuning their WordPress, and keeping the whole thing serviced. The video shows one of those sites, 1gros.
Front-page banners for the partner shops
The work on those sites was not only plumbing. Their homepages needed something to lead with, so the PlayStation Plus subscription got its own banner, tier by tier, in two treatments the shops could pick between.

Dark treatment

Gradient treatment
Serviced from wherever I was
Those shops sell around the clock, so the upkeep did not wait for a desk. A fair share of it happened over SSH from a phone: editing the sync crontab on the 1gros VPS, checking the databases behind playstationplus.ru and ps-store.ru, watching the load on the Xbox API box. Nothing elegant about it, but a price sync that stalls at midnight gets fixed at midnight.

Sync crontab on the 1gros VPS

Databases of the served shops

Load on the Xbox API box
The Grabber in Action
Grabbing the static product listings off the PlayStation site, the raw material the catalogue is built from.
The other half of the same job: setting the price coefficient that turns a store price into the price the shop sells at.
Outcome
The grabber sustains 20,000+ positions an hour, and price changes reach both the shop and the Yandex.Market listing without anyone touching them. The platform handled real transactions from launch, and the scraping infrastructure survived multiple PlayStation and Xbox Store front-end redesigns through targeted selector updates.
Support: LLM Crawler Load
During ongoing technical support the site showed abnormally high load that did not match the real traffic. The logs traced it to crawlers from the popular LLM vendors, ClaudeBot among them, working through the catalogue. The configuration was adjusted and their agents were added to the exclusion rules: they brought no visitors and no orders, only load.
Support: the front page that was not ours
The hosting also came under attack. One of the served shops came up one day with its front page replaced outright, serving a listing for illegal drugs instead of games. It was sorted out the same day. Some part of me was flattered that our little shop drew that much attention.
Related
Projects

Aurora.Luxe, Technical Expertise and Specifications for a Silk Clothing Store
Before: another team developed the store, and when the 1C exchange failed nobody could see why or check the work precisely. After: technical expertise and acceptance on the owner's side, and specifications such as the logging system that make new work checkable point by point.

CasaLista - Home Services Marketplace in Buenos Aires
Before: a services catalogue that had to be retyped once per language, so moderators either duplicated work or shipped half-translated listings. After: one upload that lands the item in every locale at once, plus a standalone landing page for a single service in three languages.

Gazbet, Aerated Concrete Company Store
Before: no website at all, every enquiry started with a phone call and a manual quote. After: a product catalogue with a calculator that turns the buyer’s own parameters into a concrete offer, so more visits end in a sale.
Posts
Analytics counters do not belong in theme files
A tracking snippet pasted into a template disappears with the next theme update, competes with the page for the browser, and double-counts when someone pastes it twice. All three have the same fix.
Make delivery a consequence of payment, not a task someone performs
Digital goods delivered by hand are limited by working hours and protected only by attention. Moving delivery onto order events changes what the system guarantees, and the API is what stops a second channel becoming a second implementation.
Derive the queue, do not store it: resumable email campaigns
A bulk email run that crashes halfway is either restarted from the beginning, mailing everyone twice, or tracked with a cursor that goes stale. There is a third option: compute who still needs the message instead of recording it.
Pricing a catalogue against a store you do not control
Reselling means your prices depend on someone else changing theirs. Fetching on page load takes your shop down with theirs; a nightly export goes stale. The middle path is a cache with a pricing strategy behind it.
A support widget that already knows who it is talking to
Pasting a chat widget into a theme template works until the theme updates, the settings need changing, or the widget adds a database query to every page. Three small decisions fix all of it.
Taking payment through a gateway that belongs to another site
When the payment gateway is contracted to a different site, the buyer leaves and you have to learn the outcome somehow. The two ways this integration usually fails are an unauthenticated callback and two order records that drift apart.
Open for contract collaboration
I am available for contract-based collaboration. If you have an interesting project idea, schedule a call via Calendly.
Schedule a 30-min call