EvenLeads is a web app plus a Chrome extension. The web app finds public "looking to hire" posts and writes a draft reply. The extension posts that reply from the user's own account once they approve it, or on its own if they switch Auto-send on. Wiring the two together in Manifest V3 taught me a few things.
1. A content-script bridge between the dashboard and the extension
The dashboard is an ordinary web page, so it can't call extension APIs. A small content script runs only on our own domain and relays messages:
- the page dispatches a CustomEvent with a request type and payload
- the content script picks it up and forwards it with chrome.runtime.sendMessage
- the background service worker handles it and answers
- the content script fires a response CustomEvent back to the page
Every request type has a matching response type, so the page can time out cleanly when the extension isn't installed or was just reloaded. The whole thing is deliberately dull and easy to test.
2. Service workers die, so expect "Extension context invalidated"
MV3 stops the background worker and starts it again whenever Chrome decides to. Anything held in memory disappears, and a page still talking to the old instance gets "Extension context invalidated". What helped:
- state lives in chrome.storage, never in module variables
- every message is handled as if it could be the first one after a restart
- the page detects an invalidated context and asks for a refresh instead of failing silently
3. Keep your own service worker logs
You can't open DevTools on a user's service worker. The background script wraps console.log, warn and error and keeps the last 500 entries in a ring buffer. A test command over the same bridge returns that buffer, so when a send fails during testing I can read what the worker did without going near chrome://extensions.
4. The web app decides and the extension executes
The extension never chooses what to send. It receives one task at a time, holds an in-flight lock so the same lead can't be sent twice, runs it and reports the result to the dashboard. Keeping decisions on the server keeps the extension small and much easier to reason about.
5. Intents instead of selectors
LinkedIn, Reddit and Facebook change their markup often. Hard-coded selectors for the comment box or the send button would break constantly, so the extension uses a small declarative intent engine: open the composer, type, submit, verify. Each intent tries known selectors first and falls back to role and visible text.
6. Default to slow
The extension could reply seconds after a post appears. By default it waits for a person. Instant replies to every matching post look automated however good the text is, and many communities don't allow vendor replies at all. Approval is the default and Auto-send is a switch the user turns on.
Disclosure: an AI agent wrote this post from the EvenLeads code and posted it for me. I read and approved the text before it went up.

