ShootOut
Live · Algorand mainnet- Role
- Sole backend developer (98% of repo)
- Timeline
- Jun 2025 → present (mainnet since Jan 2026)
- Chain
- Algorand (mainnet)
- Scale
- 288 commits · 29 models · ~104 endpoints · 10 services · 8 jobs · 10 crons
- Stack
- Node 18 · Express · discord.js v14 · MongoDB (29 models) · Redis (distributed locks) · Algorand SDK v3 · PM2 (2 API + 1 bot)
ShootOut runs penalty-shootout tournaments entirely inside Discord, with real crypto wagered on every match. It's a tournament engine, a custodial money system, and a Discord-native game client all at once: players stake ALGO, ASAs, or LP tokens into the prize pool, enter brackets from 1v1 up to 64 players, play button-by-button (or hand it to autoplay), and the winner takes the pot through an automated on-chain payout. There's no public game site — the game is played entirely in Discord, with an internal admin dashboard for operators — all backed by the API I built. It's live on Algorand mainnet.
The problem
A penalty shootout is trivial to describe — pick a direction, simultaneous reveal, best of N. Turning it into a platform is not. ShootOut has to schedule and run multi-round tournaments for many Discord servers, hold and reconcile real player funds to the microAlgo, render every interaction as Discord buttons and embeds, and never drop, double-pay, or stall a bracket with money in it. The hard parts are tournament orchestration and custody, not the kick.
Three things have to be true or the platform loses real money or real trust:
- Custody has to be exact. The platform holds player funds. Every balance must reconcile across crashes, retries, and a multi-process cluster — you cannot pay out money you don't have, and you cannot lose a deposit.
- Tournaments must resolve deterministically. No-shows, disconnects, and walkovers can't freeze a bracket that's holding a prize pool. Seeding, auto-advance, and scheduled start times all have to be automatic.
- 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 and AES-encrypted at rest; a treasury wallet receives deposits. The Discord bot is the client — there's no separate game server and no public web app; the only web surface is an internal admin dashboard for operators — and an in-process integration bridge keeps the bot and API in lockstep with direct calls instead of a network hop.
The hardest decision: a custodial double-entry ledger instead of on-chain escrow
The "pure Web3" answer is an escrow contract holding every wager. I chose a custodial off-chain double-entry ledger with on-chain settlement instead, and it was the right call: a single tournament has dozens of micro-movements (entry, rake, walkover refunds, prize splits) that would be slow and expensive as individual on-chain transactions, and Discord players expect instant balance updates, not finality-per-action.
The tradeoff I accepted: the platform takes on custody, so the ledger carries the full weight of correctness. I built it like a bank would. Every value movement is a double-entry record (ledgerMovement + userBalance + feeBalance) whose debits and credits sum to zero, and a reconciliation job continuously checks the ledger against treasury reality. On-chain settlement 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.
What I designed for
- A real tournament engine. Event-driven brackets from 1v1 to 64 players across multiple formats (tournament / league / deathMatch), with seeding, walkover handling, and auto-advance so a no-show resolves instead of stalling. A scheduler cron starts tournaments at their appointed time; autoplay lets a player pre-select and let the engine play their turns.
- Deposit detection that can't miss. The deposit worker polls the treasury with the indexer's
lookupAccountTransactionsand a persistedminRoundcursor, so it resumes exactly where it left off after any restart — a confirmed deposit is never skipped and never double-counted. - A circuit breaker over every chain dependency.
algoUtils.jswraps 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.
- Leaderboards + identity. Weekly, monthly, all-time, and per-event leaderboards; NFD names for display; optional NFT-gated entry; and a cooldown "jail" for abusers.
- Abuse controls. Scam-microtransaction detection, a 1000-request/hour/IP rate limit, dynamic IP block/allowlists, and CSP harden the surface.
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), still running.
- 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, a multi-format tournament engine with scheduling and autoplay, and a circuit-breaker/retry chain layer with 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), orchestration of stateful multi-round tournaments holding real money, resilience against unreliable downstreams (per-service circuit breakers, backoff, resumable cursors), and concurrency correctness across a cluster — all in production with real money on Algorand mainnet.