Back to Blog
Engineering
Aug 26, 2026

PX TopUp: One API, Three Roles, Two Clients

Uthsob Chakraborty

Uthsob Chakraborty

Software Engineer

PX TopUp: One API, Three Roles, Two Clients

PX TopUp is a marketplace for game top-ups: buyers order credit, agents fulfil it, admins moderate and pay everyone out. Three roles, two clients, one API. This is the shape of it and the handful of rules that keep it boring.

The system, end to end

PX TopUp system architecture: three clients over HTTPS into one Next.js host, a five-stage request pipeline, five core services, MongoDB and Firestore for data, and Resend, FCM, R2, PostHog and EAS Update as external systems.

One API ยท three roles ยท two clients.

There is one host. The web marketplace, the agent and admin console, and the Expo app all talk to the same Next.js App Router deployment over HTTPS with a bearer token, refreshed silently on expiry. No BFF, no per-client API, no gateway. A second surface would have meant a second place for an authorization rule to drift, and drifted authorization is how a marketplace leaks money.

Behind that host, five services own five nouns โ€” wallet, catalog, growth and risk, comms, orders โ€” and two stores split along one axis: durability versus liveness. MongoDB holds everything anyone will ever need to audit. Firestore holds the chat thread, because a message that arrives 400ms late is a worse bug than a message that is hard to query.

Every request walks the same five gates

Request lifecycle: five gates โ€” rate limit, verify access token, role check, schema validation, handler โ€” each with one typed rejection (429, 401, 403, 422); then the four write paths a surviving handler may take; then the order state machine from placed to confirmed with a disputed and refunded branch.

Five gates in, typed errors out, and the only path money takes.

The order never changes, and each gate owns exactly one rejection. That constraint is what makes the whole thing debuggable: a status code tells you which gate fired without reading a stack trace.

  • Role check re-reads from the database. The role in the token is a hint, not a fact. An admin demoted two minutes ago is demoted on the next request, not on the next login.

  • Validation happens at the boundary. Parse once on the way in, then every function below it takes a type instead of a maybe.

  • A raw 500 is a bug, not an outcome. If a handler can fail in a way a client should handle, that failure gets a code and a shape.

Money has exactly one path

Two rules cover most of what could go wrong with the money, and both are visible in the diagram above.

Every order create carries an idempotency key. A buyer on a flaky mobile connection taps twice; the second request returns the first order instead of creating another one. Without this you are reconciling duplicate charges by hand, at night.

The ledger is append-only and the balance is a projection. No handler patches a balance column โ€” it writes an entry, and the balance is what the entries add up to. Amounts run through decimal.js in BDT, never a float. A refund is a new reversing entry, not a deletion, so the audit trail survives the mistake.

Commission only splits when an order reaches confirmed. Everything before that is a promise, and the state machine guards every transition, so there is no code path that pays an agent for an order that was later disputed.

What sits outside

The external column is deliberately dull: Resend for verify and reset mail, FCM for the order bell and chat chime, R2 for proofs and logos and banners, PostHog for product analytics, EAS Update for OTA bundles and the APK version gate. Each one is reachable from exactly one service, which means swapping any of them is a change in one file.

The version gate is the one worth calling out. Shipping a mobile client against an evolving API means some users are always a build behind, so the gate lets the server refuse a client that is too old rather than letting it fail in ways nobody can reproduce.

What I would keep

The single host and the fixed pipeline order. Both are the kind of decision that feels like under-building on day one and pays for itself the first time an authorization question has exactly one answer.

ArchitectureNext.jsMongoDBFirestoreExpoPayments