BetPay
[ LIVE ]My side project, live in production: a multi-tenant SaaS that automates 1xBet deposits and withdrawals over mobile money for local agents, run on a phone farm that holds their SIMs.
- Node.js
- WebSocket (ws)
- PostgreSQL
- Kotlin / Android
- PWA
- Railway
- Phone farm
The problem
Funding a 1xBet account from mobile money in Burkina Faso goes through a local agent, over WhatsApp. The client sends their ID, waits for the agent’s number, pays, sends a screenshot as proof, and waits again while the agent checks it by eye and types the credit by hand. The illustration above shows a typical evening deposit: 48 minutes from request to credit.
BetPay turns that agent into a SaaS tenant. The agent signs up, connects their MobCash agent account, and mails in their SIM. Their customers get a web app in the agent’s own colours, and deposits and withdrawals run without anyone typing.
Money moves last
One rule shapes the whole transaction flow: never move the leg I control until the leg I can’t reverse has confirmed.
Deposit. The client enters their 1xBet ID, and the server looks up the
account holder’s name so they can check it is the right account before paying.
The transaction is recorded as AWAITING_SMS and the client dials the agent’s
merchant USSD code. The 1xBet credit through MobCash runs only after the
operator’s payment-confirmation SMS reaches the server and matches. If the credit
fails after the money arrived, the transaction is marked FAILED and raises an
alert. It never fails silently.
Withdrawal. A two-stage wizard. First the server verifies the 1xBet payout code and checks that it belongs to the entered 1xBet ID. That step moves no money and creates no transaction. Only then does the client pick an operator and phone number, and the farm phone dials the payout USSD on the agent’s SIM. The server checks code ownership again at execution, in case a client skips the first step.
The phone farm
Hosted agents ship their SIM, and one farm phone holds several agents’ SIMs.
The server attributes every inbound payment SMS and every USSD result to an
agent by the SIM’s Android subscription_id. The ICCID would be the obvious key,
but non-rooted phones can’t read it.
A farm phone that drops off silently stalls every deposit of every agent on it, so the farm is monitored like production infrastructure:
- Each phone sends health heartbeats. The super-admin “Parc SIM” page shows devices, SIMs, PIN coverage, and an operations feed.
- After a grace period offline, the agents and the super admin get an alert, once per outage. On reconnect, the phone resends the payment SMS it received in between and the server reconciles them.
- Mobile money PINs live encrypted on the phone. The server pushes a PIN and records the acknowledgement, but never stores the PIN itself.
Clients dial USSD on mobile data, which drops their connection mid-transaction. A durable status store replays missed updates when they reconnect with a new connection.
The SaaS around it
- One PWA for every agent. The agent ID in the URL sets the name, logo, colours (extracted from the agent’s logo), merchant code, and withdrawal address. Branding is data, not a build. The service worker picks up deploys on the next open.
- Agent portal. Password sign-in with per-agent tokens, onboarding with a free trial, a subscription paid through a mobile money aggregator, a message inbox, and a referral program with an automatic 1% commission.
- Super-admin controls. Agent approval, pricing, banners, gateway settings stored in the database rather than in env vars, and per-agent switches that pause deposits or withdrawals without suspending the account.
Where it stands
The SaaS runs in production on Railway. The two original agents moved onto it in October 2026, and the Flutter client app it started with is retired in favour of the PWA. The protocol was written down before the code, which is the main reason the same backend has outlived three generations of front end.