paint-brush
How to Build an On-Chain Pay-Per-View Protocol on Rootstockby@nescampos
380 reads
380 reads

How to Build an On-Chain Pay-Per-View Protocol on Rootstock

by Néstor CamposNovember 30th, 2024
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

Monetizado is an on-chain pay-per-view protocol. It allows you to monetize any web page and static content through Web3.0. It can be used to protect pages so that only subscribers can see it.
featured image - How to Build an On-Chain Pay-Per-View Protocol on Rootstock
Néstor Campos HackerNoon profile picture

In the attention economy (Web 2.0), users are "pushed" to pay for subscriptions to access exclusive content (all kinds of content, including adult content), but nothing ensures that you get the benefits you are paying for, either because the content creator does not continue to create new things or you have to subscribe for extended periods when you only want to pay for what you consume.


However, Web 3.0, the intention economy, brings the opportunity for users to pay only for what they consume, instead of for extended periods, through micropayments. In turn, it incentivizes content creators to stay updated with more creations, receiving their income immediately, with very low costs, without depending on centralized gateways.

Monetizado, An On-Chain Pay-Per-View Protocol

Monetizado is an on-chain pay-per-view platform that allows you to monetize any web page and static content (if you don't have access to the backend to make changes) through Web3.


You can implement Monetizado on news sites, social networks, exclusive content portals, and more. You could also use it to incentivize users to pay and not see advertising on your sites.

Features

Monetizado allows you to:

  • Specify protected content with a specific amount that users must pay to access.
  • Review the protected content you have created.
  • To your followers/users, pay to see your content.
  • Check if a user has access to your content.
  • Change the cost of access to content if required.
  • Unprotect the content (if you want to release it to everyone for some time).
  • Withdraw the money collected for your content.

Use Case

You can use monetized to protect pages so that only subscribers can see it, as in:

  • News portals.
  • Videos.
  • Audios.
  • Files
  • Blogs.
  • Social networks.
  • And much more.

Development Stack

To build this platform, we used:


  • Solidity, for the smart contract
  • Remix, to deploy the smart contract
  • Rootstock testnet, as the network for the project
  • Javascript, to create a SDK and integrate with websites.


Our smart contract is pretty basic, allowing you to specify a name of content to be monetized, an amount (in rBTC), and several functions to enable/disable content, and pay and receive payments, among others.


MonetizadoLibrary.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

library MonetizadoLibrary {
    struct ProtectedContent {
        string name;
        uint256 accessCost;
        bool isProtected;
        uint256 sequenceId;
        address creator;
        uint256 amountAvailable;
        uint256 amountCollected;
        mapping(address => Subscriber) subscribers;
    }

    struct Subscriber {
        bool paid;
        uint256 amount;
    }
}


Monetizadov1.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./MonetizadoLibrary.sol";

contract Monetizadov1 {
    
    // Struct with info about the protected content (for paying to access)
    struct ProtectedContentInfo {
        string name;
        uint256 accessCost;
        bool isProtected;
        uint256 sequenceId;
        address creator;
        uint256 amountAvailable;
        uint256 amountCollected;
    }


    mapping(address => bool) private creators;
    mapping(address => bool) public hosting;

    mapping(address => MonetizadoLibrary.ProtectedContent[]) private paginasProtegidas;

    event GrantedAccess(address usuario, address creator, uint256 sequenceId);

    address private _owner;
    uint256 private _platformFeePercentage;
    uint256 private _platformBalance;

    modifier onlyOwner() {
        require(msg.sender == _owner, "Only the owner can call this function");
        _;
    }

    constructor() {
        _owner = msg.sender;
        _platformFeePercentage = 0;
        _platformBalance = 0;
    }


    function addProtectedContent(string memory name, uint256 accessCost) public returns (uint256) {
        
        uint256 cantidadPaginasCreador = paginasProtegidas[msg.sender].length;
        MonetizadoLibrary.ProtectedContent[] storage paginas = paginasProtegidas[msg.sender];
        MonetizadoLibrary.ProtectedContent storage pagina = paginas.push();
        pagina.name = name;
        pagina.accessCost = accessCost;
        pagina.isProtected = true;
        pagina.sequenceId = cantidadPaginasCreador;
        pagina.creator = msg.sender;
        pagina.amountCollected = 0;
        pagina.amountAvailable = 0;

        creators[msg.sender] = true;

        return cantidadPaginasCreador;
    }

    function getProtectedContentsForCurrentUser() public view returns (ProtectedContentInfo[] memory) {
        uint256 cantidadPaginasPorCreador = paginasProtegidas[msg.sender].length;
        ProtectedContentInfo[] memory paginas = new ProtectedContentInfo[](cantidadPaginasPorCreador);

        for (uint256 i = 0; i < cantidadPaginasPorCreador; i++) {
            MonetizadoLibrary.ProtectedContent storage pagina = paginasProtegidas[msg.sender][i];
            paginas[i] = ProtectedContentInfo(pagina.name, pagina.accessCost, pagina.isProtected, pagina.sequenceId, pagina.creator, pagina.amountAvailable, pagina.amountCollected);
        }

        return paginas;
    }

    function getProtectedContentByAddressAndId(address creator, uint256 sequenceId) public view returns (ProtectedContentInfo memory) {
        MonetizadoLibrary.ProtectedContent storage pagina = paginasProtegidas[creator][sequenceId];
        ProtectedContentInfo memory paginas = ProtectedContentInfo(pagina.name, pagina.accessCost, pagina.isProtected, pagina.sequenceId, pagina.creator, pagina.amountAvailable, pagina.amountCollected);
        return paginas;
    }

    function payAccess(address creator, uint256 sequenceId) external payable {

        MonetizadoLibrary.ProtectedContent storage pagina = paginasProtegidas[creator][sequenceId];
        require(msg.value == pagina.accessCost, "Incorrect payment amount");
        require(pagina.isProtected == true, "The page is not protected and you do not need to pay access");

        MonetizadoLibrary.Subscriber storage subscriber = pagina.subscribers[msg.sender];
        subscriber.paid = true;
        subscriber.amount = msg.value;
        pagina.amountCollected += msg.value;
        pagina.amountAvailable += msg.value;
        emit GrantedAccess(msg.sender, creator, sequenceId);
    }

    function currentUserHasAccess(address creator, uint256 sequenceId) public view returns(bool) {
        MonetizadoLibrary.ProtectedContent storage pagina = paginasProtegidas[creator][sequenceId];
        return pagina.subscribers[msg.sender].paid;
    }

    function changeAccessCost(uint256 sequenceId, uint256 newCost) external {
        MonetizadoLibrary.ProtectedContent storage pagina = paginasProtegidas[msg.sender][sequenceId];
        pagina.accessCost = newCost;
    }

    function unprotectContent(uint256 sequenceId) external {
        MonetizadoLibrary.ProtectedContent storage pagina = paginasProtegidas[msg.sender][sequenceId];
        pagina.isProtected = false;
    }

    function protectContent(uint256 sequenceId) external {
        MonetizadoLibrary.ProtectedContent storage pagina = paginasProtegidas[msg.sender][sequenceId];
        pagina.isProtected = true;
    }

    function changePlatformFee(uint256 feePlatform) external onlyOwner {
        require(feePlatform <= 100, "The fee should be between 0.01 to 1% (1 - 100 in a 10000 scale)");
        _platformFeePercentage = feePlatform;
    }

    function withdrawMoneyFromContent(uint256 sequenceId,uint256 amount) external {
        MonetizadoLibrary.ProtectedContent storage pagina = paginasProtegidas[msg.sender][sequenceId];
        require(pagina.amountAvailable >= amount, "Insufficient balance");
        uint256 amountForPlatform = amount * _platformFeePercentage / 10000;
        _platformBalance += amountForPlatform;
        payable(_owner).transfer(amountForPlatform);
        payable(msg.sender).transfer(amount - amountForPlatform);
        pagina.amountAvailable -= amount;
    }

    function getPlatformFee() public view returns(uint256) {
        return _platformFeePercentage;
    }

    function getPlatformBalance() public view returns(uint256) {
        return _platformBalance;
    }

    function withdrawMoneyPlatform(uint256 amount) external onlyOwner {
        require(_platformBalance >= amount, "Insufficient balance");
        payable(msg.sender).transfer(amount);
        _platformBalance -= amount;
    }

}


Additionally, we created a Javascript SDK that allows the use of the smart contract across websites, especially for users who want to pay to access specific content.

How to Use

  1. On the page you want to monetize, import Web3.JS and Ethers.JS. You can do it from CDN, for example:
<script src="https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js"></script>
<script src="https://cdn.ethers.io/lib/ethers-5.2.umd.min.js" type="application/javascript"></script>


  1. Download and import monetizadov1.js
<script src="./monetizado.js"></script>
  1. Create the protected content using the smart contract or in the Manager.


  2. Add a link tag in the head of the HTML code of your page, with the attribute "rel" with the value "monetized" and in href it follows the following structure:


  • network: It is the network where the content has been protected.


  • creator_address: It is the address (0x..) of the content creator. It can be your address if you are the creator.


  • sequence_id: It is the ID that the contract was terminated when you specified the new protected content (starts from 0 onwards, it is numeric).


For example:

<link rel="monetizado" href="rootstock:testnet://0xda3ec0b8bddd2e8bdedede3333fbaf938fcc18c5/0" />


The previous example means that the content is protected (by Id 0), by the creator (0xda3ec0b8bddd2e8bdedede3333fbaf938fcc18c5 for example), and that combination is the payment must be made to unlock it.


  1. Use the window.monetizado property (instructions here).


Project Walkthrough

This project is already deployed on Rootstock testnet, and you can explore the various links below, including repositories and a demo:

  1. Smart contract instructions: https://github.com/Monetizado/Contracts
  2. Javascript SDK: https://github.com/Monetizado/monetizadojs
  3. Monetizado Proxy SDK (to allow monetizing full pages using an intermediate page to pay and verify): https://github.com/Monetizado/proxyjs
  4. Monetizado Manager (to manage your content, collect the money, and more without interacting with the smart contract): https://monetizado.github.io/manager/
  5. Demo with Rootstock: <https://monetizado.github.io/demosmonetizado/demo_rootstock.html ](https://monetizado.github.io/demosmonetizado/demo_rootstock.html)
  6. Video demo:


Challenges and Solutions

Specifically for this idea, we set ourselves the challenge of being able to monetize Web 2.0 platforms using Web3, without a major impact on both the content creator and the user.


Therefore, we had to create the SDK in Javascript that would allow monetizing content with few changes to the website, since it is very likely that most content creators would have difficulties editing their websites (or they use external platforms where they do not have much space to edit).


With that solution, we then had to see how to identify the content that was being monetized, and there we took advantage of the HTML link tag to specify that content, and the SDK would detect the amount in rBTC, if the user had already paid and had access, or had to pay.

Conclusion

Rootstock represents a huge opportunity in BitcoinFi, taking advantage of its capabilities and robustness as a decentralized network. In this context, to make micropayments and connect it with the creation economy.


This example, Monetized, shows the simplicity of using Web3 on Web2 platforms, where there is a huge space for all types of users, reducing the friction to integrate these technologies.