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:
1
import discord
2
client_id = 770584244094369803
3
token = "NzcwNTg0MjQ0MDk0MzY5ODAz.X5fsgA.H_Y6xXSYKP-5SSB5okYjhI9AF60"
4
permissions = 67584
5
6
client = discord.Client()
7
8
@client.event
9
async def on_ready():
10
invite_url = f"https://discordapp.com/oauth2/authorize?client_id={client_id}&scope=bot&permissions={permissions}"
11
12
print("This bot can be invited to server by visiting {invite_url}"
13
print("Discord bot is online")
14
15
@client.event
16
async def on_message(message):
17
print(f"Received message: {message.content}")
18
19
client.run(token)
Copied!
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.
1
@client.event
2
async def on_message(message):
3
4
if "!btctip" not in message.content:
5
return
6
7
server_id = message.guild.id
8
server_name = message.guild.name
9
member_id = message.author.id
10
member_name = message.author.name
11
12
print(f"\n-> message: {message.content} |||| server_id: {server_id} | server_name: {server_name} | member_id: {member_id} | member_name: {member_name}\n")
13
14
15
if "!btctip show my balance" in message.content.lower():
16
await message.channel.send("<show balance>")
17
return
18
19
elif "!btctip deposit" in message.content.lower():
20
depositor_id = member_id
21
depositor_name = member_name
22
23
words = message.content.lower().split(' ')
24
25
for i in range(len(words)):
26
if words[i] == "!btctip" and words[i+1] == "deposit":
27
amount = words[i+2]
28
break
29
30
await message.channel.send(f"deposit request by {depositor_id} for {amount}")
Copied!
  • 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:
1
client = MongoClient("mongodb://localhost:27017")
2
db = client['btc-discord-bot'] # DB reference
3
4
def check(self, server_id, server_name, member_id, member_name):
5
server_id = str(server_id)
6
member_id = str(member_id)
7
8
# check if server is new
9
servers_dbcol = db['servers']
10
if servers_dbcol.find_one({'server_id': server_id}) == None:
11
# insert server to collection
12
print(f"adding new server: {server_id}\n")
13
14
# data to be inserted
15
server = {'server_id': server_id, 'name': server_name}
16
17
# adds data to collection
18
servers_dbcol.insert_one(server)
19
20
# check if user is new
21
users_dbcol = db['users']
22
if users_dbcol.find_one({'user_id': member_id}) == None:
23
# insert user to collection
24
print(f"adding new user: {member_id}\n")
25
26
# data to be inserted
27
new_wallet = create_wallet("discord-bot-user-"+member_id)
28
user = {'user_id': member_id, 'user_name': member_name, 'wallet': new_wallet}
29
30
# insert user
31
users_dbcol.insert_one(user)
Copied!
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:
1
def create_wallet(name):
2
wallet_params = {
3
'user_label': name
4
}
5
new_wallet = lnpay_py.create_wallet(wallet_params)
6
return new_wallet
Copied!
Database entry for new_wallet is a JSON like this:
1
example_value_of_new_wallet = {
2
'id': 'wal_',
3
'created_at': 1611472924,
4
'updated_at': 1611472924,
5
'user_label': 'test-wallet-created-from-lnpay-py',
6
'balance': 0,
7
'statusType': {
8
'type': 'wallet',
9
'name': 'active',
10
'display_name': 'Active'
11
},
12
'access_keys': {
13
'Wallet Admin': ['waka_'],
14
'Wallet Invoice': ['waki_'],
15
'Wallet Read': ['wakr_']
16
}
17
}
Copied!
Similarly I've created functions to get user balance:
1
def get_balance(self, user_id):
2
user_id = str(user_id)
3
wallet_key = db['users'].find_one({'user_id': user_id})['wallet']["access_keys"]["Wallet Read"][0]
4
5
wallet = LNPayWallet(wallet_key)
6
info = wallet.get_info()
7
8
return info['balance']
Copied!
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
1
pip install lnpay-py
Copied!
It's documentation is available at https://github.com/lnpay/lnpay-py.
You can then initialize the API this way:
1
import lnpay_py
2
3
lnpay_api_key = "sak_.." # Your secret API key
4
5
lnpay_py.initialize(lnpay_api_key)
Copied!

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.
1
def withdraw_pay_invoice(withdrawer_id, payreq):
2
withdrawer_id = str(withdrawer_id)
3
4
# transfer
5
user_wallet_admin_key = db['users'].find_one({'user_id': withdrawer_id})['wallet']["access_keys"]["Wallet Admin"][0]
6
withdrawer_wallet = LNPayWallet(user_wallet_admin_key)
7
invoice_params = {
8
'payment_request': payreq,
9
'passThru': {'app': 'discord-bot'}
10
}
11
pay_result = withdrawer_wallet.pay_invoice(invoice_params)
12
13
settled = False
14
15
if 'lnTx' in pay_result.keys():
16
if pay_result['lnTx']['settled'] == 1:
17
settled = True
18
return "sats withdrawn"
19
20
return "payment unsuccessful."
Copied!
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.
1
@client.event
2
async def on_message(message):
3
4
server_id = message.guild.id
5
server_name = message.guild.name
6
member_id = message.author.id
7
member_name = message.author.name
8
9
if "!btctip" not in message.content:
10
return
11
12
check(server_id, server_name, member_id, member_name)
13
14
# Read withdraw message here
15
if "!btctip withdraw" in message.content.lower():
16
try:
17
18
# Sender is withdrawer
19
withdrawer_id = member_id
20
withdrawer_name = member_name
21
22
# extracting lightning invoice
23
words = message.content.lower().split(' ')
24
for i in range(len(words)):
25
if words[i] == "!btctip" and words[i+1] == "withdraw":
26
pay_req = words[i+2]
27
break
28
29
# call withdraw_pay_invoice() to do the transaction
30
result = withdraw_pay_invoice(withdrawer_id, pay_req)
31
32
await message.channel.send(result)
33
34
except ValueError as e:
35
print(e)
36
await message.channel.send("ay check again")
37
return
38
Copied!

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.
1
def deposit_get_payreq(depositor_id, amount):
2
3
depositor_id = str(depositor_id)
4
amount = int(amount)
5
6
user_invoice_key = db['users'].find_one({'user_id': depositor_id})['wallet']["access_keys"]["Wallet Invoice"][0]
7
depositor_wallet = LNPayWallet(user_invoice_key)
8
invoice_params = {
9
'num_satoshis': amount,
10
'memo': 'depositing sats in discord',
11
'passThru': {'server_id': server_id, 'server_name': server_name, 'depositor_id': depositor_id, 'depositor_name': depositor_name, 'app': 'discord-bot'}
12
}
13
invoice = depositor_wallet.create_invoice(invoice_params)['payment_request']
14
return invoice
Copied!
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:
1
if words[i] == "!btctip":
2
recipient = words[i+1]
3
amount = words[i+2]
4
break
5
6
sender_id = member_id
7
sender_name = member_name
8
9
if '!' in recipient: # desktop client
10
recipient_id = int(recipient.split('!')[1][: -1])
11
else: # mobile client
12
recipient_id = int(recipient.split('@')[1][: -1])
13
14
recipient_name = await client.fetch_user(recipient_id)
15
recipient_name = str(recipient_name).split('#')[0]
Copied!
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.
1
sender_id = str(sender_id)
2
receiver_id = str(receiver_id)
3
amount = int(amount)
4
5
# transfer
6
sender_wallet_key = db['users'].find_one({'user_id': sender_id})['wallet']["access_keys"]["Wallet Admin"][0]
7
sender_wallet = LNPayWallet(sender_wallet_key)
8
receiver_wallet_id = self.db['users'].find_one({'user_id': receiver_id})['wallet']["id"]
9
10
transfer_params = {
11
'dest_wallet_id': receiver_wallet_id,
12
'num_satoshis': amount,
13
'memo': 'internal discord bot transfer'
14
}
15
transfer_result = sender_wallet.internal_transfer(transfer_params)
Copied!
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!
Last modified 1yr ago