Collin Rukundo

I built a blogging platform powered by the Lightning Network

For the past couple of weeks, I have been working as a Fellow at Qala, a program training African developers on Bitcoin and Lightning. I have the golden opportunity to learn from an incredible A-team of engineers and trailblazers in the Bitcoin community.

Two weeks ago, we started working on individual projects that allow us to use our skills to build on the Bitcoin and Lightning network. In this article, I will showcase my first project: Paywalled.

Paywalled is a blogging platform that takes advantage of the lightning network to support content creators through bitcoin micropayments. It was inspired by Alex Bosworth's Y'alls.org

paywalled.png

I thought it would be fun to start by creating a use-case that shows the lightning network in all its glory as a solution for cheap, instant, high volume and very private payments. A simple blogging platform where users have to pay a few thousand satoshis before they can view, publish, edit or comment on content with instant settlement seemed like a good place to start.

On Paywalled, all transactions are public and are shown in a stream on the landing page as they come in. Payments for each article are also visible on the post page. Logged-in users have the option to remain anonymous and they are represented by a question mark which is the default for all users.

The Tech

The repo: https://github.com/crukundo/lnd-paywall. Please feel free to fork this and make the most of it.

I took some time to create a detailed README for anyone who wants to run this. This also assumes you have bitcoind and lnd setup and your nodes are running. For development, I used regtest. It was easier that way so I could focus on building the application instead of waiting for my local node to sync on testnet. To be honest, I also really enjoy mining blocks for fun. You can use whatever chain is convenient. You will need to configure two lightning nodes, one that you will link to Paywalled to generate payment requests among other functions and another to make payments against the generated invoices.

Paywalled is built on Django as a backend. The UI is entirely Bootstrap and it takes advantage of jQuery, vanilla JS for a few functions. I also used htmx for interactivity such as when checking for the status of a payment request.

Once you have setup your nodes, you can clone the repo and follow the rest of the instructions. There’s a sample .env file with the relevant environment variables this application expects. The default database is SQLite3 but you can set this up to work with a different engine like Postgres.

In your .env file, you will set the paths to your lnd node, as well as the tls.cert and admin.macaroon files in the lnd directory. There are a few more variables to set as you will find in the README. You can also set the network chain at this point. The default network is regtest. This is how you get Paywalled to talk to your local/remote node. This is abstracted by a gRPC client that attaches to your node and allows you to execute rpc functions:

lnrpc = lnd_grpc.Client(
    lnd_dir = settings.LND_FOLDER,
    macaroon_path = settings.LND_MACAROON_FILE,
    tls_cert_path = settings.LND_TLS_CERT_FILE,
    network = settings.LND_NETWORK,
)

For example, in order to generate a new invoice, our client sends an rpc request to the node through the function AddInvoice. We run this as a method on our Article model. We can then call article.generate_pub_invoice() in the view at any point when we need to generate a new invoice to publish an article. Here's the article model for reference.

class Article(models.Model):
    DRAFT = "D"
    PUBLISHED = "P"
    STATUS = ((DRAFT, _("Draft")), (PUBLISHED, _("Published")))

    uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False)
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        related_name="articles",
        on_delete=models.SET_NULL,
    )
    date_created = models.DateTimeField(auto_now_add=True)
    date_published = models.DateTimeField(auto_now=True)
    title = models.CharField(max_length=255, null=True, unique=True)
    status = models.CharField(max_length=1, choices=STATUS, default=DRAFT)
    content = models.CharField(_("Content"), max_length=40000, blank=True)
    edited = models.BooleanField(default=False)
    objects = ArticleQuerySet.as_manager()

    class Meta:
        verbose_name = _("Article")
        verbose_name_plural = _("Articles")
        ordering = ("-date_published",)

    def __str__(self):
        return self.title

    def get_absolute_url(self): 
        return reverse('articles:article', kwargs={ "article_uuid": self.uuid })

    def generate_pub_invoice(self):

        add_invoice_resp = lnrpc.add_invoice(value=settings.MIN_PUBLISH_AMOUNT, memo="Payment to Paywalled to publish article", expiry=settings.PUBLISH_INVOICE_EXPIRY)
        r_hash_base64 = codecs.encode(add_invoice_resp.r_hash, 'base64')
        r_hash = r_hash_base64.decode('utf-8')
        payment_request = add_invoice_resp.payment_request

        from apps.payments.models import Payment
        payment = Payment.objects.create(user=self.user, article=self, purpose=Payment.PUBLISH, satoshi_amount=settings.MIN_PUBLISH_AMOUNT, r_hash=r_hash, payment_request=payment_request, status='pending_payment')
        payment.save()

Paywalled also uses the LookupInvoice function to check the status of a payment request such as whether an invoice has been settled fully before releasing content to be published or viewed. The check_payment request polls every 10seconds after a payment request has been generated. Once payment is confirmed, it updates the Payment object in the database with the user_id (if the user is logged in) or with a session_key. This ensures this user will always have access to this article without having to pay again. It also return a HTTP response code 286 to stop polling with confirmation of payment. This is the structure of the request.

def check_payment(request, pk):
    """
    Checks if the Lightning payment has been received for this invoice
    """
    # get the payment in question
    payment = Payment.objects.get(pk=pk)

    r_hash_base64 = payment.r_hash.encode('utf-8')
    r_hash_bytes = codecs.decode(r_hash_base64, 'base64')
    invoice_resp = lnrpc.lookup_invoice(r_hash=r_hash_bytes)


    if request.htmx:
        if invoice_resp.settled:
            # create session key
            if not request.session.session_key:
                request.session.create()
            # Payment complete
            payment.status = 'complete'
            if request.user.is_authenticated:
                payment.user = request.user
                payment.session_key = request.session.session_key
            else:
                # if user is anon, save in session
                payment.session_key = request.session.session_key
            payment.save()
            return HttpResponseStopPolling("<div id='paymentStatus' data-status='paid' class='alert alert-success' role='alert'>Payment confirmed. Thank you</div>")
        else:
            # Payment not received
            return HttpResponse("<div id='paymentStatus' data-status='pending' class='alert alert-warning' role='alert'>Invoice payment is still pending. Will check again in 10s</div>")

How does Paywalled work?

Anyone can create an account and publish their articles. In order to publish, you need to pay a small fee. The publish button is disabled until payment to the generated invoice has been made.

Paywalled-publish.png

Once published, a blog can be viewed by everyone. However, the content is protected by a paywall which prevents anyone from reading beyond the first few lines until a payment has been made to the generated invoice. The platform polls the check_payment function until payment is confirmed.

Screenshot 2022-04-25 at 23.41.26.png

Paying a lightning invoice

If this platform was running on a remote node on mainnet, you could use a lightning enabled wallet like Phoenix, Muun or Breez. Paywalled comes with a few bells and whistles. There's a 'toolbar' where you can copy the invoice to your device clipboard, encode it into a QR code, or open your lightning-compatible wallet.

Screenshot 2022-04-26 at 02.37.43.png

However, since you are testing in active development, you are probably running on regtest, so you should have mined some blocks and sent sufficient sats to your other node (remember the one I told you to setup at the beginning of this piece). Then use the lightning cli lncli in your terminal to pay the invoice. Like so:

lncli2 payinvoice lnbcrt21u1p3xd9mnpp5u76lr0nq56tqtx267rq4w5r3ehngvpjjfxjtedwsnge5pj54rnlqdpl2pshjmt9de6zqar0ypgxz7thv9kxcetyyp6x7grsw43xc6tndqsxzun5d93kcegcqzpgxqyjw5qsp5sx689kyh8q9s2yg9vua87fhp33nt5ye9eyhhx3e5wdran2tckshs9qyyssqx6yldt6qhfx6cqwxya3egs47ckulctc3z4j24lqzxced5vgjg8y4vgfwyh9k5zv3ut5wkm83ew0kespegn4q3yrklk8lrje0v2vxrcsptlkr53

This will decode the payment request and ask you to confirm the payment with the response:

Payment hash: e7b5f1be60a69605995af0c1575071cde686065249a4bcb5d09a3340ca951cfe
Description: Payment to Paywalled to publish article
Amount (in satoshis): 2100
Fee limit (in satoshis): 2100
Destination: 0353838a558ac5bd43392c86822ed8560251dc415579344ae8e5c6a890ff78dcdb
Confirm payment (yes/no): yes
+------------+--------------+--------------+--------------+-----+----------+----------+-------+
| HTLC_STATE | ATTEMPT_TIME | RESOLVE_TIME | RECEIVER_AMT | FEE | TIMELOCK | CHAN_OUT | ROUTE |
+------------+--------------+--------------+--------------+-----+----------+-----------------+----------------------+
| HTLC_STATE | ATTEMPT_TIME | RESOLVE_TIME | RECEIVER_AMT | FEE | TIMELOCK | CHAN_OUT        | ROUTE                |
+------------+--------------+--------------+--------------+-----+----------+----+------------+--------------+--------------+--------------+-----+----------+-----------------+----------------------+
| HTLC_STATE | ATTEMPT_TIME | RESOLVE_TIME | RECEIVER_AMT | FEE | TIMELOCK | CHAN_OUT        | ROUTE                |
+------------+--------------+--------------+--------------+-----+----------+-----------------+----------------------+
| SUCCEEDED  |        0.064 |        0.256 | 2100         | 0   |      167 | 126443837259776 | 0353838a558ac5bd4339 |
+------------+--------------+--------------+--------------+-----+----------+-----------------+----------------------+
Amount + fee:   2100 + 0 sat
Payment hash:   e7b5f1be60a69605995af0c1575071cde686065249a4bcb5d09a3340ca951cfe
Payment status: SUCCEEDED, preimage: 1cbda8237d56735cbc85b58e34240f6c91f5b24b3773c090338f05987bf7339e

Your payment is a success and you can now publish or view the article. The Lightning network (and content creator) thanks you.

Next steps

  • Payouts to content creators by opening channels with public keys to their node
  • Add a comment section where readers pay to share their thoughts on the blogs.

Learnings

  • Currently, for users to publish on the platform, they have to setup an account with a username and password. This doesn’t help privacy concerns but I needed a permanent way to track a content creator’s articles and their due rewards. django.contrib.sessions is great but it has it’s limitations. My research led me to LNURL-auth which creates a way to identify users by their node's public key. I hope to implement this but anyone can adopt this.

  • To build this project, I had to learn plenty about how the lightning network works - especially with regard to how payment requests are generated, their structure as well as how the lightning API works so I could programatically generate new invoices and look up the status of those payment requests. I also had to tinker with a few things on the front and backend to ensure a painless experience. This was two weeks of hard work but I enjoyed every bit of it because I like a challenge. I learned to push through the blocks, be willing to start over and keep a can-do attitude. Since coding is largely exponential, I can't wait to apply what I have learned here in my next bitcoin/lightning project.

Disclaimer - I acknowledge that some of the text content that appears in the screenshots was copied off a number of websites. It is the property of the respective owners. I do not claim any of it and it was purely used to test the platform.

Thoughts? Leave a comment

Comments
  1. Karan Bhatia — Apr 27, 2022:

    Great job friend!

  2. Peter — May 3, 2022:

    is live version available somewhere or i need to install on my server to use it?