Building a LAPP Series: Discord Tipping Bot

Welcome back to building a Lapp series. We'll be building a discord bot in this post. Discord is an instant messaging platform geared towards gamers. Discord has become popular among everyone.

1 - Overview

What we're building is a tipping bot. This bot is accessed via messages sent on your discord server. For example if you write !btctip @recipient 1000, the bot will tip 1000 satoshis from your balance to recipient's balance.

tipping @shan 1000 satoshis

The bot maintains balance of every person who uses the bot. It wouldn't really be bitcoin if you cannot deposit or withdraw. You can deposit bitcoin by sending !btctip deposit 1000 in the server's chat. The bot will return a lightning invoice which you can pay to increase your balance. You can then tip other people on the discord server.

This is great stuff for people to get started with bitcoin.

2 - LNPAY's role

Lnpay.co plays the responsibility of holding yours funds. It's a custodial service that wraps a merchant API around a bitcoin lightning node.

You can signup for LNPAY here https://lnpay.co/home/signup. Then create a wallet for the bot. This wallet is the treasury for the bot. Every deposit and withdraw go through it.

3 - Registering a Discord bot

A discord bot needs to be registered with discord. After that your bot can be invited to any server by visiting a URL.

First we need to register a bot in discord. For this go to https://discord.com/developers/applications and create on "New Application"

creating new discord bot

Click on "Create" to create an application. You'll be redirected to your application's page. Here you can configure it. As a developer you'll be needing some API keys for your bot which are available here.

discord app credentials

A bot is part of an application. To create the bot go to "Bot" tab and click on "Add Bot" under "Build-A-Bot"

creating a bot

After creating scroll down a select 2 permissions: send messages and read message history:

These permissions are represented by an integer. In our case it's 67584.

You have your bot created! For now it doesn't do anything.

You can invite your bot to your server by going to https://discordapp.com/oauth2/authorize?client_id={client_id}&scope=bot&permissions={permissions}

Here client_id = 770584244094369803 and permissions = 67584

4 - Building the bot

We'll be using python to program our bot. We'll be using discordpy for this.

Tech stack:

  • Infrastructure: Amazon AWS EC2 instance

  • Language: Python 3.7

  • Frameworks: Discordpy and Flask. (Flask for webhooks only)

  • Database: MongoDB

  • Bitcoin lightning node: lnpay.co

4.1 - Setting up python code

Create a file main.py as this. When this script is running your bot will be online and can operate:

import discord
client_id = 770584244094369803
token = "NzcwNTg0MjQ0MDk0MzY5ODAz.X5fsgA.H_Y6xXSYKP-5SSB5okYjhI9AF60"
permissions = 67584
client = discord.Client()
@client.event
async def on_ready():
invite_url = f"https://discordapp.com/oauth2/authorize?client_id={client_id}&scope=bot&permissions={permissions}"
print("This bot can be invited to server by visiting {invite_url}"
print("Discord bot is online")
@client.event
async def on_message(message):
print(f"Received message: {message.content}")
client.run(token)

And there you have it! You have discord bot running.

Now let's add some functionality to it. Everything our bot does is triggered by a message. That means all our logic will be inside on_message().

Generally discord bots have a trigger phrase. I have decided it to be "!btctip". We can define a set of triggers like:

  • !btctip deposit <amount>

  • !btctip <user> <amount>

  • !btctip withdraw <invoice>

  • !btctip show my balance

In code we check if these terms are in the message. Information such as amount, recipient, invoice has to be extracted from the message. Discordpy provides us other information such as server id, user id (person who sent the message), etc.

@client.event
async def on_message(message):
if "!btctip" not in message.content:
return
server_id = message.guild.id
server_name = message.guild.name
member_id = message.author.id
member_name = message.author.name
print(f"\n-> message: {message.content} |||| server_id: {server_id} | server_name: {server_name} | member_id: {member_id} | member_name: {member_name}\n")
if "!btctip show my balance" in message.content.lower():
await message.channel.send("<show balance>")
return
elif "!btctip deposit" in message.content.lower():
depositor_id = member_id
depositor_name = member_name
words = message.content.lower().split(' ')
for i in range(len(words)):
if words[i] == "!btctip" and words[i+1] == "deposit":
amount = words[i+2]
break
await message.channel.send(f"deposit request by {depositor_id} for {amount}")
  • message.content gives you the message that was sent.

  • message.guild tells you which server this message came from.

  • message.author tells you who sent the message.

  • await message.channel.send(reply) sends a reply back to where the message came from.

4.2 - Database

We're using mongoDB to store user data such as wallet keys. Each user has a separate lnpay wallet created for them. The access keys for those wallets are stored in our database. When we first start, we have no users. Since there is no sign up page, we have to check if a user is in our database and add them to the database in case they're not.

I am using a check function every time I receive a message. It's defined this way:

client = MongoClient("mongodb://localhost:27017")
db = client['btc-discord-bot'] # DB reference
def check(self, server_id, server_name, member_id, member_name):
server_id = str(server_id)
member_id = str(member_id)
# check if server is new
servers_dbcol = db['servers']
if servers_dbcol.find_one({'server_id': server_id}) == None:
# insert server to collection
print(f"adding new server: {server_id}\n")
# data to be inserted
server = {'server_id': server_id, 'name': server_name}
# adds data to collection
servers_dbcol.insert_one(server)
# check if user is new
users_dbcol = db['users']
if users_dbcol.find_one({'user_id': member_id}) == None:
# insert user to collection
print(f"adding new user: {member_id}\n")
# data to be inserted
new_wallet = create_wallet("discord-bot-user-"+member_id)
user = {'user_id': member_id, 'user_name': member_name, 'wallet': new_wallet}
# insert user
users_dbcol.insert_one(user)

At this point its good to read the MongoDB documentation. In the above code I'm taking server id, server name, user id, and user name and adding them to database if they're not in there already.

I am also creating a new lnpay wallet using create_wallet() for this new user and storging their wallet access keys in the database.

create_wallet() is using the lnpay-py API wrapper to create the user's wallet. It's defined this way:

def create_wallet(name):
wallet_params = {
'user_label': name
}
new_wallet = lnpay_py.create_wallet(wallet_params)
return new_wallet

Database entry for new_wallet is a JSON like this:

example_value_of_new_wallet = {
'id': 'wal_',
'created_at': 1611472924,
'updated_at': 1611472924,
'user_label': 'test-wallet-created-from-lnpay-py',
'balance': 0,
'statusType': {
'type': 'wallet',
'name': 'active',
'display_name': 'Active'
},
'access_keys': {
'Wallet Admin': ['waka_'],
'Wallet Invoice': ['waki_'],
'Wallet Read': ['wakr_']
}
}

Similarly I've created functions to get user balance:

def get_balance(self, user_id):
user_id = str(user_id)
wallet_key = db['users'].find_one({'user_id': user_id})['wallet']["access_keys"]["Wallet Read"][0]
wallet = LNPayWallet(wallet_key)
info = wallet.get_info()
return info['balance']

Take some time to read this code. It's really simple. The entire documentation for lnpay-py can be found here and here and here.

5 - Integrating with LNPAY

LNPAY's role is in withdrawing, depositing, transfering between wallets and connecting you to the Lightning Network. LNPAY has a python wrapper for its API which can be installed with

pip install lnpay-py

It's documentation is available at https://github.com/lnpay/lnpay-py.

You can then initialize the API this way:

import lnpay_py
lnpay_api_key = "sak_.." # Your secret API key
lnpay_py.initialize(lnpay_api_key)

5.1 - Withdrawing

Let's say you've tipped a friend 1000 satoshis with this bot. How will your friend be able to spend it if they're not able to withdraw? To do this your friend can send '!btctip withdraw <invoice>' in the chat. The bot will automatically pay that invoice.

def withdraw_pay_invoice(withdrawer_id, payreq):
withdrawer_id = str(withdrawer_id)
# transfer
user_wallet_admin_key = db['users'].find_one({'user_id': withdrawer_id})['wallet']["access_keys"]["Wallet Admin"][0]
withdrawer_wallet = LNPayWallet(user_wallet_admin_key)
invoice_params = {
'payment_request': payreq,
'passThru': {'app': 'discord-bot'}
}
pay_result = withdrawer_wallet.pay_invoice(invoice_params)
settled = False
if 'lnTx' in pay_result.keys():
if pay_result['lnTx']['settled'] == 1:
settled = True
return "sats withdrawn"
return "payment unsuccessful."

You get the response in pay_result. You can then check if it was successful by checking the 'settled' field of the response JSON. It's equal to 1 if transaction is successful.

The function withdraw_pay_invoice() can be called by reading for discord messages and triggering this function with extracted information.

@client.event
async def on_message(message):
server_id = message.guild.id
server_name = message.guild.name
member_id = message.author.id
member_name = message.author.name
if "!btctip" not in message.content:
return
check(server_id, server_name, member_id, member_name)
# Read withdraw message here
if "!btctip withdraw" in message.content.lower():
try:
# Sender is withdrawer
withdrawer_id = member_id
withdrawer_name = member_name
# extracting lightning invoice
words = message.content.lower().split(' ')
for i in range(len(words)):
if words[i] == "!btctip" and words[i+1] == "withdraw":
pay_req = words[i+2]
break
# call withdraw_pay_invoice() to do the transaction
result = withdraw_pay_invoice(withdrawer_id, pay_req)
await message.channel.send(result)
except ValueError as e:
print(e)
await message.channel.send("ay check again")
return

5.2 - Depositing

If you run out of satoshis to tip other people you can deposit more satoshis with '!btctip deposit 1000'. The bot will return a lightning invoice to your lnpay wallet which you can pay to increase its balance.

Similar to withdrawing, we can read the message, extract the amount and return a lightning invoice.

def deposit_get_payreq(depositor_id, amount):
depositor_id = str(depositor_id)
amount = int(amount)
user_invoice_key = db['users'].find_one({'user_id': depositor_id})['wallet']["access_keys"]["Wallet Invoice"][0]
depositor_wallet = LNPayWallet(user_invoice_key)
invoice_params = {
'num_satoshis': amount,
'memo': 'depositing sats in discord',
'passThru': {'server_id': server_id, 'server_name': server_name, 'depositor_id': depositor_id, 'depositor_name': depositor_name, 'app': 'discord-bot'}
}
invoice = depositor_wallet.create_invoice(invoice_params)['payment_request']
return invoice
bot returning an invoice for user to deposit

Once the user pays the invoice they will be able tip that from their lnpay wallet.

Note: This is an invoice to the user's lnpay wallet which was created when they first used the bot.

Since we're maintaining a separate lnpay wallet for each user we don't have to implement webhooks to listen for transactions and update balances on our database. All balances maintained in lnpay itself.

5.3 - Tipping

This is the core of what we're doing.

When tipping someone, the bot can trigger transaction from sender's wallet to receiver's wallet using lnpay's API and their wallet keys (which we have saved in our database). Since all wallets are created in lnpay, you only need to do an internal transfer.

A tip can initiated by

!btctip @recipient 20

This message has 3 parts.

  • !btctip : invoking command for our bot

  • @recipient : mentioning the recipient of this tip

  • 20 : the amount of satoshi you want to tip

In action it looks like this:

As this is a message, it'll come under our on_message() method.

Once we have the message, we can parse it to break down the data we need. In code it looks like this:

if words[i] == "!btctip":
recipient = words[i+1]
amount = words[i+2]
break
sender_id = member_id
sender_name = member_name
if '!' in recipient: # desktop client
recipient_id = int(recipient.split('!')[1][: -1])
else: # mobile client
recipient_id = int(recipient.split('@')[1][: -1])
recipient_name = await client.fetch_user(recipient_id)
recipient_name = str(recipient_name).split('#')[0]

Note: When you mention a person in discord you get their user id. Though when you mention someone from discord's desktop client you have a '!' in the string and '@' when mentioning from mobile client. I am not sure why this is the case but that's how discord has implemented it.

It's good to check data before you do the transaction, such as amount should be positive and user shouldn't be tipping themselves.

In order to do a the transaction you only need to:

  • Check the transaction is valid (user has enough balance)

  • get sender's wallet admin key

  • get receiver's wallet id

  • use lnpay-py to transfer amount from sender to receiver.

sender_id = str(sender_id)
receiver_id = str(receiver_id)
amount = int(amount)
# transfer
sender_wallet_key = db['users'].find_one({'user_id': sender_id})['wallet']["access_keys"]["Wallet Admin"][0]
sender_wallet = LNPayWallet(sender_wallet_key)
receiver_wallet_id = self.db['users'].find_one({'user_id': receiver_id})['wallet']["id"]
transfer_params = {
'dest_wallet_id': receiver_wallet_id,
'num_satoshis': amount,
'memo': 'internal discord bot transfer'
}
transfer_result = sender_wallet.internal_transfer(transfer_params)

documentation on transffering between wallet can be found here.

Wrapping up

Congratulations! You have made a tipping system that works on a global scale!

By now you should see the potential of this. You can use this same recipe and build a reddit or twitter bot. You can take the concepts you've learned and apply them to other areas.

The source code of my implementation is completely open-source at: https://github.com/ParmuSingh/bitcoin-tip-bot-discord

See ya on next post!