How to Build a Dogecoin Tipping Bot for Discord and Telegram in 2026

Disclosure: This post may contain affiliate links. If you make a purchase through these links, we may earn a commission at no extra cost to you.

April 2026 – Dogecoin tipping is more than a transaction; it is a cultural ritual. From Reddit’s early days to the “Zap” culture on Nostr, sending a few DOGE to someone who made you laugh or helped you debug code is the purest expression of the Shibe ethos: Do Only Good Everyday.

If you run a Discord server or Telegram group for crypto enthusiasts, gamers, or developers, a custom tipping bot can transform passive members into an engaged community. It also gives you a practical, hands‑on education in Web3 backend engineering—managing cryptographic addresses, tracking off‑chain balances, and interfacing with the Dogecoin network.

This tutorial will guide you through building a custodial tipping bot (the most common type for chat platforms) using Python. We will cover architecture, database design, deposit address generation, command handling, and security best practices. By the end, you will have a functional bot that allows users to tip each other instantly without paying Dogecoin network fees on every micro‑transaction.

Note: This bot is custodial – you (the bot operator) hold the private keys for the main wallet and manage user balances in a database. This is safe for small, trusted communities, but you must never risk more than you are willing to lose. For production use, consider multi‑sig or regular audits.


1. Custodial vs. Non‑Custodial Bots

Before writing code, you must understand the fundamental trade‑off in tipping bot design.

FeatureCustodial BotNon‑Custodial Bot
How balances are storedCentral database (PostgreSQL, Redis)Each user holds their own private key; on‑chain or Lightning
Transaction feesNo fees for tips (only for deposits/withdrawals)Every tip pays network fee (even if tiny, they add up)
SpeedInstant (database update)Seconds to minutes (blockchain confirmation)
Security riskUsers trust the bot operator not to steal fundsUsers control their own keys (higher responsibility)
ComplexityLow – no per‑user key managementHigh – need to generate and store keys securely

Most community tipping bots (like the famous Reddit tipbot) are custodial. They hold a master Dogecoin wallet and assign virtual balances to user IDs. When you !tip @friend 10, the bot simply updates two rows in a database. Only when a user requests a withdrawal does the bot broadcast an on‑chain transaction.

This tutorial builds a custodial bot because it is the most practical for high‑frequency, low‑value tips. You will still need a real Dogecoin wallet to receive deposits and process withdrawals.

Cultural context: To understand the phrases your bot should recognise (e.g., “much wow”, “such tip”, “shibe”), read our Ultimate Dogecoin Dictionary. Using the right language makes your bot feel native to the Shibe Army.


2. Tech Stack and Architecture

We will use Python for its simplicity and rich ecosystem. You can adapt the logic to Node.js if preferred.

2.1 Required Libraries

LibraryPurpose
discord.pyDiscord bot API wrapper (v2.x, supports slash commands)
python-telegram-botTelegram bot API wrapper (v20+)
asyncpg or psycopg2PostgreSQL driver (async for Discord bot)
redis-pyOptional: for in‑memory caching of balances
libdogecoin (Python bindings)Generate addresses, sign transactions (optional but recommended)
aiohttpFor making RPC calls to a Dogecoin node

Alternatively, you can run a full Dogecoin Core node and use its JSON‑RPC API for wallet operations. The RPC approach is simpler for prototyping.

2.2 Overall Architecture

[User] --(Discord/Telegram API)--> [Bot Application]
                                         |
                                         v
                              [Database (PostgreSQL)]
                              Stores: user_id, balance,
                                      deposit_address
                                         |
                                         v
                              [Dogecoin Node (RPC)]
                              - Generate deposit addresses
                              - Check incoming transactions
                              - Broadcast withdrawal txs

The bot:

  • Listens for commands (/tip, /balance, /withdraw).
  • For each user, it generates a unique Dogecoin deposit address (or reuses one).
  • Periodically scans the blockchain for deposits to those addresses and credits user balances.
  • When a user tips, it updates balances in the database.
  • When a user withdraws, it constructs and sends an on‑chain transaction.

For low‑level cryptographic operations, consider using Libdogecoin. See our Building with Libdogecoin: A 2026 Guide for details on key generation and signing without a full node.


3. Database Schema

We need tables to track users, their balances, and deposit addresses.

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    platform VARCHAR(10) NOT NULL,   -- 'discord' or 'telegram'
    platform_user_id VARCHAR(64) NOT NULL, -- Discord snowflake or Telegram chat ID
    balance DECIMAL(20,8) NOT NULL DEFAULT 0,
    deposit_address VARCHAR(34) UNIQUE, -- Dogecoin address for this user
    created_at TIMESTAMP DEFAULT NOW(),
    UNIQUE(platform, platform_user_id)
);

CREATE TABLE transactions (
    id SERIAL PRIMARY KEY,
    user_id INT REFERENCES users(id),
    type VARCHAR(20),                  -- 'deposit', 'tip_sent', 'tip_received', 'withdraw'
    amount DECIMAL(20,8),
    counterparty_user_id INT NULL,     -- for tips
    txid VARCHAR(64) NULL,             -- on‑chain transaction hash (if any)
    timestamp TIMESTAMP DEFAULT NOW()
);

CREATE TABLE pending_withdrawals (
    id SERIAL PRIMARY KEY,
    user_id INT REFERENCES users(id),
    amount DECIMAL(20,8),
    destination_address VARCHAR(34),
    status VARCHAR(20) DEFAULT 'pending', -- pending, processed, failed
    created_at TIMESTAMP DEFAULT NOW()
);

For high performance (many tips per second), you might add a Redis cache layer for balances. But for a moderate community, PostgreSQL is fine.


4. Step‑by‑Step Logic Flow

We will implement the core commands using discord.py. The same patterns apply to Telegram.

4.1 Setting Up the Bot Client

import discord
from discord.ext import commands
import asyncpg
import aiohttp
import json
from decimal import Decimal

intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix='!', intents=intents)

# Database pool
db_pool = None

@bot.event
async def on_ready():
    global db_pool
    db_pool = await asyncpg.create_pool(
        user="postgres", password="secret",
        database="dogetipbot", host="localhost"
    )
    print(f"Logged in as {bot.user}")

4.2 Registering a User (Automatic)

When a user first interacts, we need to create a record and generate a deposit address for them.

async def get_or_create_user(user_id, platform='discord'):
    async with db_pool.acquire() as conn:
        row = await conn.fetchrow(
            "SELECT * FROM users WHERE platform=$1 AND platform_user_id=$2",
            platform, str(user_id)
        )
        if row:
            return row
        # Generate a new deposit address via Dogecoin RPC
        addr = await generate_deposit_address()
        await conn.execute(
            "INSERT INTO users (platform, platform_user_id, deposit_address) VALUES ($1, $2, $3)",
            platform, str(user_id), addr
        )
        return await conn.fetchrow(
            "SELECT * FROM users WHERE platform=$1 AND platform_user_id=$2",
            platform, str(user_id)
        )

The generate_deposit_address function calls your Dogecoin node RPC:

async def generate_deposit_address():
    async with aiohttp.ClientSession() as session:
        payload = {
            "jsonrpc": "1.0",
            "method": "getnewaddress",
            "params": ["tipbot", "legacy"],
            "id": 1
        }
        auth = aiohttp.BasicAuth('rpcuser', 'rpcpassword')
        async with session.post('http://127.0.0.1:22555', json=payload, auth=auth) as resp:
            data = await resp.json()
            return data['result']

4.3 The Tip Command

@bot.command(name='tip')
async def tip_cmd(ctx, member: discord.Member, amount: float):
    if amount <= 0:
        await ctx.send("Amount must be positive.")
        return
    sender_id = ctx.author.id
    receiver_id = member.id
    if sender_id == receiver_id:
        await ctx.send("You cannot tip yourself.")
        return

    sender = await get_or_create_user(sender_id)
    receiver = await get_or_create_user(receiver_id)

    async with db_pool.acquire() as conn:
        async with conn.transaction():
            # Check sender balance
            balance = sender['balance']
            if balance < Decimal(str(amount)):
                await ctx.send(f"Insufficient balance. You have {balance:.2f} DOGE.")
                return
            # Deduct from sender, add to receiver
            await conn.execute(
                "UPDATE users SET balance = balance - $1 WHERE id = $2",
                Decimal(str(amount)), sender['id']
            )
            await conn.execute(
                "UPDATE users SET balance = balance + $1 WHERE id = $2",
                Decimal(str(amount)), receiver['id']
            )
            # Log transactions
            await conn.execute(
                "INSERT INTO transactions (user_id, type, amount, counterparty_user_id) VALUES ($1, 'tip_sent', $2, $3)",
                sender['id'], Decimal(str(amount)), receiver['id']
            )
            await conn.execute(
                "INSERT INTO transactions (user_id, type, amount, counterparty_user_id) VALUES ($1, 'tip_received', $2, $3)",
                receiver['id'], Decimal(str(amount)), sender['id']
            )
    await ctx.send(f"{ctx.author.mention} tipped {member.mention} **{amount} DOGE**! Much wow!")

4.4 Balance Command

@bot.command(name='balance')
async def balance_cmd(ctx):
    user = await get_or_create_user(ctx.author.id)
    await ctx.send(f"{ctx.author.mention}, your balance is **{user['balance']:.2f} DOGE**.")

4.5 Deposit Command

@bot.command(name='deposit')
async def deposit_cmd(ctx):
    user = await get_or_create_user(ctx.author.id)
    addr = user['deposit_address']
    await ctx.send(f"Send DOGE to this address to fund your tip wallet:\n`{addr}`\n"
                   "Funds will appear after 1 confirmation (~1 minute).")

4.6 Withdrawal Command

@bot.command(name='withdraw')
async def withdraw_cmd(ctx, amount: float, destination: str):
    user = await get_or_create_user(ctx.author.id)
    if amount <= 0:
        await ctx.send("Amount must be positive.")
        return
    if amount < 1:   # optional minimum
        await ctx.send("Minimum withdrawal is 1 DOGE.")
        return

    async with db_pool.acquire() as conn:
        async with conn.transaction():
            balance = user['balance']
            if balance < Decimal(str(amount)):
                await ctx.send(f"Insufficient balance. You have {balance:.2f} DOGE.")
                return
            # Deduct balance immediately to prevent double withdrawal
            await conn.execute(
                "UPDATE users SET balance = balance - $1 WHERE id = $2",
                Decimal(str(amount)), user['id']
            )
            # Create pending withdrawal record
            withdraw_id = await conn.fetchval(
                "INSERT INTO pending_withdrawals (user_id, amount, destination_address) VALUES ($1, $2, $3) RETURNING id",
                user['id'], Decimal(str(amount)), destination
            )
    # Now process the on‑chain transaction (asynchronously)
    await process_withdrawal(withdraw_id, amount, destination, ctx, user)

The process_withdrawal function should run in a background task (e.g., using asyncio.create_task). It calls the Dogecoin RPC sendtoaddress:

async def process_withdrawal(withdraw_id, amount, dest, ctx, user):
    try:
        async with aiohttp.ClientSession() as session:
            payload = {
                "jsonrpc": "1.0",
                "method": "sendtoaddress",
                "params": [dest, amount],
                "id": 1
            }
            auth = aiohttp.BasicAuth('rpcuser', 'rpcpassword')
            async with session.post('http://127.0.0.1:22555', json=payload, auth=auth) as resp:
                data = await resp.json()
                if 'error' in data and data['error']:
                    raise Exception(data['error']['message'])
                txid = data['result']
        # Update pending record
        async with db_pool.acquire() as conn:
            await conn.execute(
                "UPDATE pending_withdrawals SET status='processed', txid=$1 WHERE id=$2",
                txid, withdraw_id
            )
            await conn.execute(
                "INSERT INTO transactions (user_id, type, amount, txid) VALUES ($1, 'withdraw', $2, $3)",
                user['id'], Decimal(str(amount)), txid
            )
        await ctx.send(f"Withdrawal of {amount} DOGE sent to `{dest}`\nTXID: {txid}")
    except Exception as e:
        # On failure, refund the balance
        async with db_pool.acquire() as conn:
            await conn.execute(
                "UPDATE users SET balance = balance + $1 WHERE id = $2",
                Decimal(str(amount)), user['id']
            )
            await conn.execute(
                "UPDATE pending_withdrawals SET status='failed' WHERE id=$1", withdraw_id
            )
        await ctx.send(f"Withdrawal failed: {str(e)}. Funds have been refunded to your tip balance.")

4.7 Scanning for Deposits (Background Task)

To credit users when they send DOGE to their deposit address, run a periodic task that queries the Dogecoin node for new transactions to known addresses.

async def scan_deposits():
    await bot.wait_until_ready()
    while not bot.is_closed():
        async with db_pool.acquire() as conn:
            # Get all users with deposit addresses
            rows = await conn.fetch("SELECT id, platform_user_id, deposit_address FROM users WHERE deposit_address IS NOT NULL")
            for row in rows:
                addr = row['deposit_address']
                # Use RPC `listreceivedbyaddress` or `getreceivedbyaddress`
                received = await get_received_by_address(addr)
                if received > 0:
                    # Check if we already processed this amount (we need a more robust method using txids)
                    # For simplicity, we assume each deposit is a single transaction.
                    # Better: track last block height and scan new transactions.
                    await conn.execute(
                        "UPDATE users SET balance = balance + $1 WHERE id = $2",
                        Decimal(str(received)), row['id']
                    )
                    # Insert deposit transaction record
                    await conn.execute(
                        "INSERT INTO transactions (user_id, type, amount) VALUES ($1, 'deposit', $2)",
                        row['id'], Decimal(str(received))
                    )
        await asyncio.sleep(60)  # scan every minute

The get_received_by_address function:

async def get_received_by_address(address):
    async with aiohttp.ClientSession() as session:
        payload = {
            "jsonrpc": "1.0",
            "method": "getreceivedbyaddress",
            "params": [address, 1],  # 1 confirmation required
            "id": 1
        }
        auth = aiohttp.BasicAuth('rpcuser', 'rpcpassword')
        async with session.post('http://127.0.0.1:22555', json=payload, auth=auth) as resp:
            data = await resp.json()
            return data['result']

Start the background task in on_ready:

bot.loop.create_task(scan_deposits())

5. Security and Rate Limiting

5.1 Race Conditions

The tip command updates two rows and deducts balance. To prevent double‑spending, we use an SQL transaction (as shown). In PostgreSQL, UPDATE locks the row, so concurrent tips from the same user will be serialized.

For higher throughput, consider using Redis WATCH or optimistic locking, but for most Discord bots, database transactions are sufficient.

5.2 Withdrawal Double‑Spend Prevention

In the withdrawal flow, we deduct the balance before broadcasting the transaction. If the RPC call fails, we refund the balance. This ensures that even if the bot crashes between deduction and broadcast, the user cannot withdraw the same funds twice.

5.3 Rate Limiting User Commands

Prevent abuse by adding a cooldown:

from discord.ext import commands

@bot.command(name='tip')
@commands.cooldown(1, 5, commands.BucketType.user)
async def tip_cmd(ctx, member: discord.Member, amount: float):
    ...

You can also implement server‑wide limits to avoid spam.

5.4 Private Key Protection

The Dogecoin node’s wallet contains the master private keys for all deposit addresses. Never expose the RPC credentials. Use environment variables and run the bot on a secure VPS. For large balances, consider a multi‑sig setup or a separate “hot wallet” that only holds the tipping bot’s float.

5.5 Input Validation

Always validate amount to be a positive number and not exceed user balance. For withdrawals, validate the destination address format (starts with D or A for mainnet). Use a regex: ^[DA][a-km-zA-HJ-NP-Z1-9]{25,34}$.

For a production‑grade bot, you may want to run a dedicated Dogecoin node on a Raspberry Pi to avoid relying on third‑party APIs. See How to Build a Dedicated Dogecoin Node on a Raspberry Pi for Under $50.


6. Extending to Telegram

The logic is identical; only the API changes. Use python-telegram-bot v20+ with async handlers. Here is a minimal example:

from telegram import Update
from telegram.ext import Application, CommandHandler, ContextTypes

async def tip(update: Update, context: ContextTypes.DEFAULT_TYPE):
    args = context.args
    if len(args) < 2:
        await update.message.reply_text("Usage: /tip @username amount")
        return
    target = args[0]  # simple; you'd parse mention properly
    amount = float(args[1])
    # ... same database logic ...
    await update.message.reply_text(f"Tipped {target} {amount} DOGE!")

app = Application.builder().token("YOUR_TOKEN").build()
app.add_handler(CommandHandler("tip", tip))
app.run_polling()

Telegram user IDs are integers; you can get them from update.effective_user.id.


7. Testing and Deployment

  1. Run on testnet first. Set up a Dogecoin testnet node (change RPC port and address prefix). Use testnet DOGE from faucets.
  2. Use environment variables for database credentials, RPC password, and bot tokens.
  3. Log all commands for auditing. Store user actions in a separate log table.
  4. Monitor your node’s disk space. The blockchain grows; prune your node or increase storage.

Once stable, announce your bot to the community. Encourage users to tip small amounts initially.


8. Conclusion

Building a Dogecoin tipping bot is an excellent way to learn Web3 backend engineering while contributing to the Shibe ecosystem. You’ve implemented a custodial bot that allows instant, feeless tipping, automatic deposit detection, and secure withdrawals.

The code you write can be open‑sourced to help other communities. Share it on GitHub, document it well, and watch the “much wow” messages roll in.

Remember: with custodial power comes responsibility. Never hold more DOGE in the bot’s wallet than you are willing to lose, and consider regular audits or moving large balances to cold storage.

Now go forth and spread the tipping culture—one !tip at a time.

🔒 If you plan to store the bot’s reserve funds long‑term, secure them with a hardware wallet. See our Best Dogecoin Wallets in 2026 guide.

Not financial or security advice. This tutorial is for educational purposes. Always test thoroughly and consult a security professional for production deployments.

Leave a Comment