bouncer-feed-filter
AI-powered browser extension that filters unwanted posts from Twitter/X feeds using natural language rules and multiple AI backends
What this skill does
# Bouncer Feed Filter
> Skill by [ara.so](https://ara.so) — Daily 2026 Skills collection.
Bouncer is a browser extension (Chrome/Edge/iOS) that uses AI to filter unwanted posts from Twitter/X feeds in real time. Users define filters in plain language ("crypto", "engagement bait", "rage politics"), and Bouncer classifies and hides matching posts using AI models — local (WebGPU via WebLLM) or cloud (OpenAI, Gemini, Anthropic, OpenRouter, Imbue).
## Repository Structure
```
Bouncer/ # Main extension source
src/
background/ # Service worker / background scripts
content/ # Content scripts (Twitter DOM interaction)
popup/ # Extension popup UI
adapters/ # Site adapters (Twitter/X)
models/ # AI backend integrations
utils/ # Shared utilities
icons/ # Extension icons
manifest.json # Chrome extension manifest
package.json
tsconfig.json
```
## Installation & Build
### From Source (Chrome/Edge)
```bash
git clone https://github.com/imbue-ai/bouncer.git
cd bouncer/Bouncer
npm install
npm run build
```
Load in Chrome:
1. Go to `chrome://extensions`
2. Enable **Developer mode**
3. Click **Load unpacked** → select `Bouncer/` folder
4. Navigate to `twitter.com` or `x.com`
### Development Build (watch mode)
```bash
cd Bouncer
npm run dev # watch mode with hot rebuild
```
### Production Build
```bash
npm run build # outputs to Bouncer/dist or inline
```
## AI Backend Configuration
Bouncer supports multiple providers. Configure via the extension popup Settings panel.
### Provider / Model Matrix
| Provider | Model IDs | Auth |
|----------|-----------|------|
| Local WebGPU | `Qwen3-4B`, `Qwen3.5-4B`, `Qwen3.5-4B Vision` | None |
| OpenAI | `GPT-5 Nano`, `gpt-oss-20b` | API key |
| Google Gemini | `2.5 Flash Lite`, `2.5 Flash`, `3 Flash Preview` | API key |
| Anthropic | `Claude Haiku 4.5` | API key |
| OpenRouter | `Nemotron Nano 12B VL`, `Ministral 3B` | Account token |
| Imbue | Default | None (built-in) |
API keys are stored in Chrome's `chrome.storage.local` — never hardcoded.
## Core Architecture
### 1. MutationObserver — Content Script
The content script watches the Twitter feed for new posts:
```typescript
// src/content/feedObserver.ts
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node instanceof HTMLElement) {
const post = extractPost(node);
if (post) classifyAndFilter(post);
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
```
### 2. Post Extraction — Twitter Adapter
```typescript
// src/adapters/twitter.ts
export interface ExtractedPost {
id: string;
text: string;
authorHandle: string;
imageUrls: string[];
element: HTMLElement;
}
export function extractPost(element: HTMLElement): ExtractedPost | null {
const article = element.querySelector('article[data-testid="tweet"]');
if (!article) return null;
const tweetText = article.querySelector('[data-testid="tweetText"]')?.textContent ?? '';
const handle = article.querySelector('[data-testid="User-Name"] a')?.getAttribute('href') ?? '';
const images = [...article.querySelectorAll('img[src*="pbs.twimg.com/media"]')]
.map(img => (img as HTMLImageElement).src);
return {
id: article.closest('[data-testid]')?.getAttribute('data-testid') ?? crypto.randomUUID(),
text: tweetText,
authorHandle: handle.replace('/', ''),
imageUrls: images,
element: article as HTMLElement,
};
}
```
### 3. Classification Request
```typescript
// src/models/classify.ts
export interface ClassificationResult {
filtered: boolean;
matchedCategory: string | null;
reasoning: string;
}
export async function classifyPost(
post: ExtractedPost,
filters: string[],
model: ModelConfig
): Promise<ClassificationResult> {
const prompt = buildClassificationPrompt(post.text, filters, post.imageUrls);
const response = await model.provider.complete(prompt);
return parseClassificationResponse(response);
}
function buildClassificationPrompt(
text: string,
filters: string[],
imageUrls: string[]
): string {
return `You are a content filter. Given a social media post, determine if it matches any of the user's filter categories.
Filter categories: ${filters.map(f => `"${f}"`).join(', ')}
Post text:
${text}
${imageUrls.length > 0 ? `The post contains ${imageUrls.length} image(s).` : ''}
Respond with JSON:
{
"filtered": boolean,
"matchedCategory": "category name or null",
"reasoning": "brief explanation"
}`;
}
```
### 4. Hiding Filtered Posts
```typescript
// src/content/filterUI.ts
export function hidePost(element: HTMLElement, reason: string): void {
element.style.transition = 'opacity 0.3s ease-out';
element.style.opacity = '0';
setTimeout(() => {
element.style.display = 'none';
element.dataset.bouncerFiltered = 'true';
element.dataset.bouncerReason = reason;
}, 300);
}
export function showFilteredIndicator(count: number): void {
const indicator = document.getElementById('bouncer-filtered-count');
if (indicator) indicator.textContent = `${count} filtered`;
}
```
## Adding a New AI Provider
```typescript
// src/models/providers/myProvider.ts
import type { ModelProvider, CompletionRequest, CompletionResponse } from '../types';
export class MyProvider implements ModelProvider {
private apiKey: string;
private endpoint = 'https://api.myprovider.com/v1/chat/completions';
constructor(apiKey: string) {
this.apiKey = apiKey;
}
async complete(request: CompletionRequest): Promise<CompletionResponse> {
const response = await fetch(this.endpoint, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: request.model,
messages: [{ role: 'user', content: request.prompt }],
max_tokens: 256,
}),
});
const data = await response.json();
return {
text: data.choices[0].message.content,
usage: data.usage,
};
}
}
```
Register it in the provider registry:
```typescript
// src/models/registry.ts
import { MyProvider } from './providers/myProvider';
export function createProvider(config: StoredConfig): ModelProvider {
switch (config.provider) {
case 'my-provider':
return new MyProvider(config.apiKey);
// ... other cases
}
}
```
## Result Caching
Bouncer caches classification results so repeated posts don't trigger new inference calls:
```typescript
// src/utils/cache.ts
const CACHE_KEY = 'bouncer-post-cache';
export async function getCachedResult(postId: string): Promise<ClassificationResult | null> {
const stored = await chrome.storage.local.get(CACHE_KEY);
const cache = stored[CACHE_KEY] ?? {};
return cache[postId] ?? null;
}
export async function cacheResult(postId: string, result: ClassificationResult): Promise<void> {
const stored = await chrome.storage.local.get(CACHE_KEY);
const cache = stored[CACHE_KEY] ?? {};
cache[postId] = result;
// Limit cache size
const keys = Object.keys(cache);
if (keys.length > 1000) delete cache[keys[0]];
await chrome.storage.local.set({ [CACHE_KEY]: cache });
}
```
## Filter Management
Filters are stored and retrieved via `chrome.storage.sync`:
```typescript
// src/utils/filters.ts
export async function getFilters(): Promise<string[]> {
const result = await chrome.storage.sync.get('bouncerFilters');
return result.bouncerFilters ?? [];
}
export async function addFilter(topic: string): Promise<void> {
const filters = await getFilters();
if (!filters.includes(topic)) {
await chrome.storage.sync.set({ bouncerFilters: [...filters, topic] });
}
}
export async function removeFilter(topic: string): Promise<void> {
const filters = await Related in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.