Build a Chrome Extension That People Actually Pay For
Build13 min read·April 12, 2026·--

Build a Chrome Extension That People Actually Pay For

Chrome extensions are one of the most underrated SaaS distribution channels. 3 billion Chrome users, frictionless install, recurring revenue. Here's how to build one from scratch.

@
@kivorablog
April 12, 2026
Share

Why Chrome Extensions Are a Hidden SaaS Goldmine


The Chrome Web Store has 3 billion potential users. Installing an extension takes 2 clicks. There's no App Store review process (review takes 1–5 days vs weeks for mobile). And because extensions live in the browser, they integrate directly into users' existing workflows.


Extensions that solve a small, specific pain in a workflow people do daily are some of the stickiest products you can build.


Extension CategoryExample PainWillingness to Pay
Productivity"I copy-paste between tabs 50x per day"High
Writing assistant"I write emails slowly"High
Research"I need to summarise pages I'm reading"Medium-High
Job search"I apply to 20 jobs per day"High
Social media"I need to schedule posts while browsing"Medium
Developer tools"I need to format/parse data while testing"High



Free Stack for Extension Development


ToolPurposeCost
Manifest V3 (Chrome Extension API)Extension frameworkFree
React + ViteBuild the popup UIFree
SupabaseUser auth + subscription statusFree tier
GroqAI featuresFree tier
StripePayments2.9% + $0.30 per transaction



Step 1: Understand the Extension Architecture


A Chrome extension has three distinct execution contexts:


┌─────────────────────────────────────────────────────────┐
│                    BROWSER                              │
│                                                         │
│  ┌─────────────────┐    ┌───────────────────────────┐  │
│  │  POPUP           │    │  CONTENT SCRIPT           │  │
│  │                  │    │  (runs in page context)   │  │
│  │  What users see  │    │  - Can read/modify DOM    │  │
│  │  when they click │    │  - Communicates via       │  │
│  │  the extension   │    │    messages               │  │
│  │  icon            │    │                           │  │
│  └─────────────────┘    └───────────────────────────┘  │
│           │                          │                  │
│           └──────────┬───────────────┘                  │
│                      ▼                                  │
│            ┌─────────────────┐                          │
│            │  SERVICE WORKER  │                          │
│            │  (background)    │                          │
│            │  - No DOM access │                          │
│            │  - API calls     │                          │
│            │  - Auth state    │                          │
│            └─────────────────┘                          │
└─────────────────────────────────────────────────────────┘



Step 2: Create the Extension Structure


mkdir my-extension && cd my-extension
npm create vite@latest popup -- --template react
cd popup && npm install

# Install extension-specific packages
npm install @supabase/supabase-js webext-bridge
npm install -D @crxjs/vite-plugin

my-extension/
├── popup/              ← React app for the popup UI
│   ├── src/
│   │   ├── App.jsx
│   │   └── main.jsx
│   └── vite.config.js
├── content/
│   └── content.js      ← Content script (injected into pages)
├── background/
│   └── service-worker.js ← Background service worker
├── manifest.json       ← Extension configuration
└── icons/              ← 16, 32, 48, 128px icons



Step 3: The Manifest File (V3)


{
  "manifest_version": 3,
  "name": "My AI Extension",
  "description": "AI-powered productivity tool for Chrome",
  "version": "1.0.0",
  "permissions": [
    "activeTab",
    "storage",
    "contextMenus"
  ],
  "host_permissions": [
    "https://*.supabase.co/*",
    "https://api.groq.com/*"
  ],
  "action": {
    "default_popup": "popup/index.html",
    "default_icon": {
      "16":  "icons/16.png",
      "32":  "icons/32.png",
      "48":  "icons/48.png",
      "128": "icons/128.png"
    }
  },
  "background": {
    "service_worker": "background/service-worker.js",
    "type": "module"
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js":      ["content/content.js"]
    }
  ],
  "icons": {
    "16":  "icons/16.png",
    "48":  "icons/48.png",
    "128": "icons/128.png"
  }
},



Step 4: Build the Popup UI


// popup/src/App.jsx
import { useState, useEffect } from 'react'
import { supabase } from './lib/supabase'

function App() {
  const [user, setUser]         = useState(null)
  const [loading, setLoading]   = useState(true)
  const [pageText, setPageText] = useState('')
  const [summary, setSummary]   = useState('')
  const [working, setWorking]   = useState(false)

  useEffect(() => {
    // Get current auth state
    supabase.auth.getUser().then(({ data: { user } }) => {
      setUser(user)
      setLoading(false)
    })

    // Get selected text from the current page
    chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
      chrome.tabs.sendMessage(tabs[0].id, { action: 'GET_SELECTION' }, (response) => {
        if (response?.selection) setPageText(response.selection)
      })
    })
  }, [])

  async function summarise() {
    if (!pageText) return
    setWorking(true)
    setSummary('')

    // Call your background service worker for the API call
    chrome.runtime.sendMessage(
      { action: 'SUMMARISE', text: pageText },
      (response) => {
        setSummary(response.summary)
        setWorking(false)
      }
    )
  }

  if (loading) return <div className="p-4">Loading...</div>

  if (!user) return <SignIn />

  return (
    <div style={{ width: 360, padding: 16, fontFamily: 'Inter, sans-serif', background: '#0a0a0a', color: '#fff', minHeight: 200 }}>
      <h1 style={{ fontSize: 16, fontWeight: 700, marginBottom: 12 }}>AI Assistant</h1>

      {pageText ? (
        <div>
          <p style={{ fontSize: 12, color: '#737373', marginBottom: 8 }}>
            Selected: {pageText.slice(0, 80)}...
          </p>
          <button
            onClick={summarise}
            disabled={working}
            style={{ background: '#dc2626', color: '#fff', border: 'none', borderRadius: 8, padding: '8px 16px', cursor: 'pointer', width: '100%' }}
          >
            {working ? 'Summarising...' : 'Summarise Selection'}
          </button>
        </div>
      ) : (
        <p style={{ fontSize: 13, color: '#737373' }}>Select text on any page, then open this extension.</p>
      )}

      {summary && (
        <div style={{ marginTop: 12, background: '#141414', borderRadius: 8, padding: 12, fontSize: 13, lineHeight: 1.6 }}>
          {summary}
        </div>
      )}
    </div>
  )
},

export default App



Step 5: Monetise With Stripe


// background/service-worker.js
const FREE_LIMIT = 10 // 10 free summaries per day

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'SUMMARISE') {
    handleSummarise(message.text).then(sendResponse)
    return true // Keep channel open for async
  }
})

async function handleSummarise(text) {
  // Check usage limit from storage
  const { dailyUsage = 0, lastReset, isPro = false } = await chrome.storage.local.get(['dailyUsage', 'lastReset', 'isPro'])

  // Reset counter if it's a new day
  const today = new Date().toDateString()
  if (lastReset !== today) {
    await chrome.storage.local.set({ dailyUsage: 0, lastReset: today })
  }

  if (!isPro && dailyUsage >= FREE_LIMIT) {
    return { error: 'Free limit reached', upgradeRequired: true }
  }

  // Call Groq API
  const response = await fetch('https://api.groq.com/openai/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${GROQ_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      model: 'llama-3.1-8b-instant',
      messages: [
        { role: 'system', content: 'Summarise the following text in 3 bullet points. Be concise.' },
        { role: 'user',   content: text.slice(0, 3000) }
      ]
    })
  })

  const data = await response.json()
  const summary = data.choices[0].message.content

  // Increment usage counter
  await chrome.storage.local.set({ dailyUsage: dailyUsage + 1 })

  return { summary }
},



Publishing to the Chrome Web Store


  • Build your extension: npm run build
  • Zip the output folder
  • Go to chrome.google.com/webstore/devconsole
  • Pay the one-time $5 developer registration fee
  • Upload your zip and fill in the listing details
  • Submit for review (typically 1–3 business days)

Pricing Strategy

ModelPriceProsCons
One-time purchase$9–$29Simple, good for simple toolsNo recurring revenue
Monthly subscription$4–$15/monthRecurring revenueHigher churn
FreemiumFree + $9/month ProViral growth + revenueRequires valuable paid tier

The most successful indie extensions use freemium: generous free tier (enough to prove value), clear paid upgrade (removes limits or adds power features).

Read more on Kivora Blog

Read more on Kivora Blog

Get started →