SkyCards, ground truth: reverse‑engineering a flight‑spotting game

ZacnyLos1 pts0 comments

SkyCards, ground truth: reverse‑engineering a flight‑spotting game | JonLuca's Blog<br>Sep 12, 2025<br>SkyCards, ground truth: reverse‑engineering a flight‑spotting game

TL;DR - I reverse‑engineered SkyCards, a game that turns live flight spotting into a collectible. This post walks through intercepting traffic, reading a React Native + Hermes bundle, finding a native request‑signing bridge, and wiring up a small client that talks to FlightRadar24's protobuf/gRPC endpoints to automate captures.

SkyCards in a nutshell

SkyCards takes the plane‑spotter vibe and turns it into a collection loop. The app shows live flights near you with a rarity score. You pick one, jump into a mini‑game, and try to "photograph" the aircraft as it crosses the frame. If roughly ≥90% of the plane is inside the shot, you capture it. Captures store tail number, model (e.g., "Boeing 787"), route, and a timestamp. Rarer flights give more XP. You have a camera with a limited number of shots that refresh over time (with optional in‑app purchases for more).

SkyCards - map view

Once you tap a plane, you see its card and can decide whether to try a capture.

SkyCards - plane card

If you try to capture it, you play a mini‑game: the plane flies across the screen and you snap the photo when it's fully in frame.

SkyCards - capture mini‑game

On success, you get a confirmation screen and the flight is added to your collection.

SkyCards - capture success

Digging into the app

I enjoy aviation and reverse engineering, so this felt like a good crossover project. I started by booting up Proxyman to see if I could intercept the app's traffic.

Luckily the app didn't do certificate pinning or TLS fingerprinting, so I was off to the races.

The flights endpoint returned binary (protobuf) data that I didn't have definitions for yet, while the capture‑related endpoint spoke JSON.

When you attempt a capture, two client actions pop out immediately:

Spend camera shot - decrements your shot inventory.

Register capture - sends the result (selected flight, "% in frame," and some metadata).

The two endpoints called when you go through a mini game

Notably, the captures endpoint was separate from the consume (shot‑spend) endpoint and didn't validate against a prior consume call. Practically, you could sinkhole the consume endpoint and never burn shots.

The mini‑game itself runs client‑side; "how much of the plane is in frame" ultimately becomes a number in the request body, seen below. coverage represents the percent of the plane in the shot, and cloudiness captures how cloudy the frame is (which affects score/XP).

"sub": "your-user-id",<br>"iat": 1757403992,<br>"data": {<br>"alt": 3000,<br>"speed": 0,<br>"reg": "SKY-CARDS",<br>"callsign": "ONBOARDING",<br>"associatedAirportId": 3147,<br>"flightId": 1,<br>"track": 45,<br>"icon": 0,<br>"status": 0,<br>"timestamp": 1757403964331,<br>"onGround": false,<br>"source": 0,<br>"model": "B738",<br>"xp": 1380,<br>"xpUserBonus": 1700,<br>"coverage": 100,<br>"cloudiness": 85,<br>"glow": false,<br>"coins": 1,<br>"newAirport": false

Registering the capture (signing)

The register endpoint was the interesting one: a single POST with a signed JSON payload.

HTTP payload (redacted)

The server wouldn't accept unsigned register requests, so it was time to fire up the decompiler.

Android reverse engineering: APKLab + JADX

I booted a rooted Android emulator, installed the app from the Play Store, and extracted the APK. Then I opened it in APKLab and let it infer Java sources from the DEX/smali files.

It turned out the app was built with React Native, which meant the interesting bits were likely in the JS bundle.

Architecture tour: React Native + Hermes (and how I read it)

The app is React Native using Hermes. I jumped straight into the bundle-application and auth logic in RN apps is usually in JS. Native modules are typically small bridges to platform functionality (camera, crypto, storage) or third‑party deps, so I didn't bother there first.

Hermes emits compact bytecode. Gone are the days of trivially reversing RN bundles; Hermes reads more like a tiny VM than JavaScript. (Tools like hermes-dec help extract readable bytecode from the bundle.)

Fortunately, function names and strings weren't heavily obfuscated, which made tracing pleasant.

My workflow:

Grep the RN bundle for endpoint‑like strings.

Jump to the callers, then bubble up to action creators.

For bytecode that's tedious to read, paste chunks into GPT‑5 to get a JS sketch, then verify manually.

That takes you straight to the JS that assembles the capture payload. The actual signing happens elsewhere.

I was honestly surprised how well GPT‑5 read and interpreted the bytecode. It's not perfect, but it's a huge time saver for tedious chunks.

GPT‑5 returned a solid outline of what the original JavaScript looked like.

The full conversation and examples it decompiled are here - some pretty impressive stuff.

Where the heavy lifting happens: the native bridge for signing

React Native calls into a native "request signer."...

skycards capture game native plane endpoint

Related Articles