Web Monetization for NPM packages!!
Tags: gftwhackathon, node, webdev, showdev
Published: 2020-06-05 20:55:25

[instructions]: # "To submit to the Grant For The Web x DEV Hackathon, please fill out all sections."

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](https://www.npmjs.com/package/monetize-npm-cli)
1. `wrapper-coil-extension` [npm](https://www.npmjs.com/package/wrapper-coil-extension)

---

### `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

```json
{
  "webMonetization": {
    "wallet": "$yourWalletAddressGoesHere"
  }
}
```

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](https://dev.to/projectescape/major-improvements-to-monetize-npm-cli-1l6c)

~~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](https://github.com/projectescape/monetize-npm-cli#providers).

---

### 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.
1. Careful installation of packages and prevention of installation of unnecessary packages.
1. 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:

[note]: # "Foundational Technology, Creative Catalyst, or Exciting Experiments"

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(?)
1. Creative Catalyst - It is using the existing technologies to find ways to distribute and monetize content.
1. 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

```bash
npm install -g monetize-npm-cli
```

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

```bash
monetize-npm-cli -v
```

![Version image](https://dev-to-uploads.s3.amazonaws.com/i/exi57dlphe44ca1qspnn.png)

Let's go to the help page

```bash
monetize-npm-cli -h
```

![Help Image](https://dev-to-uploads.s3.amazonaws.com/i/59i4zy04e78xlpl1krah.png)

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

```bash
monetize-npm-cli --login
```

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

![Login Browser](https://dev-to-uploads.s3.amazonaws.com/i/rwurhlgvu9p7l8rmm752.png)

On successful login, we will see this on our terminal

![Login Terminal](https://dev-to-uploads.s3.amazonaws.com/i/vfdzsivepg3zev2biefo.png)

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

Let's try listing those packages

```bash
monetize-npm-cli --list --expand
```

You can expect to see something like this

![List Terminal](https://dev-to-uploads.s3.amazonaws.com/i/ubxt16h70uqustcvlhr6.png)

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

![Added Snippet](https://dev-to-uploads.s3.amazonaws.com/i/rc0cyny9kyi6b0mcmnc4.png)

Let's try running the app now

```bash
monetize-npm-cli index.js
```

As soon as base64url starts getting paid

![base64usl Paying](https://dev-to-uploads.s3.amazonaws.com/i/62vgwhtotvov6e2yz424.png)

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

![terminalOutput](https://dev-to-uploads.s3.amazonaws.com/i/lxcek6ponb90fvhygy5b.png)

## Link to Code

[note]: # "Our markdown editor supports pretty embeds. If you're sharing a GitHub repo, try this syntax: `{% github link_to_your_repo %}`"

### `monetize-npm-cli`

{% github projectescape/monetize-npm-cli %}

### `wrapper-coil-extension`

{% github projectescape/wrapper-coil-extension %}

## How I built it

[note]: # "For example, what's the stack? did you run into issues or discover something new along the way? etc!"

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

```javascript
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);
        });
      });
    },
  };
})();
```

`globalThis.monetization` was implemented using a proxy like this

```javascript
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;
    }
  },
});
```

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

Module providers are passed another proxy for the same purpose

```javascript
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;
          }
        },
      }),
```

### `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

[reminder]: # "We hope you consider expanding your submission into a full-on application for the Grant for the Web CFP, due on June 12."

All the resources are already linked throughout the post.