Built a Chrome Extension to View Facebook Stories Anonymously (Without Triggering "Seen")
Ever wanted to watch Facebook Stories without leaving a trace? Here's how we reverse-engineered Facebook's GraphQL transports to build a zero-leak, Manifest V3 anonymous story viewer.

Open Source on GitHub: github.com/plitto007/FacebookUnseenStories
Ever wanted to catch up on a friend's Facebook Story without showing up in their viewer list? Whether you want to browse without social pressure, check updates from acquaintances, or quietly do competitor research, viewing stories anonymously is a frequent user need.
Building this for modern Facebook, however, is a tricky engineering puzzle. Facebook is a complex React Single Page Application (SPA) powered by internal GraphQL APIs and multiple network transports.
In this quick deep dive, we'll examine how Facebook's story tracking works under the hood and how we engineered a lightweight, Manifest V3 Chrome extension to view Stories completely undetected—without breaking media playback or violating browser security models.
1. How Facebook Tracks Story Views
When you watch a Story on Facebook or Messenger web, two distinct network actions occur:
- Content Fetching (Read):
Facebook queries endpoints likeStoriesViewerBucketPrefetcherMultiBucketsQueryorStoriesTrayQueryto load image and video assets. These must never be blocked, or the player will freeze on a black screen. - Seen Status Mutation (Write):
Once a story card displays, Facebook fires a GraphQL mutation:StoriesUpdateSeenStateMutationStoriesReaderUpdateSeenStateMutation- Payload actions containing
stories_update_seen_state
This mutation notifies Facebook servers to log your account ID into the author's viewer tray.
sequenceDiagram
autonumber
actor User
participant Player as Facebook Story Player
participant Ext as Unseen Interceptor
participant Server as Facebook Servers
User->>Player: Opens Friend's Story
Player->>Server: Fetches Story Media (Image/Video)
Server-->>Player: Media Stream Loaded (Story Plays)
Player->>Ext: Sends StoriesUpdateSeenStateMutation
rect rgb(30, 41, 59)
Note over Ext: Intercepts & Drops Seen Request
Ext-->>Player: Returns Mock 200 OK Response
end
Note over Player: Story proceeds smoothly (no retry loop)
Note over Server: Server never receives the seen receipt!
2. The Manifest V3 Hurdle
In Manifest V2, extensions could simply use chrome.webRequest.onBeforeRequest with blocking privileges. But in Manifest V3:
webRequestBlockingis deprecated for standard extensions.declarativeNetRequestcannot inspect HTTP POST bodies (FormDataorx-www-form-urlencoded).- All Facebook features route through the same unified
/api/graphql/endpoint. Blocking the URL pattern would break your entire Facebook feed, notifications, and the story viewer itself.
The Solution: Client-side network interception directly inside the browser's JavaScript runtime.
3. The Architecture: Main World Interception
Extensions usually run in an Isolated World, meaning modifying window.fetch inside a standard content script won't affect the page's scripts.
Using Manifest V3's "world": "MAIN", we inject our interceptor script (content-main.js) at document_start before Facebook's React runtime even loads.
{
"content_scripts": [
{
"matches": ["*://*.facebook.com/*", "*://*.messenger.com/*"],
"js": ["content/content-main.js"],
"run_at": "document_start",
"world": "MAIN"
},
{
"matches": ["*://*.facebook.com/*", "*://*.messenger.com/*"],
"js": ["content/content-isolated.js"],
"run_at": "document_start"
}
]
}
4. Multi-Layer Transport Blocking
Facebook relies on three distinct browser network APIs. We hook all three to ensure zero leaks:
A. Intercepting fetch()
const originalFetch = window.fetch;
window.fetch = async function (input, init) {
const url = typeof input === 'string' ? input : input?.url || '';
const body = init?.body;
if (isStorySeenRequest(url, body)) {
notifyBlocked(url);
// Return a mock 200 OK so Facebook's React state won't crash or retry
return new Response(
JSON.stringify({
data: {
stories_update_seen_state: {
success: true,
__typename: 'StoriesUpdateSeenStateMutationResponse'
}
}
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
}
return originalFetch.apply(this, arguments);
};
Why return a fake 200 OK?
If the network request fails or throws, Facebook's React error handlers trigger aggressive retries or freeze playback. Providing a clean{ success: true }mock allows the story viewer to transition smoothly to the next slide.
B. Intercepting XMLHttpRequest (XHR)
For legacy components, we hook XHR.prototype.open and XHR.prototype.send, simulating a completed response (readyState: 4, status: 200).
C. Overriding navigator.sendBeacon()
When a tab is closed or navigated away, Facebook dispatches final view receipts via sendBeacon(). Overriding navigator.sendBeacon prevents last-second receipts from escaping when you close a tab.
5. Bridging to the Extension UI
The Main World interceptor cannot directly invoke Chrome Extension APIs. To bridge this gap, we use an Isolated World content script (content-isolated.js) communicating via window.postMessage:
graph LR
MainScript["content-main.js<br/>(Main World)"] -- "postMessage (BLOCKED)" --> IsolatedScript["content-isolated.js<br/>(Isolated World)"]
IsolatedScript -- "chrome.storage" --> Storage[("Local Storage")]
IsolatedScript -- "chrome.runtime" --> SW["Service Worker"]
SW --> Badge["Toolbar Badge (ON/OFF)"]
- Config Sync: When you toggle the popup switch,
chrome.storage.localnotifies the page viapostMessage. - Live Counter: Each blocked seen mutation increments the counter displayed on your extension popup and toolbar badge.
6. Privacy First: Zero Tracking
- 100% Client-Side: No tokens, credentials, or personal messages are ever inspected or transmitted.
- Scored Permissions: Only requires access to
facebook.comandmessenger.com. - No Background Resource Drain: The service worker stays idle until triggered by storage events.
🚀 Get the Extension & Source Code
The complete source code is open-source and ready to load as an unpacked extension:
🔗 GitHub Repository: https://github.com/plitto007/FacebookUnseenStories
# Clone the repository
git clone https://github.com/plitto007/FacebookUnseenStories.git
# Load into Chrome:
# 1. Open chrome://extensions/
# 2. Enable "Developer mode" (top right)
# 3. Click "Load unpacked" and select the project folder
If you find this project useful, feel free to star the repo on GitHub! ⭐
Related Articles
Mastering EXIF Metadata Extraction and Framing in Native Android
Deep-dive into ExifInterface, parsing lens details, shutter speed, ISO, and rendering clean minimal frames with Jetpack Compose canvas.

Architecting Sight Bridge AI: On-Device LiteRT & Multimodal Edge Radar
How we achieved sub-25ms walking hazard reflex detection running 100% on-device with TensorFlow Lite/LiteRT and multimodal Gemini assistant integration.
Building Lightweight Serverless CMS with Cloudflare D1 and R2
A complete breakdown of building high-performance, cost-effective blog systems with Next.js, Cloudflare D1 SQLite, and R2 zero-egress object storage.