Discord Bots

Designing an Economy Bot That Scales

Build a Discord economy bot that stays correct under load — atomic balance updates, safe transfers, cooldowns, fast leaderboards and an economy that doesn't inflate.

On this page
  1. Start with the right storage
  2. The race condition that duplicates money
  3. Transfers need transactions
  4. Keep a transaction log
  5. Cooldowns that survive restarts
  6. Fast leaderboards
  7. Design an economy that doesn’t inflate
  8. Guard against abuse
  9. Resources as you grow
  10. Summary

Economy bots look simple: users earn coins, spend coins and compare balances. In a single test server, almost any implementation works. At a few thousand servers, the same code starts duplicating money, losing transfers and timing out on leaderboards. This article covers the design decisions that keep an economy correct and fast as it grows.

Start with the right storage

An economy is a ledger, and ledgers need a real database. JSON files corrupt when two writes collide, and in-memory balances vanish on every restart. Use MySQL or PostgreSQL — every Kerit Cloud plan includes a MySQL database, and higher tiers add PostgreSQL. The table design itself is covered in designing a database schema for a Discord economy bot; this article focuses on the logic around it.

Two decisions to make early:

Global or per-server balances? A global economy follows users everywhere; a per-server economy lets each community run its own. Per-server is more common and keys every balance on (guild_id, user_id). Changing this later means migrating every row, so choose deliberately.

Integers, not floats. Store currency as whole numbers in a BIGINT column. Floating-point arithmetic produces balances like 99.99999999, and rounding errors accumulate. If you need decimals, store the smallest unit (cents) as an integer.

The race condition that duplicates money

Here’s the classic bug:

// DON'T DO THIS
const user = await db.getUser(guildId, userId);
if (user.balance < price) return reply('Not enough coins.');
await db.setBalance(guildId, userId, user.balance - price);

If a user clicks “Buy” twice quickly, both requests read the same balance, both pass the check, and both write balance - price. The user pays once and receives two items. With transfers, the same pattern can create or destroy money.

The fix is to let the database do the check and the update in one atomic statement:

UPDATE balances
SET balance = balance - ?
WHERE guild_id = ? AND user_id = ? AND balance >= ?;

If the affected-row count is 1, the purchase succeeded. If it’s 0, the user couldn’t afford it. There’s no window between reading and writing, so double-clicks, retries and concurrent commands can’t overspend.

Transfers need transactions

A transfer touches two rows: subtract from one user, add to another. Both must succeed or neither should. Wrap them in a transaction:

const conn = await pool.getConnection();
try {
  await conn.beginTransaction();
  const [debit] = await conn.execute(
    'UPDATE balances SET balance = balance - ? WHERE guild_id = ? AND user_id = ? AND balance >= ?',
    [amount, guildId, fromId, amount]
  );
  if (debit.affectedRows !== 1) {
    await conn.rollback();
    return 'insufficient';
  }
  await conn.execute(
    `INSERT INTO balances (guild_id, user_id, balance) VALUES (?, ?, ?)
     ON DUPLICATE KEY UPDATE balance = balance + VALUES(balance)`,
    [guildId, toId, amount]
  );
  await conn.commit();
  return 'ok';
} catch (err) {
  await conn.rollback();
  throw err;
} finally {
  conn.release();
}

That’s MySQL with the mysql2 driver; PostgreSQL uses INSERT ... ON CONFLICT (guild_id, user_id) DO UPDATE SET balance = balances.balance + EXCLUDED.balance. Also validate inputs before touching the database: the amount must be a positive integer, and users can’t transfer to themselves.

Keep a transaction log

For anything beyond a toy economy, record every change in an append-only table: who, how much, why and when. It costs little and pays off the first time a user claims coins disappeared or you need to reverse an exploit. With a log, you can audit, refund and detect abuse; without one, you can only guess.

Cooldowns that survive restarts

/daily and /work commands need cooldowns. Storing them in memory means a restart — which happens on every deploy — resets every cooldown and lets users claim twice. Store the last-claim time with the balance instead, and again check and update atomically:

UPDATE balances
SET balance = balance + 100, last_daily = NOW()
WHERE guild_id = ? AND user_id = ?
  AND (last_daily IS NULL OR last_daily < NOW() - INTERVAL 1 DAY);

For very high-traffic bots, Redis keys with a TTL make an excellent cooldown store — SET cooldown:daily:<guild>:<user> 1 EX 86400 NX succeeds only if the key doesn’t exist. See Redis vs SQL for when that trade-off is worth it.

Fast leaderboards

A leaderboard query sorts every user in a server by balance. Without an index, that’s a full table scan on every /leaderboard. Add a composite index that matches the query:

CREATE INDEX idx_guild_balance ON balances (guild_id, balance DESC);

SELECT user_id, balance FROM balances
WHERE guild_id = ?
ORDER BY balance DESC
LIMIT 10;

Descending index order is supported in MySQL 8 and PostgreSQL. Leaderboards also don’t need to be real-time: cache the top ten per guild for 30–60 seconds and popular servers stop hammering the database. A user’s own rank (“you are #1,204”) is more expensive — compute it on demand with COUNT(*) WHERE balance > ?, and cache that too.

Design an economy that doesn’t inflate

Technical correctness isn’t the only way an economy breaks. If coins only ever enter the system, balances inflate until prices are meaningless and the top of the leaderboard is out of reach. Healthy economies have sinks as well as sources:

  • Items and upgrades that are consumed or expire.
  • Transfer fees or taxes.
  • Gambling with a house edge (check your community’s rules and Discord’s policies on real-money value first).
  • Prestige resets that trade balance for a permanent perk.

Watch the total coins in circulation per guild over time. A steadily climbing line means you need more sinks.

Guard against abuse

Popular economies attract farmers. Some practical defences:

  • Per-user rate limits on earning commands, in addition to cooldowns.
  • Minimum account age or server membership time before earning, to slow alt accounts.
  • Caps on transfers per day, so a farmed alt can’t funnel unlimited coins to a main.
  • The transaction log, which makes patterns like “ten new accounts all paying one user” easy to spot.

Resources as you grow

Economy bots are database-heavy rather than memory-heavy. The bot process itself stays modest if you avoid caching every balance in memory; the database does the work. On Kerit Cloud’s Discord bot plans, economy bots with real traffic typically sit in the Extreme or Ultimate range — 3–6 GB of RAM and 20–30 GB of NVMe — which leaves room for a busy database, command bursts and caching. If your database outgrows the included one, a standalone database plan gives it dedicated resources and longer backup retention.

Summary

A scalable economy stores integer balances in a real database, updates them atomically so double-clicks can’t duplicate money, wraps transfers in transactions, keeps cooldowns in storage rather than memory, indexes and caches leaderboards, and logs every change. Add sinks so the currency keeps its value, and basic abuse limits so farmers don’t break it for everyone else.