An LLM revived a 2016 game with one byte An agent running Deepseek Flash v4.1 in the PI harness made a 2016 Android game, "Army of Darkness Defense," installable on current Android versions by editing a single byte in the APK's binary manifest to raise its targetSdkVersion from 23 to 24, the minimum that clears the install gate on Android 15 (API 35), 16 (API 36) and 17 (API 37). The author, who states he has no Android development knowledge, set up the experiment after Android 14 rejected the APK with INSTALL_FAILED_DEPRECATED_SDK_VERSION; the agent first confirmed the app's signature checks were confined to bundled ad and in-app-purchase SDKs, making re-signing safe, then avoided a full apktool rebuild in favor of a width-preserving edit. The author argues the approach generalizes to enterprise legacy migrations, where the goal is the minimum viable compatibility rather than the newest SDK, since each targetSdk value above 24 activates behavioral rules the app was never written against. A week ago I tried to install one of my favourite childhood games, “Army of Darkness Defense” from 2016, on my Android phone for the nostalgia. It was no longer on the Play Store, so I found the APK on the web. It refused to install: INSTALL FAILED DEPRECATED SDK VERSION: App package must target at least SDK version 24, but found 23 Since Android 14 the OS refuses apps built for ancient API levels, because malware historically targeted low targetSdkVersion to opt out of the runtime permission model. The app still works. The platform just stopped accepting it. That is the same situation a lot of enterprise software is in. The software works, the vendor is gone or the upgrade costs a fortune, and the only thing standing between it and a supported machine is a compatibility gate that nobody dares open by hand. Usually the answer is a rewrite, a paid migration project, or a VM kept alive for another decade. I’d been watching people port PS5 games to PC with LLMs, and that was the thing that made me stop and think: the model wasn’t doing anything clever, it was just reading a lot of tedious format documentation and carefully getting details right. That is exactly the profile of a legacy migration. a non-creative Boring task which an Engineer must do here and there. So I set up an experiment. I have no Android development knowledge at all. I gave the APK to an agent Deepseek Flash v4.1 in the PI harness and asked it to make the game install. A note on whose work this is. I didn’t solve an Android problem, I couldn’t have. The reasoning below is the agent’s, the decisions were its, and the commands are the ones it ran. My contribution was setting up the experiment, reading what it did, and occasionally stopping it from going down the wrong path. The wrong instinct: newest is best the-wrong-instinct-newest-is-best first thing which comes to mind is to bump the SDK to 35 latest . That is also how you turn a working game into a crash loop, it doesn’t work targetSdk is not a “how new am I” label, it’s a contract. Every value above 24 switches on another behavioural rule the app was never written against: FileProvider requirements at 24, background service restrictions at 26, cleartext HTTP blocked at 28, package visibility at 30. A 2016 game engine that loads assets from local paths and phones home to dead ad networks breaks in several of those. The right question is not “what is newest” but “what is the lowest SDK that clears the gate”. That is a table, not a guess — and it plateaus at 24: | Android | API | minimum targetSdk to install | |---|---|---| | 14 | 34 | 23 | | 15 | 35 | 24 | | 16 | 36 | 24 | | 17 | 37 | 24 | So 23 → 24 is the smallest change that clears every current and announced release. This is the part that generalises to enterprise migrations: the goal is the minimum viable compatibility, not the newest everything. Check for anti-tamper first, not after check-for-anti-tamper-first-not-after If the app verifies its own signature, re-signing it bricks it, and you only find out once it’s already broken. The agent checked before touching anything: strings -a classes.dex | grep -E "GET SIGNATURES|getPackageInfo|checkSignatures" That hits, which looks alarming. So it asked which class calls them — grepping the 2049-class inventory for PackageManager , integrity or tamper returned nothing. The signature APIs were only reachable through bundled ad and IAP SDKs. Checking native code told the same story: the getSignature string sits next to in-app-purchase receipt parsing, not APK integrity. Verdict: safe to re-sign. One byte, not a rebuild one-byte-not-a-rebuild The obvious route is apktool d / apktool b , which re-encodes the whole binary manifest — string pool rebuilt, attributes reordered, resources re-serialised. For a one-number change that is a huge diff and a fresh chance to break something. Instead: a width-preserving in-place edit. targetSdkVersion is a 4-byte little-endian integer, and 23 and 24 occupy exactly the same space, so nothing else in the file has to move. The manifest inside an APK is chunked binary XML, not text. Two details cost the agent its first two attempts — attribute offsets are relative to attributeStart , not headerSize , and the typed value sits at +16 inside the attribute: a = node off + 16 + attr start + i attr size correct, not node off + headerSize name = strings name index → "targetSdkVersion" vtype = buf a + 15 0x10 == TYPE INT DEC vdata = u32 buf, a + 16 → 23 Then the entire change: struct.pack into "