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!
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
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
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
Active development of more packages as developers are being paid for their hobbies.
Careful installation of packages and prevention of installation of unnecessary packages.
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
Foundational Technology - It is a template for monetizing the web and is a plugin(?)
Creative Catalyst - It is using the existing technologies to find ways to distribute and monetize content.
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
First of all, let's check whether the package is installed properly
monetize-npm-cli -v
Let's go to the help page
monetize-npm-cli -h
To monetize any package, we need to first login to our provider
monetize-npm-cli --login
This will open up a browser window where you can use your credentials to login
On successful login, we will see this on our 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
You can expect to see something like this
Let's add some access to globalThis.monetization from the app which is being run inside the container
Let's try running the app now
monetize-npm-cli index.js
As soon as base64url starts getting paid
We can see the event we set fired up in the console
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
Usage
const{ login, logout, monetize }=require("wrapper-coil-extension");// To Login with your Coil Accountlogin();// To Logoutlogout();// To start Monetizationmonetize(monetizationPackages);
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
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
constmonetization=(()=>{letpackages=[];constwalletHash={};constnameHash={};return{getpackages(){returnpackages;},setpackages(val){packages=val;val.forEach((p,index)=>{if (walletHash[p.webMonetization.wallet]===undefined)walletHash[p.webMonetization.wallet]=[index];elsewalletHash[p.webMonetization.wallet].push(index);nameHash[`${p.name}@${p.version}`]=index;});},getState(name,version){if (nameHash[`${name}@${version}`]!==undefined){returnpackages[nameHash[`${name}@${version}`]].state;}console.log(`No package ${name}@${version} found\n`);returnundefined;},addEventListener(name,version,listener,foo){if (!(listener==="monetizationpending"||listener==="monetizationstart"||listener==="monetizationstop"||listener==="monetizationprogress")){console.log(`${listener} is not a valid event name\n`);returnfalse;}if (nameHash[`${name}@${version}`]!==undefined){packages[nameHash[`${name}@${version}`]][listener].push(foo);returntrue;}console.log(`No package ${name}@${version} found\n`);returnfalse;},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`);returnfalse;}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);}returntrue;}console.log(`No package ${name}@${version} found\n`);returnfalse;},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
globalThis.monetization=newProxy(monetization,{set:()=>{console.log("Not allowed to mutate values\n");},get(target,key,receiver){if (key==="getState"||key==="addEventListener"||key==="removeEventListener"){returnReflect.get(...arguments);}else{console.log(`Not allowed to access monetization.${key}\n`);returnnull;}},});
This prevents tampering of the original object while exposing only the needed functionality.
Module providers are passed another proxy for the same purpose
newProxy(monetization,{set:()=>{console.log("Not allowed to mutate values\n");},get(target,key,receiver){if (key==="packages"||key==="invokeEventListener"){returnReflect.get(...arguments);}else{console.log(`Not allowed to access monetization.${key}\n`);returnnull;}},}),
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.
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.