ShootOut — a custodial money-handling backend for live, real-money Algorand tournaments
Role: Lead backend developer (sole author, 288 commits / 98%) · Client: 3BUX LLC Stack: Node.js 18 · Express · discord.js v14 · MongoDB (Mongoose, 29 models) · Algorand JS SDK v3 · Redis (distributed locks) · PM2 (cluster: 2 API + 1 bot) · SQLite/Knex (bot) Live: hojshootout.com · API api.hojshootout.com/api/v1 · on Algorand mainnet since 2026-01-28 Timeline: Jun 2025 → Mar 2026 (~9 months)
The problem
ShootOut runs penalty-shootout tournaments inside Discord with real crypto on the line: players deposit ALGO, ASAs, or LP tokens, enter 1v1-through-64-player brackets (tournament / league / deathMatch formats, optional NFT-gated entry), and winners get automated on-chain payouts. The mechanic is simple — pick a direction, simultaneous reveal — but the engineering bar is not. Three things have to be true or the platform loses real money:
- Custody has to be exact. The platform holds player funds. Every balance must reconcile to the microAlgo, across crashes, retries, and a multi-process cluster — you cannot pay out money you don't have, and you cannot lose a deposit.
- The chain is unreliable on a human timescale. Algod nodes time out, the indexer lags, Pera rate-limits. A deposit that the chain confirmed but the backend missed is a furious user and a support ticket.
- It can't be raced or gamed. Two cluster instances must never double-credit a deposit or double-pay a winner; a scammer must not be able to grief the treasury with dust.
Architecture
Per-user wallets are generated server-side (algosdk.generateAccount) and AES-encrypted at rest; a treasury wallet receives deposits. The Discord bot is the client — there's no separate game server — and an in-process integration bridge (1,747 LOC) keeps the bot and API in sync with direct calls rather than a network hop.
The hardest decision: a custodial double-entry ledger instead of on-chain escrow
The "pure Web3" answer is an escrow smart contract holding every wager. I chose a custodial off-chain double-entry ledger with on-chain settlement instead, and it was the right call for this product: tournaments have dozens of micro-movements (entry, rake, refunds on walkover, prize splits) that would be slow and expensive as individual on-chain transactions, and Discord users expect instant balance updates, not 4-second finality per action.
The tradeoff I accepted: the platform takes on custody, which means the ledger now carries the full weight of correctness. So I built it like a bank would. Every value movement is a double-entry record (ledgerMovementModel + userBalanceModel + feeBalanceModel) — debits and credits that must sum to zero — and a reconciliation job continuously checks the ledger against treasury reality. On-chain settlement (makeAssetTransferTxn / makePaymentTxn) happens only at the edges: deposit in, withdrawal out. There are no smart contracts here by design — the proof is the live mainnet product and the ledger discipline, not a contract address.
What I designed for failure
- Deposit detection that can't miss.
depositJob(1,459 LOC) polls the treasury with the indexer'slookupAccountTransactionsand a persistedminRoundcursor, so it resumes exactly where it left off after any restart — a confirmed deposit is never skipped and never counted twice. - A circuit breaker over every chain dependency.
algoUtils.js(2,963 LOC) wraps algod, the indexer, Pera, and NFD each in its own breaker (CLOSED → OPEN → HALF_OPEN) with exponential backoff + jitter and a Bottleneck limiter. When a node degrades, the breaker trips and sheds load instead of hammering a dying service and cascading the failure. - Cluster-safe by construction. Redis distributed locks serialize the operations that must not interleave across the PM2 cluster (deposit credit, payout, bracket advance) — two instances can't double-credit or double-pay.
- Abuse controls. Scam-microtransaction detection, a 1000-request/hour/IP rate limit, dynamic IP block/allowlists, and CSP harden the public surface.
- Tournament integrity. An event-driven engine handles walkovers, auto-advance, seeding, and multi-format brackets, so a no-show or disconnect resolves deterministically instead of stalling a bracket with money in it.
Results
- 288 commits, sole backend author (98% of the repo), over ~9 months — a live, mainnet, real-money platform (migrated testnet → mainnet 2026-01-28).
- 29 models · 15 controllers · ~104 endpoints · 10 services · 8 job workers · 10 cron schedules, behind a PM2 cluster.
- A custodial double-entry ledger with reconciliation behind a 2,963-LOC circuit-breaker/retry chain layer and Redis distributed locks for cluster safety.
What this demonstrates
The exact discipline fintech and on-chain-settlement roles screen for: custody done like a bank (double-entry, reconciliation, audit), resilience against unreliable downstreams (per-service circuit breakers, backoff, cursors that resume), and concurrency correctness across a cluster (distributed locks) — all running in production with real money on Algorand mainnet.