During the quarantine, I got to know a group of semi-pro Pokemon TCG online players and built a marketplace to serve their digital card needs.

To play at the semi-pro level in the Pokemon online TCG (PTCGO), it is imperative to have specific rare cards in your deck. These rares are almost exclusively found in booster packs - 10 card stratified samples of an expansion set. Packs can be obtained in the PTCGO by finishing daily challenges in-game (low yield) or by buying physical Pokemon booster packs.  

Code card included with physical Pokemon TCG products

Every physical Pokemon TCG product comes with a code card that you can use to redeem that same product online. For booster packs, the contents of the virtual item are not the same as the physical ones.

When surveyed, most of the players in the group would buy codes directly from www.potownstore.com. These would be codes resold from opened booster packs at a discount since buying booster packs outright would be too expensive.  

It actually gets fairly expensive to get the cards that you need to build a competitive deck. Each pack contains one guaranteed rare from the expansion's rare pool. Since competitive decks often requires multiple copies of the same card  (maximum 4 per deck) and decks are 60 cards, costs can easily rack up.

For example: Eternatus V is a very competitively useful card from the latest expansion with 72 possible rares. To complete a deck with 4 of these cards, it would take 4 x 72 = 288 packs on average. At market prices, buying the codes would cost $0.70 * 288 = $201.60 with no guarantee of success!

Meanwhile, a number of YouTubers that regularly do unboxing videos of physical booster packs. Many of them actually give away code cards during their unboxings to their viewers. Thousands of codes are given away daily.

There was an interesting arbitrage opportunity here. If I could build an automated tool that could automatically extract and redeem the codes in the video, I could set up an alternative market for the semi-pro players to acquire codes at a much cheaper cost.

The Tool

Mission: create a computer vision algorithm that automatically finds, downloads and scrapes YouTube videos for codes and redeem them into a player's account.

This project was done in python and bash and it turned out to be a great choice since there were many pre-made packages that manipulated video.

Search for new videos

Even though YouTube provides an API for searching videos, it's severely rate limited. That's why I used the youtube-search-python library. This would conduct a youtube search for "pokemon card opening" every 3 seconds and return the links of all the results.

The only changes I had to make to the package was to allow different search settings. The specific one I was after was the upload date filter.  

Because YouTube's search is not perfect + computer vision is computationally expensive, I implemented filters on the results to skip downloading irrelevant videos/videos with low likelihood of free codes.

dqwords = ['online', 'gameplay', 'collection', 'marvel', 'yu-gi-oh', 'yugioh', 'digimon', 'baseball', 'japan', 'korea', 'show', 'funko', 'stream', 'binder', 'made', 'fake', 'mtg', 'ptcg', 'review', 'tcgo', 'shadow', 'grade', 'parker']

Download Videos

Downloading the video instead of streaming it allows me to process it much faster than the maximum streaming speed (2x). To do this, I used the pafy library.

YouTube allows mp4 downloads of all their videos at 360p. Higher resolution videos usually also has their highest resolution available. All resolutions in between can only be streamed and not downloaded.

Testing with the processing algorithm showed that 360p was clear enough to recognize QR codes which was great because it saved time on the actual video processing (less pixels) and video download (less bytes).

Assuming that it's passed through the filters, I would pass the link into pafy and download the metadata. I would skip downloading the video if it:

  1. Has > 5 views (codes most likely already redeemed)
  2. Length >25 minutes (staking a lot of processing power for potentially 0 codes)
  3. Is a recorded stream (definitely already redeemed)

Because download is an IO operation, I put it on a separate thread to avoid blocking search with the multiprocessing module. Once a video is finished downloading, its name is added to a dequeue() to be processed. If the titles contains terms likely to yield codes (giveaway, code, etc.), it gets added to the front of the queue.  

Process video and extract QR codes

Processing is easy enough. OpenCV loads the image from its saved location on ram and then processes the frames one-by-one using multiple threads. The pyzbar library is a fast way to do this - on a 6 core machine, a clip of 5 minutes takes around 23 seconds to process every frame.

To save work, it was important not to process every video completely - some videos are long and have no codes. To avoid wasting time on them, I only processes one third of a video at a time. If the middle third of the video contains no codes, the algorithm abandons it and moves on to the next one. Otherwise, it continues to process the other two thirds. Videos are handled one at a time.

Submit Extracted Data to Pokemon.com

This was the trickiest part. The Pokemon company does not provide any sort of APIs to access account data or redeem codes so I had to reverse engineer all of it from the website. By inspecting network requests and replaying them, I was able to submit codes to an account without the web interface.

It ended up being really fun because there were so many measures in place to prevent exactly what I was trying to do:

  1. Rate Limit
    The maximum number of codes that you could redeem per day was set at 100. However, you could get around this limit by changing your ip address. I developed a tool that rotated vpn servers whenever this limit was hit - by the time one ip was used up, an older one would have its daily limit reset.
  2. Session Timeout
    Because pokemon uses the CAS protocol, all sessions expired at a set time without the ability to refresh or extend sessions. I wrote code to re-login and get new session cookies and tokens.
  3. Captcha
    Even though I had new session cookies, the site would not allow you to enter them without solving a reCaptcha. This + session timeout was quite a wrench but I cooked up a selenium script that mimicked human movement to beat it. As a fallback, it would use a plugin called captcha buster that would solve using audio cues. Because I had previously developed the vpn server switcher, I could ensure that the ip address would not be flagged

Setting Up The Market

After firing up an AWS instance, I began to test it on my own account. After just 1 day of continously running it, I was able to amass a sizeable amount of virtual packs. All of this was worth ~$110.  

With this success, I began to take orders from the community. My pricing was set at 50% off the current market price with the tradeoff being a delay for orders to be filled.

Before I began the project, I had communicated with a number of players who were interested and had set up a client base. When the tool was completed, I soft launched to these players for them to try it out. From there, customer acquisition happened through word-of-mouth as more people wanted in.

Response was overwhelming! Within the first 15 days after launch, I recorded more than $400 in revenue (the launch luckily coincided with the release of a new expansion). It actually got so popular that I had to temporarily shut the service.

The core weakness of something like this is that it doesn't really scale. I am facilitating access to a limited resource - one that is inconsistent in its yield. It's also very exposed to potential crackdowns from the Pokemon company for all the ways that I bypass their checks.

Still, this provides valuable savings for several players and helps them keep up their rankings without as much of a financial burden. In the future, I'm planning on periodically running the tool and only opening it to the existing client base.