Security

Hamster Kombat Automation: Building an Auto-Clicker Bot to Farm Coins

In this article, I’ll walk through how I explored automating the rapidly growing game Hamster Kombat. Not every tactic I tried was successful, but you may still find the methods useful in other contexts.

I won’t go into detail about the game itself—by now, even people without a mobile phone have probably heard of it. Let’s jump straight to the interesting part.

Objective

The game runs via a Telegram bot, and the goal is to grow passive income from the exchange. You can boost earnings by buying cards, each of which increases your income by a set amount. Funds to buy cards come from two sources: the passive income itself and tapping the big button in the center of the screen.

While playing, I quickly realized I wanted to automate two things: clicking the button and choosing the best cards to buy. Each card has two key attributes: how much it boosts your income and its price. Some cards give a small boost and cost a lot; others are a great deal. To make the analysis easier, I introduced a metric I call cost per income (you could also think of it as the price of the increment). For example, if a card costs 1,000,000 coins and adds 4,000 coins to your income, its cost per income is 1,000,000 / 4,000 = 250 coins. The lower this number, the more it makes sense to buy the card.

A card’s stats change after every purchase, so eyeballing it and picking the best price-per-increase option is nearly impossible. I started by building a spreadsheet, listing all the cards, and sorting them by unit return (price/gain). That worked to a point, but it still leaves you manually updating each card’s changed stats after every purchase. So, let’s automate it.

Right off the bat, the obvious way to interact with the game was to analyze what’s on the screen and emulate taps using Android Debug Bridge (ADB). Ideally, I’d do everything via HTTP requests with no phone involved at all, but that would mean analyzing the network traffic—and since the game runs only on a phone, inside Telegram, that sounded like a lot of work. So I decided to start with the more straightforward approach.

Automating with ADB

Simulating Screen Taps

I’ll assume everyone knows how to install ADB and talk to a phone with it, so I’ll skip the basics and get straight to it. Attempts to run the game on Windows failed: it somehow detects it’s not on a real phone and displays a warning. Fine—phone it is. Grab an Android device and fire up ADB. The “tap the button” problem is easily solved with a single command:

e:\Android\sdk\platform-tools\adb.exe shell input tap 540 1800

540 and 1800 are the screen coordinates of the point we want to tap. To avoid circling back to this later, let me mention the so‑called Morse input. In the game, you can get an extra million coins every day by entering a specific code using Morse code. A dot is a short tap; a dash is a long one. We already know how to do a short tap; a long tap is done with the following command:

e:\Android\sdk\platform-tools\adb.exe shell input swipe 500 1500 500 1500 555

The first four numbers are the coordinates of the start and end points, and 555 is the duration of the movement. In our case, the start and end are the same.

To cut down on manual work, you can write a Python script that takes a word as input, converts it into a sequence of dots and dashes, and feeds it into the game by simulating key presses. Below are the core functions you’ll need.

A function that simulates typing a period:

def tochka():
cmd = "adb.exe shell input tap 500 1500"
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, creationflags=0x08000000)
process.wait()

A function that simulates typing a dash:

def tire():
cmd = "adb.exe shell input swipe 500 1500 500 1500 500"
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, creationflags=0x08000000)
process.wait()

A function that parses a sequence of dots and dashes and sends it to the phone (a space denotes a three‑second pause used to separate letters):

def mz2tap(morze):
for simvol in morze:
match simvol:
case ".":
tochka()
case "-":
tire()
case " ":
time.sleep(3)

I won’t cover converting text to Morse code—that’s not what we’re here for. Example usage:

mz2tap(".--")
mz2tap(".")
mz2tap("-...")
mz2tap("...--")

With tap automation sorted out, it’s time to analyze the cards so we can pick the best one.

Recognizing cards

Working with the cards took some effort. I decided to tackle it head‑on: scroll through the cards and take screenshots, then crop out the excess and OCR what was left.

To start, I took a bunch of screenshots with a Python script. This code loops, takes a screenshot, and saves it to the cards_dir directory on the computer:

while True:
cmd = "adb.exe shell screencap -p /sdcard/screencap.png"
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, creationflags=0x08000000)
process.wait()
cmd = f"adb.exe pull /sdcard/screencap.png {os.path.join(cards_dir, datetime.datetime.now().strftime("%Y-%m-%d_%H_%M_%S_%f"))}.png"
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, creationflags=0x08000000)
process.wait()
print(cmd)

While the script was running, I manually opened each card on my phone. Once it’s finished, delete the duplicate screenshots:

hash_list = []
list_of_files = os.walk(cards_dir)
dubl_count = 0
for root, folders, files in list_of_files:
for file in files:
file_path = os.path.join(root, file)
Hash_file = hashlib.md5(open(file_path, 'rb').read()).hexdigest()
if Hash_file not in hash_list:
hash_list.append(Hash_file)
else:
dubl_count += 1
os.remove(file_path)
print(file_path)
if dubl_count > 0:
print(f"Deleted {dubl_count} duplicates")
else:
print("No duplicates")

We’ll use the pytesseract module for OCR. It’s very handy—you can extract text from a screenshot with a single line of code.

string = pytesseract.image_to_string(image, lang='eng')

However, a test run showed that the recognized text contained a lot of noise. Clearly, the image needs to be cropped so it includes only the text. I won’t include the entire cropping script to avoid cluttering the article with boilerplate; I’ll just note that I used the PIL module, mainly the Image.crop and Image.getpixel functions.

I experimented with a couple of screenshots to identify the boundaries of the region of interest and developed an algorithm that discards large areas whose color doesn’t match the background or the text. I then tested it across all screenshots to make sure everything was working correctly.

After pruning the noise, about 99% of the screenshots OCR’d correctly; I cleaned up the odd ones by hand and consolidated the results into a table. Picking cards did become much more convenient, but I soon realized it didn’t really help. After every purchase I still had to plug the phone into a computer, run the script, and verify the recognition. In practice, it’s faster to just update the table manually after each purchase. Unfortunately, in this form the experiment had to be deemed a failure.

Automating via API Requests

Analyzing Traffic

No way around it—let’s take a closer look at the game. First, we need to figure out what’s preventing the hamster from running on a computer. Let’s open Telegram Web in a browser, go to the bot, and try launching the game again. The hamster is insisting we switch to the phone.

Start message
Start message

Open the developer console; you can see the game runs in a separate iframe.

Developer console
Developer console

Let’s take a closer look at the iframe tag’s src parameter—you’ll see a very long query string. If you look carefully, there’s a parameter called tgWebAppPlatform with the value web. Try changing it to android and open the resulting URL in a new window.

It worked!
It worked!

Great, the game runs fine in the browser. Now we can start experimenting with requests. Here’s the approach: open the Network tab, clear out the old requests, perform an action in the game, and see which requests are sent to the server. For example, let’s try buying a card.

Purchase request
Purchase request

It’s easy to see that this sends a POST request to the following address:

https://api.hamsterkombatgame.io/clicker/buy-upgrade

Looking at the Payload tab, you can see that the request includes two parameters:

{upgradeId: "hamster_youtube_gold_button", timestamp: 1722086163455}

The upgradeId parameter looks a lot like the name of the card we bought, and the timestamp parameter, judging by its name and value, seems to be a Unix timestamp. Out of curiosity, let’s check the request’s response (the Response tab), and it’s a goldmine—tons of different game parameters, including the full stats for all cards:

{
"clickerUser": {
"id": "ХХХХХХХХХХХХ",
"totalCoins": 5389463.6559000015,
"balanceCoins": 4603871.6559000015,
"level": 6,
"availableTaps": 3500,
"lastSyncUpdate": 1722086163,
"exchangeId": "mexc",
...
"upgradesForBuy": [
{
"id": "ceo",
"name": "CEO",
"price": 725363,
"profitPerHour": 2789,
"condition": null,
"section": "PR&Team",
"level": 16,
"currentProfitPerHour": 2513,
"profitPerHourDelta": 276,
"isAvailable": true,
"isExpired": false
},
{
"id": "marketing",
"name": "Marketing",
"price": 8557,
"profitPerHour": 838,
"condition": null,
"section": "PR&Team",
"level": 9,
"currentProfitPerHour": 718,
"profitPerHourDelta": 120,
"isAvailable": true,
"isExpired": false
},
...
}
...
}

So it turns out that, first, we can buy cards via a request, and second, after each purchase we get a response with the updated attributes of all cards. Nice. And to think we were wrangling with screenshot OCR…

Experiments with the big round button showed that the server doesn’t receive a request for every single press. The game detects a streak of consecutive clicks, then sends the total number of presses and the remaining coin count in a POST request to the following URL:

https://api.hamsterkombatgame.io/clicker/tap

And passes the following parameters:

data = {
"count": 500, # Number of taps
"availableTaps": 0,
"timestamp": timestamp()
}

Just be careful when computing the count parameter: multiplying it by the coins per tap and subtracting that result from the total available coins should yield availableTaps.

Writing the script

Now that we’ve sorted out the requests required to interact with the server and their parameters, it’s time to do a bit of coding. Let’s write a Python script:

import requests
from time import time
def tap():
url = "https://api.hamsterkombatgame.io/clicker/tap"
data = {
"count": 500,
"availableTaps": 0,
"timestamp": int(time())
}
response = requests.post(url, json=data)
if response.status_code == 200:
try:
result = response.json()
return result
except ValueError as e:
print(f"{e}")
return None
print(f"Error: {response.status_code, response.text}")
return None
print(tap())

After running it, we get a 422 error in response. There are many possible reasons for this. A server might return it when the request is generally valid, but something about it doesn’t pass muster. Looking closely at the script, we realize it never identifies who we are. We told it we tapped 500 times, but how is the game supposed to know which user did that?

If you take a closer look at the original tap request in the browser’s DevTools, you’ll notice that the request headers include an Authorization header with the following value:

Bearer 32432432432432432446t...cfC1y523432432432419

Let’s try modifying the script: instead of the request line response = requests.post(...), write the following code:

key = '32432432432432432446t...cfC1y523432432432419'
headers: dict = {
'User-Agent': 'Mozilla/5.0 (Linux; Android 14; Nokia 3310; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/78.0.3904.108 Mobile Safari/537.36',
'Content-Type': 'application/json',
"Authorization": f"Bearer {key}",
}
response = requests.post(url, json=data, headers=headers)

Now the response returns JSON with the updated configuration, and the game UI reflects the changes as if we had actually tapped the specified number of times. The last step is to add a loop that, for example, periodically performs a single tap, receives the updated config in response (which indicates how many more taps are worth doing), and on the next pass sends an adjusted request accordingly.

I won’t include the full code here—consider it homework to write your own implementation. Generally, tapping stops making sense pretty quickly, because the income from taps becomes negligible compared to the passive income from buying cards. For example, my taps bring in about 3 coins per second, while my passive income is 1,500 coins per second, so tapping is hardly worth it.

Let’s try a better approach: fetch the list of product cards with their prices and convert it into a convenient format. While analyzing the requests, we found that besides the purchase request there’s also one that simply updates the item attributes—that’s the one we need:

https://api.hamsterkombatgame.io/clicker/upgrades-for-buy

You don’t need to pass any parameters except the token in the header, and in return it sends back a JSON with a bunch of fields. Our task is to extract the card parameters, compute the price increment for each card, and sort them from cheapest to most expensive. Here’s what I came up with:

def get_cards():
key = '32432432432432432446t...cfC1y523432432432419'
headers: dict = {
'User-Agent': 'Mozilla/5.0 (Linux; Android 14; Nokia 3310; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/78.0.3904.108 Mobile Safari/537.36',
'Content-Type': 'application/json',
"Authorization": f"Bearer {key}",
}
url = "https://api.hamsterkombatgame.io/clicker/upgrades-for-buy"
response = requests.post(url, headers=headers)
if response.status_code == 200:
try:
data = response.json()
cards = []
for task in data["upgradesForBuy"]:
if task["isAvailable"] == True and task["isExpired"] == False and task["profitPerHourDelta"] > 0:
section = task["section"]
name = task["name"]
price = task["price"]
profitPerHour = task["profitPerHourDelta"]
udel_cena = price / profitPerHour
cards.append(
[section, name, price, int(udel_cena)])
sorted_cards = sorted(cards, key=lambda x: x[3])
for card in sorted_cards:
print(f"{card[3]}:\t{card[2]}:\t{card[1]}:\t{card[0]}")
except ValueError as e:
print(f"{e}")

As a result, we end up with a list of cards sorted in descending order of “benefit”:

7: 551: HamsterBook: PR&Team
7: 606: X: PR&Team
7: 2000: Risk management team: PR&Team
7: 7757: Special Hamster Conference: Specials
7: 2327: Hamster YouTube Channel: Specials
7: 11000: Web3 academy launch: Specials
8: 2205: IT team: PR&Team
...
200: 1000000: TON + Hamster Kombat = Success: Specials
1000: 5000000: Call for BTC to rise: Specials
1000: 1000000: HamsterWatch for soulmate: Specials
2628: 725363: CEO: PR&Team
4000: 12000000: Business jet: Specials

QED, basically. From here, you can build a fully automated tapper bot that periodically reports button-press events to the server, checks the balance, and, when needed, purchases the most profitable cards.

Key takeaways

So, we’ve learned:

  • Use ADB to simulate taps
  • Perform OCR on screenshots to control the application
  • Analyze and tamper with network requests

In my case, the right place to start was with the accessible parts of the game’s code, and to use ADB and OCR only as a last resort if the code turned out to be heavily obfuscated or wrapped in complex protections. That said, it’s important to know these approaches, because in other situations you might run into exactly those scenarios.

Additionally, when building your automation script, set aside time to review the API’s usage limits—things like requests per second, how many accounts are allowed per IP, and so on—otherwise you risk having your account suspended.

When it comes to optimal strategies in Hamster Kombat, the question is what pays off more: snapping up any available cards as long as they increase your income, waiting and saving for the most cost-effective cards, or mapping out a sequence of purchases based on each card’s attributes.

All in all, games are meant to be fun—and I think we had a blast!

it? Share: