DEV Community

Aniket
Aniket

Posted on • Edited on

Web Monetization for NPM packages!!

For a long time, I wanted to do some meaningful contribution to the community but was never able to do so. This hackathon gave me the perfect way for doing that, by creating a way to monetize NPM packages!

What I built

I built 2 npm packages

  1. monetize-npm-cli npm
  2. wrapper-coil-extension npm

monetize-npm-cli

Quoting its readme

monetize-npm-cli is a modular CLI that helps monetize npm packages using the Web Monetization API and different providers.

And that's exactly what it is

I built a CLI ( for the first time! ) that allows you to run your app inside a container like environment, which it doesn't necessarily know about if it does not go looking around.

node index.js => monetize-npm-cli index.js and you're good to go!

It finds the package.json for your main project and then goes searching inside the node_modules folder. Any package.json it finds there with the key webMonetization is picked up to be monetized

{
  "webMonetization": {
    "wallet": "$yourWalletAddressGoesHere"
  }
}
Enter fullscreen mode Exit fullscreen mode

Just adding this to package.json can allow packages to be monetized.


I wanted to keep the API as similar as possible to the one already existing for browsers, but some changes had to be made for the different environment.

document became globalThis along with the following changes

getState

document.monetization.state => globalThis.monetization.getState(name, version)

name and version are defined in package.json of each package.

Only the packages with webMonetization key in their package.json are accessible here.

addEventListener

There can be four listeners set up monetizationpending, monetizationstart, monetizationstop, monetizationprogress.

Let identify them by listenerIdentifier.

document.monetization.addEventListener(listenerIdentifier, foo) => globalThis.monetization.addEventListener(name, version, listenerIdentifier, foo)

removeEventListener

globalThis.monetization.removeEventListener(name, version, listenerIdentifier, foo)

If foo is not passed, all the listeners for that package are removed.


These methods can be used from wherever inside the application and the installed packages after checking whether globalThis.monetization exists, and can be used accordingly.

globalThis.monetization is itself a proxy of the actual object being used, to make it difficult to tamper with.


Remember the part where I said this CLI is modular? Well, that's because it can add and use many different providers easily with minimal changes!

That's where wrapper-coil-extension comes in


wrapper-coil-extension

Quoting its readme

wrapper-coil-extension is a wrapper around Coil's Web Monetization browser extension that allows it to be used from node.js.

Since I needed a provider to work with the CLI I had created, and none of the current ones had an API to achieve that, I had to instead figure out a way to make use of the already existing ones, so I built a wrapper around Coil's Extension that allows me to do so.

Since the extension doesn't currently support monetizing more than one tab at once,all the eligible packages are looped through and a webpage with their wallet is opened for some amount of time ( time can be defined by the user ). This allows payments to be sent to the respective package owners. Fixed in v0.0.7. Probabilistic revenue sharing is done where a package is selected randomly and monetized for 65 seconds each. This process is repeated until the app is closed.


Because Coil's Extension was not built for this kind of scenario, there are some things which do not work as expected everything is working as expected now, more can be seen here

Another problem that exists is that when a new tab opens and previous closes to monetize another package, chromium steals the focus. But since this is meant to be run in a production environment, this is really not an issue. Perfect bug=> feature situation XD Pointer is now changed dynamically in the same tab, thus fixing this problem.

Due to the modular nature of monetize-npm-cli, as more and more providers come up and provide different ways to monetize, their modules can be easily integrated with monetize-npm-cli with minimal changes. You can see how to create such module here.


How is this better than npm fund

You may have this question in your head ever since you opened this post. Well, we all have seen the npm fund prompt pop while installing any package that supports it. What most of us haven't done is try to run this command and go the links that are provided, after which you have to perform further digging to find out how to pay and support the developer, which makes for a bad experience, one that can make a person willing to pay opt-out.

Well, this changes that. The number of steps reduces to just installing this package globally, logging in to your provider only once, and just running the app using it.


Some other good changes this can bring

  1. Active development of more packages as developers are being paid for their hobbies.
  2. Careful installation of packages and prevention of installation of unnecessary packages.
  3. More thought on dependency cycle as if two not compatible enough versions of the same packages are listed as dependencies, they could end up being installed twice thus getting monetized twice.

Submission Category:

Here comes the hard part. Throughout the process of building my submission, I was trying to figure out which category it falls into, and I still can't put it into one

  1. Foundational Technology - It is a template for monetizing the web and is a plugin(?)
  2. Creative Catalyst - It is using the existing technologies to find ways to distribute and monetize content.
  3. Exciting Experiments - Web Monetization running outside the browser! You try telling me that's not an Exciting Experiment!

Demo

You can follow along with this demo by simply typing

npm install -g monetize-npm-cli
Enter fullscreen mode Exit fullscreen mode

First of all, let's check whether the package is installed properly

monetize-npm-cli -v
Enter fullscreen mode Exit fullscreen mode

Version image

Let's go to the help page

monetize-npm-cli -h
Enter fullscreen mode Exit fullscreen mode

Help Image

To monetize any package, we need to first login to our provider

monetize-npm-cli --login
Enter fullscreen mode Exit fullscreen mode

This will open up a browser window where you can use your credentials to login

Login Browser

On successful login, we will see this on our terminal

Login Terminal

For this demo, I have manually added webMonetization keys to various package.json of some npm packages.

Let's try listing those packages

monetize-npm-cli --list --expand
Enter fullscreen mode Exit fullscreen mode

You can expect to see something like this

List Terminal

Let's add some access to globalThis.monetization from the app which is being run inside the container

Added Snippet

Let's try running the app now

monetize-npm-cli index.js
Enter fullscreen mode Exit fullscreen mode

As soon as base64url starts getting paid

base64usl Paying

We can see the event we set fired up in the console

terminalOutput

Link to Code

monetize-npm-cli

GitHub logo projectescape / monetize-npm-cli

A CLI that helps monetize npm packages using the Web Monetization API

monetize-npm-cli

monetize-npm-cli is a modular CLI that helps monetize npm packages using the Web Monetization API and different providers.


Install

npm install -g monetize-npm-cli
Enter fullscreen mode Exit fullscreen mode

Usage

Run file

To run your app while monetizing the supported npm packages

monetize-npm-cli yourFile.js
Enter fullscreen mode Exit fullscreen mode

Help

To view help page with all details

monetize-npm-cli --help
Enter fullscreen mode Exit fullscreen mode

Login to your Provider

To login to your web monetization provider

monetize-npm-cli --login
Enter fullscreen mode Exit fullscreen mode

This will default to coil-extension if no provider is provided. See help for more details.


Logout from your Provider

To logout from your web monetization provider

monetize-npm-cli --logout
Enter fullscreen mode Exit fullscreen mode

This will default to coil-extension if no provider is provided. See help for more details.


List packages

To list all packages supporting web monetization

monetize-npm-cli --list
Enter fullscreen mode Exit fullscreen mode

Use help to get full list of supported commands


API

The aim of this CLI is to mimic the web monetization API given here as much as it could Instead of document.monetization, user…

wrapper-coil-extension

GitHub logo projectescape / wrapper-coil-extension

A wrapper for Coil's web monetization extension to make it run from node.js

wrapper-coil-extension

wrapper-coil-extension is a wrapper around Coil's Web Monetization browser extension that allows it to be used from node.js.


Install

npm install --save wrapper-coil-extension
Enter fullscreen mode Exit fullscreen mode

Usage

const { login, logout, monetize } = require("wrapper-coil-extension");

// To Login with your Coil Account

login();

// To Logout

logout();

// To start Monetization

monetize(monetizationPackages);
Enter fullscreen mode Exit fullscreen mode

timeout

(Depreciated)

Since v0.0.7, timeout is no longer used as instead of looping through packages, probablistic revenue sharing is being used.


monetizationPackages

monetizationPackages is an object of the type which is passed by monetize-npm-cli

// monetizationPackages
{
    packages:[
        {
          name: "",
          version: "",
          webMonetization: {
              wallet:""
          },
          state: "",
          monetizationpending: [],
          monetizationstart: [],
          monetizationstop: [],
          monetizationprogress: [],
        }
    ]
Enter fullscreen mode Exit fullscreen mode

How I built it

This submission was a lot of fun to build. Building a CLI and automating websites was completely new for me

monetize-npm-cli

I parsed the arguments with minimist and used kleur for logs.

fast-glob was used to find package.json while maintaining inter os compatibility.

The hard part here was designing the monetization object, as I had to deal with listeners, packages and their states, all while keeping some of the stuff private for globalThis.monetization and the object being passed to the provider module. After a lot of researching, I learned a lot about JS objects and came up with this

const monetization = (() => {
  let packages = [];
  const walletHash = {};
  const nameHash = {};

  return {
    get packages() {
      return packages;
    },
    set packages(val) {
      packages = val;
      val.forEach((p, index) => {
        if (walletHash[p.webMonetization.wallet] === undefined)
          walletHash[p.webMonetization.wallet] = [index];
        else walletHash[p.webMonetization.wallet].push(index);

        nameHash[`${p.name}@${p.version}`] = index;
      });
    },
    getState(name, version) {
      if (nameHash[`${name}@${version}`] !== undefined) {
        return packages[nameHash[`${name}@${version}`]].state;
      }
      console.log(`No package ${name}@${version} found\n`);
      return undefined;
    },
    addEventListener(name, version, listener, foo) {
      if (
        !(
          listener === "monetizationpending" ||
          listener === "monetizationstart" ||
          listener === "monetizationstop" ||
          listener === "monetizationprogress"
        )
      ) {
        console.log(`${listener} is not a valid event name\n`);
        return false;
      }
      if (nameHash[`${name}@${version}`] !== undefined) {
        packages[nameHash[`${name}@${version}`]][listener].push(foo);
        return true;
      }
      console.log(`No package ${name}@${version} found\n`);
      return false;
    },
    removeEventListener(name, version, listener, foo = undefined) {
      if (
        !(
          listener === "monetizationpending" ||
          listener === "monetizationstart" ||
          listener === "monetizationstop" ||
          listener === "monetizationprogress"
        )
      ) {
        console.log(`${listener} is not a valid event name\n`);
        return false;
      }
      if (nameHash[`${name}@${version}`] !== undefined) {
        if (!foo) {
          packages[nameHash[`${name}@${version}`]][listener] = [];
        } else {
          packages[nameHash[`${name}@${version}`]][listener] = packages[
            nameHash[`${name}@${version}`]
          ][listener].filter((found) => foo !== found);
        }
        return true;
      }
      console.log(`No package ${name}@${version} found\n`);
      return false;
    },
    invokeEventListener(data) {
      walletHash[data.detail.paymentPointer].forEach((index) => {
        packages[index].state =
          data.type === "monetizationstart" ||
          data.type === "monetizationprogress"
            ? "started"
            : data.type === "monetizationpending"
            ? "pending"
            : "stopped";
        packages[index][data.type].forEach((listener) => {
          listener(data);
        });
      });
    },
  };
})();
Enter fullscreen mode Exit fullscreen mode

globalThis.monetization was implemented using a proxy like this

globalThis.monetization = new Proxy(monetization, {
  set: () => {
    console.log("Not allowed to mutate values\n");
  },
  get(target, key, receiver) {
    if (
      key === "getState" ||
      key === "addEventListener" ||
      key === "removeEventListener"
    ) {
      return Reflect.get(...arguments);
    } else {
      console.log(`Not allowed to access monetization.${key}\n`);
      return null;
    }
  },
});
Enter fullscreen mode Exit fullscreen mode

This prevents tampering of the original object while exposing only the needed functionality.

Module providers are passed another proxy for the same purpose

new Proxy(monetization, {
        set: () => {
          console.log("Not allowed to mutate values\n");
        },
        get(target, key, receiver) {
          if (key === "packages" || key === "invokeEventListener") {
            return Reflect.get(...arguments);
          } else {
            console.log(`Not allowed to access monetization.${key}\n`);
            return null;
          }
        },
      }),
Enter fullscreen mode Exit fullscreen mode

wrapper-coil-extension

This was tough. Initially, I tried to reverse engineer Coil's Extension by looking at their code on GitHub, but it was way too much for me to understand and code again. No experience with Typescript or building any browser extension did not help also.

Then I found puppeteer ( thanks @wobsoriano )

I poked around Coil's website and found that they were setting a jwt in localStorage whenever a user logs in. This allowed for the login and logout functionality, as I had to just store the jwt locally.

For monetizing packages, I looped through all the monetization enabled packages set up probabilistic revenue sharing and made a template HTML file which would fill up with the values of the respective wallets for 65 seconds each.

A lot of work was also done to make listeners work as expected, and keeping the functionality similar to the browser counterpart.

These pages were then fed to puppeteer which sent payments using coil's extension after looking at the set wallet.

Additional Resources / Info

All the resources are already linked throughout the post.

Top comments (4)

Collapse
 
adamrrudolf profile image
Adam Rybinski

Hey, great article with examples (this Proxy was particularly new to me but I see it quite useful here). I only now stumbled upon this webmonetisation API. I wonder, how user of it can control the max amount of money streamed? Through Coil? This reminds me of how Brave browser supports websites, but this is more for devs of packages

Collapse
 
projectescape profile image
Aniket • Edited

Glad you liked it! Currently the only provider a user can pay through is coil, by paying a flat amount of $5 per month and installing coil's browser extension. Coil then pays the sites visited $0.0001 per second. This rate can later decrease so that the max amount never increases $5. As different providers appear, they could have different monthly fee and payment rates, but for now only coil exists.

Collapse
 
projectescape profile image
Aniket

Since this post was posted, I have made many changes and improvements to both the packages, which I have detailed here.


I have also updated this post accordingly
Collapse
 
joelwass profile image
Joel Wasserman

This is awesome! Very well done and a great read.

We’re going to implement something very similar at Flossbank to auto pay maintainers and would like to keep the pattern for maintainers to implement consistent.

Shoot me an email if you’d like to collaborate! Joel@flossbank.com Our code is all oss GitHub.com/flossbank

Some comments have been hidden by the post's author - find out more