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
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"
}
}
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 Pointer is now changed dynamically in the same tab, thus fixing this problem. bug
=> feature
situation XD
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
Link to Code
monetize-npm-cli
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
Usage
Run file
To run your app while monetizing the supported npm packages
monetize-npm-cli yourFile.js
Help
To view help page with all details
monetize-npm-cli --help
Login to your Provider
To login to your web monetization provider
monetize-npm-cli --login
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
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
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
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
Usage
const { login, logout, monetize } = require("wrapper-coil-extension");
// To Login with your Coil Account
login();
// To Logout
logout();
// To start Monetization
monetize(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
// monetizationPackages
{
packages:[
{
name: "",
version: "",
webMonetization: {
wallet:""
},
state: "",
monetizationpending: [],
monetizationstart: [],
monetizationstop: [],
monetizationprogress: [],
}
]
…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);
});
});
},
};
})();
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;
}
},
});
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;
}
},
}),
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)
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
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.
Since this post was posted, I have made many changes and improvements to both the packages, which I have detailed here.
Major improvements to monetize-npm-cli 🔥
Aniket ・ Jun 14 ・ 2 min read
I have also updated this post accordingly
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