Four Payment Routes, One Ledger
Client work · LiveA streaming platform rebuilt across six services, with money and playback both hardened
Four payment routes, each with its own idea of a purchase, and a client app that could not be trusted.
Every route reconciles to one ledger, and every price is decided on the server.
The problem
ZLAN is a vertical streaming platform for Myanmar and Thailand. It came to me as a handover from an outsourced vendor, after the original API and dashboard had been compromised and the database dropped. There were no automated tests or checks in the pipeline and little protection against abuse, on a system that takes money and serves paid content.
I own engineering end to end: architecture, delivery, security, infrastructure and cost. The rebuild covered six services in four months: backend, the iOS and Android app, an admin back-office, a creator portal and the marketing site.
What follows is the part worth writing down: three problems where the obvious implementation is wrong in a way that only shows up in production.
Money: four routes, one ledger
The platform bills through four separate routes: two regional payment gateways plus Apple and Google in-app purchase. Every one of them has a different notion of what a transaction is, when it is final, and how it tells you.
Two decisions did most of the work:
Pricing is decided server-side. The client asks what something costs; it never asserts it. This sounds obvious and is routinely got wrong, because store billing flows hand the client a price and it is tempting to trust the value that comes back. A client-supplied amount is an amount an attacker chooses.
All four routes reconcile onto a single accounting ledger. Not four subsystems with a reporting view stitched over the top, one ledger, with each route as an adapter into it. The alternative looks cheaper for the first two integrations and then makes the third one structural surgery, because nothing agrees on what a balance is.
Idempotency and reconciliation were designed in at the start rather than added after the first duplicate. Every external biller retries, and retries arrive out of order.
Video: making every segment stand alone
The encoder produces an adaptive bitrate ladder, and the requirement is that a player can switch rendition mid-stream without artefacts, which means every segment has to be independently decodable.
That holds only if segment boundaries land on keyframes. The implementation forces keyframes at a fixed interval and derives the GOP length from the source frame rate, with scene-cut insertion disabled so the encoder cannot add keyframes of its own. Segment duration is then an integer multiple of the GOP, so the two never disagree.
The version I inherited derived GOP from a hardcoded frame rate. On any source that wasn’t that rate, the computed GOP and the forced keyframes fought each other, and segment boundaries drifted off keyframes, which is the kind of defect that produces occasional playback glitches nobody can reproduce on demand. Deriving it from the actual source rate removed a whole class of bug rather than a symptom.
Playback is served over encrypted HLS. I also wrote down the decision not to buy commercial DRM at this stage, with the reasoning and the conditions that would change it. An unwritten decision to skip something reads later as an oversight; a written one is a position you can defend or revisit.
Trust: assume the client is lying
A paid-content app is a trust problem where the attacker controls the device. Playback credentials are released only behind a server-side platform attestation gate (Apple’s App Attest on iOS, Play Integrity on Android) and the gate is built to deny by default.
Three properties mattered more than the cryptography:
- It fails closed. Any error in the attestation path denies access. A verification step that fails open is decoration.
- Replay is blocked by a monotonic counter, not by timestamps. The device’s own signature count must strictly increase, so a captured-and-replayed assertion is rejected even if it is otherwise valid and recent.
- Dependencies stay out of the trust path. The attestation objects are parsed by a small reader I wrote rather than a third-party library, because a parser sitting inside a security boundary is a supply-chain surface, and this one needed to handle exactly one well-specified shape.
The same instinct runs through session handling: refresh tokens rotate, and presenting a token that has already been consumed is treated as theft rather than as a mistake, the whole token family is revoked, so a stolen token costs the attacker the session instead of granting one.
Cost
The cloud bill came down by roughly 90%, through a phased migration where every step was independently reversible. The reversibility was the point: a cost migration that has to be completed to be safe is one that gets abandoned halfway and leaves the system worse than before.
The first back-office was deliberately built on an off-the-shelf framework, fast, and the right choice while nobody yet knew what the business actually needed from it. Once the requirements were real I replaced it, which also moved the platform off always-on servers and took cost down again. Building the disposable version first was cheaper than guessing correctly.
What I’d take from it
Every one of these has the same shape. The naive implementation works in the demo and fails under a condition you won’t meet until you have real traffic, real devices and real money: a frame rate you didn’t expect, a retry that arrives twice, a client that lies about a price, a token captured and replayed. Senior work on a system like this is mostly knowing which of those conditions are coming.