← Back to all projects

Personal project16 min read

CactusPlay - Game shop run by a vetted contractor network

CactusPlay - Game shop run by a vetted contractor network

Tech stack

WordpressPHPNodeJSGolangWooCommerceMySQLE-commerceMarketplaceStartup

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

  1. Manual price entry by admins: rejected. Doesn't scale; prices change daily, sometimes hourly during sales.
  2. Third-party price aggregation API, none existed for CIS PlayStation/Xbox at the time.
  3. 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.

‹›
aA🔒cactusplay.ru/
⇧▢+
Главная

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.

Config table open in DataGrip on a laptop screen showing RUB_MULTIPLIER set to 1.5, the p2p multiplier at 1.1 and a proxy job config row

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 steel cactus pendant bought at a fair in Recoleta, Buenos Aires, hanging on its chain

The pendant from Recoleta

The same pendant shot against a green screen and being cut out in Photoshop

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.

Marketplace cover generated by the Go styling service, blurred bleed, shop header and edition badges

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 drawn in Photoshop for marketplace cards: shop wordmark with the cactus, region with the Argentine flag, Russian language with the Russian flag, digital edition with a download icon

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.

Category cover for Action, neon line icon in green on a near-black tileCategory cover for Adventure, neon line icon in green on a near-black tileCategory cover for Shooters, neon line icon in green on a near-black tileCategory cover for Horror, neon line icon in green on a near-black tileCategory cover for Simulators, neon line icon in green on a near-black tileCategory cover for Strategy, neon line icon in green on a near-black tileCategory cover for RPG, neon line icon in green on a near-black tileCategory cover for Casual, neon line icon in green on a near-black tileCategory cover for Arcade, neon line icon in green on a near-black tileCategory cover for Fighting, neon line icon in green on a near-black tileCategory cover for Family, neon line icon in green on a near-black tileCategory cover for Puzzles, neon line icon in green on a near-black tileCategory cover for Brain training, neon line icon in green on a near-black tileCategory cover for Sports, neon line icon in green on a near-black tileCategory cover for Music and rhythm, neon line icon in green on a near-black tileCategory cover for Party, neon line icon in green on a near-black tileCategory cover for Quizzes, neon line icon in green on a near-black tileCategory cover for Unique, neon line icon in green on a near-black tileCategory cover for Indie, neon line icon in green on a near-black tileCategory cover for For adults, neon line icon in green on a near-black tileCategory cover for Miscellaneous, neon line icon in green on a near-black tileCategory cover for Virtual reality, neon line icon in green on a near-black tileCategory cover for PS4 games, neon line icon in green on a near-black tileCategory cover for PS5 games, neon line icon in green on a near-black tileCategory cover for Add-ons, neon line icon in green on a near-black tileCategory cover for Sales, neon line icon in green on a near-black tileCategory cover for Deals, neon line icon in green on a near-black tileCategory cover for EA games, neon line icon in green on a near-black tileCategory cover for EA Play access, neon line icon in green on a near-black tileCategory cover for Subscription payment, neon line icon in green on a near-black tileCategory cover for Wallet top-up, neon line icon in green on a near-black tileCategory cover for Picks for teens, neon line icon in green on a near-black tileCategory cover for Racing, neon line icon in green on a near-black tile

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.

FreeScout support desk with web chat and Telegram in one inbox and the customer's recent orders alongside

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.

FreeScout SMTP settings pointing at mail.cactusgames.ru on port 5587 with TLS and StartTLS

Sending through mail.cactusgames.ru

FreeScout mail fetching settings: IMAP against mail.cactusgames.ru on port 993 over SSL, INBOX folder, certificate validation on

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 login screen reached from the shop, offering sign-in by mail or phone number

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.

Payment method logos accepted at checkout: MIR, Mastercard, Visa and SBP

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 page of the 1gros shop on a phone: subtotal of 851 roubles, MIR, Mastercard, Visa and SBP marks, and a note that Tochka provides the payment platform

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.

The 1gros shop page on Yandex.Market with automatically built PS4 and PS5 game cards, prices and same-day email delivery

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.

Specification page for a FreeScout plugin integrating the Yandex.Market partner API: settings screen, chat list, message history with lazy loading, sending messages and files, creating a chat by order id

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.

Anonymised Telegram exchange with a candidate who declines further work, citing university and two other jobs next semester

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 in the Yandex.Market partner interface, buyer confirming the purchase and sending a screenshot of the order page; personal data and the activation code are masked

An order chat, worked by hand

iOS mail inbox filled with Yandex.Market notifications for the 1GROS.RU shop: new order, chat opened, new chat message, order cancelled, with 1,296 unread

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.

Telegram team chat with a bot message reading order #333066 created on ps-store.ru with status Paid; participant names and the pinned message are masked

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.

PlayStation Plus subscription banner on a dark background with the three tiers: Essential, Extra and Deluxe

Dark treatment

The same PlayStation Plus banner in a warm gradient treatment, with the heading naming Essential, Extra and Deluxe

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.

Crontab of the 1gros VPS open over SSH on a phone, showing the product sync timers for playstationplus.ru and ps-store.ru

Sync crontab on the 1gros VPS

MySQL prompt over SSH on a phone listing the databases of the served shops: 1gros.ru, playstationplus.ru, ps-store.ru, psnwallet.ru

Databases of the served shops

htop over SSH on a phone showing the Xbox API server at 1.3% CPU and 11 days of uptime

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.

🔒 The defaced page

This screenshot is closed. Enter the code to see it.

Projects

Posts

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