I just shipped my first Chrome extension.
Not a tutorial toy. Not a “hello world” that never left chrome://extensions. A real Manifest V3 product with Sign in with X, local XP tracking, cloud sync, a privacy policy, and a listing live on the Chrome Web Store.
The product is called XPilot. It turns your activity on X into a game: XP for posts, replies, likes, and reposts. Levels. Streaks. A popup that shows your progress in one click.
And yes: it is free. Add it from the Chrome Web Store, sign in with X, and start earning XP on the stuff you already do. No waitlist. No credit card.
This post is the story of what I expected, what actually broke, and what I would tell myself on day one.
If you are about to build your first extension, steal the lessons. Skip the landmines. If you just want the product: install XPilot free.
I care about growth on X. Most tools in that space try to write for you. That was never the itch.
The real problem, for me and for a lot of founders I talk to, is consistency. You know you should reply more. You know streaks matter. You still flake when the day gets loud.
So the bet was simple:
Make X feel like a game people already understand: XP, levels, streaks.
And the distribution bet was equally simple:
Put the product where the habit already happens. Inside Chrome. On x.com.
A dashboard you have to remember to open loses to a popup that opens when you are already posting.
That is why XPilot is an extension first, website second.
I did not invent a framework.
Extension: WXT + TypeScript, Manifest V3.
UI: plain HTML, CSS, and JS in the popup. Fast, no React tax for v0.1.
Content script: detect my own activity on x.com and twitter.com.
Backend: Next.js on Vercel.
Auth: X OAuth 2.0 with PKCE, then an app JWT.
DB: Postgres on Neon, Drizzle ORM.
Sync: local-first, with a backup about every 12 hours.
WXT was the right call. It handles the annoying MV3 packaging, hot reload in development, and zip for the store. If you are starting today, do not hand-roll Webpack for an extension unless you enjoy pain.
I forced a tiny definition of done:
localhost host permissionsEverything else was a later problem. Opportunity radar, AI coach, seasons: later. Shipping was the boss fight.
If your last mental model of extensions is Manifest V2, throw it away.
MV3 service workers go idle. You cannot assume a long-lived background page. Alarms exist for a reason. Persistent connections are not free.
Permissions matter more than you think. Every host permission is a trust tax on the listing review and on users. I kept production hosts to:
x.com / twitter.com (where the game runs)xpilot.so / www.xpilot.so (where the API lives)Dev-only: localhost:3000.
That last detail almost bit me in production. More on that below.
Browser OAuth is familiar. Extension OAuth is a different animal.
XPilot uses chrome.identity.launchWebAuthFlow. The flow looks clean on a whiteboard:
/api/auth/xhttps://<extension-id>.chromiumapp.org/?code=...In practice, three things tried to murder me.
APP_URL exactlyOur production site redirects xpilot.so → www.xpilot.so.
If your OAuth redirect_uri is registered as one host and your app builds the other, X shows a vague "you weren't able to give access to the App" page. No stack trace. No mercy.
Fix: pick one canonical origin. Put it in APP_URL. Register that exact callback on developer.x.com. Stop arguing with DNS.
I originally returned the short-lived exchange code as a hash fragment (#code=...). Fragments are cute until redirects strip them and chrome.identity hands you an empty URL.
Fix: put the code in the query string (?code=...). Parse query first, hash as fallback.
Vercel logs told the truth:
OPTIONS /api/auth/exchange → 204POST after itThe store build talked to www, while the manifest only allowed the apex. Chrome fell back to CORS. Preflight "succeeded." The real POST never flew. The popup still showed Sign in with X like nothing happened.
Fix:
https://www.xpilot.soALLOWED_EXTENSION_IDS in production so random extensions cannot finish OAuthAlso: the popup often closes when the auth window opens. Your background script must finish login even if the UI is gone. When the user reopens the popup, auth should already be in chrome.storage.
X’s frontend is not an API contract. It is a living organism.
Content scripts that award XP for replies have to survive:
I shipped a fail-closed version that broke real replies. Then a looser version that missed intermittent posts. Then a multi-signal version: busy state, clear state, dialog stack, keyboard submit.
If you are building anything on top of a third-party SPA, budget time for “the UI changed and nothing is wrong in your TypeScript.”
I did not want a backend call on every like. I also did not want “free” to mean “runs my GPU bill into the ground.”
So XP lives in the extension first. Sync is a backup, not the gameplay loop.
Rules that kept costs and complexity down:
That architecture is why I can ship v0.1 as a free Chrome extension without feeling like every install is a liability. The game still feels instant when the network is sad. The expensive ideas (AI opportunity radar, deep coaching) can wait until people are actually addicted to the loop.
Shipping code is half the job. The listing is the other half.
Things that mattered more than I wanted them to:
0.1.0 → 0.1.1 was not vanity)Also: trader verification and regional phone issues can block you in weird ways. Budget calendar time for account admin, not just engineering.
There is a specific moment when the listing flips from draft to public and the install link works for someone who is not you.
That moment hit harder than any green CI check.
I have shipped websites before. An extension feels different. It lives in the browser chrome. It asks for trust. It sits next to passwords and ad blockers. When someone clicks Add to Chrome, they are not bouncing through a landing page. They are installing you into their daily toolbelt.
That is terrifying. It is also the point.
XPilot is live on the Chrome Web Store. Free to install. Free to use for XP, levels, and streaks.
One minute setup:
No waitlist. No “book a demo.” No paid wall in front of the core loop.
I am building the game layer for people who are done growing on guilt. v0.1 is the foundation and it is free on purpose: if the habit is not sticky when it costs nothing, it will not be sticky when it costs €29.
No. For a small popup, plain HTML is fine. Use React when the UI complexity earns it.
Use a framework like WXT unless you have a reason not to. Packaging and MV3 details are not where your product moat lives.
It is fragile and you must stay on the right side of ToS and user expectations. Track the user’s own actions in their session. Do not build a stealth scraper and call it a growth tool.
Missing privacy policy, unclear permissions, remote code, broken auth, and listings that do not match the binary.
Longer than the happy-path tutorial. Shorter than “rewrite everything because MV3.” Most of the calendar time was OAuth, store packaging, and edge cases on X’s UI, not the XP math.
If you have been waiting to build your first Chrome extension, start.
Pick a problem that only makes sense inside the browser. Keep the first version insultingly small. Assume OAuth and packaging will take longer than your ego wants. Ship anyway.
I just did. The extension is free. The lessons above are free too.
Add XPilot to Chrome and let the game begin.
Written by Alex Cloudstar (@alexcloudstar). Building XPilot free on Chrome.