โ† All posts
Blog

Building the Network: What Shipping Actually Takes

Laurynas Karvelis ยท

I started Zero Ad Network with what seemed like a small experiment: could the idea actually work? I thought a few moving parts would be enough to get it running. Even with 20 years of web development experience, I didn't expect the project to become this involved.

I've spent the past 3 years building it from scratch: a platform that lets website owners earn money without showing visitors ads, cookie consent screens or other interruptions. Two browser extensions, a WordPress plugin, a TypeScript npm package, a PHP Composer package and a Stripe payout system later, it's finally live!

Getting the Chrome extension into the Web Store wasn't easy!

Google's Chrome extension review process was more involved than I expected. First, I had to pay a one-time $5 registration fee. Every permission the extension requested needed a written justification. I also had to explain every feature that used the network, storage or the DOM. The reviewers were thorough.

It took roughly 3 weeks from the first submission to approval. The email saying "Your extension is now live in the Chrome Web Store" brought mixed feelings. First came the excitement, then a sense of responsibility: things were getting real!

Submitting the Firefox extension was much more straightforward. The codebase was almost identical, apart from a slightly different manifest.json structure and the API for communicating with zeroad.network. That connection was essential: the website needed to send the extension its token payload once a user's subscription was activated.

After a relatively smooth development experience with Chrome and Firefox, I tried to get the extension working in Safari too. I hit a wall almost immediately: I couldn't inject the custom X-Better-Web-Hello request header through declarativeNetRequest. Without that, I couldn't make the handshake between a participating website and the extension work.

I could see the privacy argument for limiting extension capabilities, but Safari was frustrating to work with. I struggled to find my way around its debugging tools. Chrome felt like a walk in the park by comparison. I'll keep an eye on whether those restrictions change. For now: two extensions, not three.

npm and Composer are a joy

Publishing the TypeScript and PHP packages was easy. I set up accounts on npmjs.com and packagist.org, connected the GitHub repositories and added a few simple GitHub Actions workflows. That was enough to get them published.

WordPress - not so much

WordPress expects you to load JavaScript and CSS through its own PHP functions, including on your plugin's pages. That makes sense, but I only learned it during review. The feedback process was helpful: each email explained what I needed to fix. I went through at least 3 rounds ๐Ÿ˜…. Of all the submissions, the WordPress plugin took me the most time and effort.

Eventually, the plugin was approved and became available in the WordPress plugin directory. That felt like a win ๐ŸŽ‰. With WordPress powering almost 40% of active websites, making the plugin available there was a priority.

The ups and downs of ORM

I started with TypeORM because I'd used it successfully before. It felt like an obvious choice.

The drawbacks became clearer as the project grew. Tree shaking was limited, and development had seemed slow for a while, although a v1 release was in progress. Beyond simple CRUD, the QueryBuilder became awkward. Queries that took 3 lines of SQL turned into 15-line method chains. Quick calls to findOneOrFail(), save() and upserts saved time, but I lost some of it wrestling with queryBuilder().

After a few evenings looking for alternatives, I tried Drizzle. It was one-sixth of the bundle size and worked with Bun's built-in SQL driver. Writing schemas felt natural, with no decorators or reflect-metadata import to arrange.

Like TypeORM, Drizzle could generate migrations through its CLI. The difference was that I only had an "up" migration, with no separate "down" script. That approach made sense to me, though it called for care. The migrations were plain SQL statements, with no TypeScript to transpile.

Drizzle gave me a simpler way to use SQL while keeping control over the queries. Its v1 work also made findMany() look very familiar from TypeORM. It took a couple of days to move the entire database stack over.

Payouts were deceptively involved

I knew the website payout and URP (Unattributed Revenue Pool) calculations wouldn't be easy to write. Even with help from Claude, the dynamic CTEs, time windows and edge cases took some untangling. Reading those queries a few days later still made me pause. It took a week to get right, and the work was completely invisible to users.

The interface for publishers' connected Stripe accounts, along with the rest of publisher onboarding, took another 3 days to polish.

The part I'm actually not ready for

Here's what I've realised: with enough time and enough attempts, I can work through the technical challenges. Extension reviews, the ORM migration, Stripe integration โ€” I can keep going until they work.

Unrelated image ;)

Marketing is a different challenge for me. There's no pull request I can merge to convince website owners to join, and no SQL query that will change people's browsing habits. It calls for a different set of skills, and for the first time in this project, I'm scared ๐Ÿ˜…. I think I'll try the same approach: keep at it until I figure it out!

So that's where Zero Ad Network is going now. Fingers crossed ๐Ÿคž

Stay tuned!

Continue reading