Architect a tip bot

Lesson 8 of 11 25 min read Your First Dogecoin App

The tip bot is Dogecoin's native art form – the original ones introduced millions of people to their first DOGE. It's also the perfect capstone: deposits, withdrawals, and instant internal transfers in one small system. This lesson is about the architecture; pair it with the tip-bot starter kit when you're ready to build the whole thing.

The key insight: tips are database rows

A naive tip bot sends an on-chain transaction for every !tip. That fails immediately: every tip pays a miner fee, takes a minute to confirm, and bloats your wallet with dust UTXOs. Every serious tip bot instead runs an internal ledger:

  • Deposits are on-chain: each user gets a deposit address; confirmed arrivals credit their ledger balance (exactly the payment-detection pattern).
  • Tips are off-chain: instant, free database moves between two users' balances.
  • Withdrawals are on-chain: sendtoaddress from the bot's wallet, debited from the ledger.

The bot's node wallet holds the pooled funds; the database records who owns what. The invariant you must protect: sum of all ledger balances ≤ wallet balance. Check it on a schedule and alert yourself if it ever drifts.

Say it plainly: this is custody. Your bot holds other people's money. That means real responsibility – security, honest accounting, and possibly regulatory duties depending on where you and your users live and how big balances get. For a hobby bot: keep balances pocket-change small, publish rules, and never treat user funds as yours. If it grows beyond pocket change, get proper advice. We teach the pattern honestly rather than pretending nobody will build one.

Ledger rules that prevent 90% of disasters

  • Store amounts as integer koinu (1 DOGE = 100,000,000 koinu, same 10-8 scheme as satoshis). Floating-point money creates phantom fractions of a DOGE.
  • Move money in transactions: debit and credit inside one database transaction with a balance check – WHERE balance >= :amount on the debit, roll back if no row updated.
  • Rate-limit withdrawals and add a per-day cap. If a bug or a compromise happens, the cap is what makes it a story instead of a disaster.
  • Keep the hot wallet thin: sweep anything beyond a working float to a wallet whose keys the bot machine does not have (see the production checklist).

A Discord skeleton to hang it on

Enough structure to see where every piece goes – balances live in the same SQLite style as the invoice lesson, and rpc is our usual wrapper:

import discord
from discord.ext import commands

intents = discord.Intents.default()
intents.message_content = True            # required for prefix commands
bot = commands.Bot(command_prefix="!", intents=intents)

@bot.command()
async def balance(ctx):
    bal = ledger_balance(ctx.author.id)          # SELECT from your ledger
    await ctx.reply(f"your balance: {bal/1e8:g} DOGE")

@bot.command()
async def deposit(ctx):
    addr = deposit_address(ctx.author.id)        # getnewaddress once, then reuse
    await ctx.author.send(f"deposit address: {addr}")   # DM, not public channel

@bot.command()
async def tip(ctx, member: discord.Member, amount: float):
    koinu = int(round(amount * 1e8))
    if ledger_transfer(ctx.author.id, member.id, koinu):  # atomic debit+credit
        await ctx.reply(f"{ctx.author.display_name} tipped "
                        f"{member.display_name} {amount:g} DOGE. wow.")
    else:
        await ctx.reply("insufficient balance.")

@bot.command()
async def withdraw(ctx, address: str, amount: float):
    check = rpc.call("validateaddress", address)
    if not check.get("isvalid"):
        return await ctx.reply("that doesn't look like a valid address.")
    koinu = int(round(amount * 1e8))
    if not withdraw_allowed(ctx.author.id, koinu):        # limits + balance
        return await ctx.reply("over limit or insufficient balance.")
    txid = rpc.call("sendtoaddress", address, amount)
    ledger_debit(ctx.author.id, koinu, txid)
    await ctx.reply(f"sent. txid: {txid}")

bot.run(BOT_TOKEN)   # token from env, never in source

What's deliberately missing – and is exactly the work that makes it real: the deposit watcher loop (lesson 7, pointed at per-user addresses), the atomic ledger_transfer, withdrawal limits, and abuse controls (cooldowns, minimum tip, blocklist). The starter kit includes a Claude Code prompt that scaffolds all of it on testnet.

Key takeaways

  • Tips are ledger moves; only deposits and withdrawals touch the chain.
  • Integer koinu + atomic transfers + the balances-vs-wallet invariant = honest accounting.
  • Custody is real responsibility: small balances, hard limits, published rules.
  • Build and soak it on testnet first – the addresses' n prefix keeps you truthful.
On the map

Built something on Dogecoin? Put it on the map.

The directory is the page people link when someone says Dogecoin has no developers. If you maintain a library, wallet, tool, or app – even a small one – it belongs here.