Live product

Stoatally

stoatally.app

Shipped Reverse engineeringAndroid
Stoatally screenshot

Stoat is an open-source Discord alternative. Its official Android client implemented 65 of the platform's own 121 API endpoints, leaked memory through unbounded caches, and re-downloaded the entire server member list every time anybody's status changed. Stoatally is a fork that fixed all three — 207 commits, 24,000 lines of Kotlin, sixteen days.

The work started before any code was written. We enumerated the platform from the outside: DNS across four domains, the OpenAPI spec, the Rust backend's source, and the SolidJS web client, written up as twenty specs and 5,600 lines of reference — including a 28-test benchmark of the search endpoint that established sort values are case-sensitive, half the documented filters are silently ignored, and the response changes shape depending on one boolean. Then we audited the official app's performance and re-verified the audit against a pristine copy, line by line. Every fix in the fork traces to a confirmed finding. We never opened a single upstream pull request; we read their issue tracker and fixed the bugs on our side.

What came out is a client that does things the platform doesn't support. Push notifications reach de-Googled phones over UnifiedPush through a relay we run, because the backend only speaks Firebase and its own push registration is broken for third-party clients. Server-wide message search exists even though the endpoint for it returns 404 — the client fans out across channels and applies every filter the server ignores. Animated emoji survive an in-app crop because we wrote a GIF89a encoder from scratch, median-cut quantization and LZW included, after reading the file server's Rust source and learning animated GIF is the one format it won't flatten. And the whole thing is built on an Android phone in Termux, on a static ARM64 aapt2 and a hand-derived clang cross-compile, because the NDK's host tools don't run on the device we build from.

  • 65 → 121 endpoints. Upstream covered 54% of its own API; the fork has route functions for all 121, with ~110 wired to real UI and 11 API-only.
  • The member-list DoS. Upstream re-fetched all ~300 members on every user-cache mutation; the fork fetches once per sheet open — modelled at ~3,700 API calls/hour down to about five.
  • Three-way push, one of them Google-free. Runtime-selectable FCM, UnifiedPush (ntfy/Gotify/NextPush) or off, registered against a self-hosted relay, because the platform's own push endpoint is unconfigured for third-party clients.
  • SnapshotStateLruMap — a 47-line Compose-observable LRU map, because mutableStateMapOf never evicts and a LinkedHashMap never recomposes. Bounds three caches at 2,000/2,000/3,000 entries.
  • 926 lines of image codec: raw GIF sub-block and RIFF/WebP ANMF sniffing, frame extraction, and a from-scratch GIF89a encoder with median-cut quantization, per-frame Local Color Tables and variable-width LZW.
  • 20 specs, 5,600 lines of reverse-engineered platform documentation — including 39 Discord-parity features proven impossible on the current API, and a performance audit re-verified against the official app.
  • Built on the phone: static ARM64 aapt2 injected via aapt2FromMavenOverride (SDK 36; Termux's own tops out at 34), and a Termux-clang + NDK-sysroot cross-compile verified to emit ELF aarch64, built by NDK r27.