Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Kismetkanceled/geniehelper/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Stagehand is a browser automation framework that powers Genie Helper’s platform scraping and publishing workflows. It provides AI-driven browser control using natural language commands.
Key Features:
- AI-Powered Actions: “Click the login button” instead of CSS selectors
- Cookie Injection: Bypass login screens by reusing captured sessions
- Data Extraction: Extract structured JSON from any webpage
- Screenshot Capture: Visual debugging and content archival
- Stealth Mode: Anti-bot detection with randomized user agents
Service Details:
- Port: 3002
- Process:
pm2 stagehand-server
- Browser: Local Playwright (Chromium)
- LLM: Ollama
qwen-2.5 for action planning
Architecture
Stagehand in the Stack
AnythingLLM Agent → Stagehand MCP (9 tools) → Stagehand Server (port 3002)
↓
Playwright Browser
↓
OnlyFans / Fansly / Instagram / etc.
MCP Integration
Stagehand is exposed to the AnythingLLM agent via the Stagehand MCP Server.
Location: /home/daytona/workspace/source/scripts/stagehand-mcp-server.mjs:1
9 MCP Tools:
| Tool | Description |
|---|
start-session | Initialize new browser session |
navigate | Load URL in existing session |
act | Perform action via natural language (click, type, scroll) |
extract | Extract structured data using AI |
observe | List interactive elements on page |
screenshot | Capture current page as image |
get-cookies | Read all cookies from session |
set-cookies | Inject cookie array (for authenticated sessions) |
close-session | End session and cleanup browser |
Stagehand Models
LLM for Actions: Stagehand uses an Ollama model for understanding natural language actions and page structure.
Default Model: ollama/qwen-2.5
Configuration:
export STAGEHAND_MODEL="ollama/qwen-2.5"
export STAGEHAND_URL="http://127.0.0.1:3002"
Model Selection Criteria:
- Fast inference (< 5s per action)
- Good DOM understanding
- Reliable JSON output
- Small memory footprint (< 5GB RAM)
Alternative Models:
ollama/dolphin-mistral:7b: Uncensored, good for adult content selectors
ollama/llama3.2:3b: Faster but less accurate
ollama/phi-3.5: Lightweight fallback
Browser Sessions
Starting a Session
MCP Tool: start-session
Parameters:
headless (boolean, optional): Run browser without GUI (default: true)
Example:
// Via MCP tool
const result = await mcpClient.callTool('start-session', {
headless: true
});
const sessionId = result.sessionId;
HTTP Equivalent:
curl -X POST http://127.0.0.1:3002/v1/sessions/start \
-H "Content-Type: application/json" \
-d '{
"modelName": "ollama/qwen-2.5",
"browser": {
"type": "local",
"launchOptions": {
"headless": true,
"args": [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
"--disable-blink-features=AutomationControlled",
"--window-size=1920,1080"
]
}
}
}'
Response:
{
"sessionId": "sess_a3f8c2b1",
"status": "started"
}
Session Lifecycle
Each session is a dedicated Chromium browser instance:
- Memory: ~300MB RAM per session
- Concurrency Limit: ~33 sessions (10GB RAM available)
- Auto-Cleanup: Sessions expire after 30 minutes of inactivity
- Manual Cleanup: Always call
close-session when done
Closing a Session
MCP Tool: close-session
Parameters:
session_id (string): Session ID from start-session
Example:
await mcpClient.callTool('close-session', {
session_id: sessionId
});
HTTP Equivalent:
curl -X POST http://127.0.0.1:3002/v1/sessions/sess_a3f8c2b1/end \
-H "Content-Type: application/json" \
-d '{}'
Scraping Workflows
Cookie-Based Authentication
The primary scraping pattern uses captured cookies to bypass login:
Flow:
- Capture Cookies: User logs into platform → browser extension captures cookies
- Store Encrypted: Cookies stored in
platform_sessions collection (AES-256-GCM)
- Start Session: Create new Stagehand browser session
- Inject Cookies: Use
set-cookies tool to inject captured cookies
- Navigate: Load creator profile page (now authenticated)
- Extract Data: Use
extract tool to scrape stats, posts, earnings
- Close Session: Clean up browser
Action Runner Step (stepExecutors.js:222):
async stagehand_cookie_login(config, signal) {
const { creator_profile_id, platform } = config;
// 1. Fetch encrypted cookies from Directus
const sessRes = await directusFetch(
`/items/platform_sessions?filter[creator_profile_id]=${creator_profile_id}&filter[platform]=${platform}&limit=1`
);
if (!sessRes.data?.[0]?.cookies) {
throw new Error(`No cookies found for ${platform}. Create HITL session.`);
}
const cookies = JSON.parse(decryptJSON(sessRes.data[0].cookies));
// 2. Start Stagehand session
const startRes = await stagehandFetch("/v1/sessions/start", {
modelName: "ollama/qwen-2.5",
browser: { type: "local", launchOptions: { headless: true } }
});
const sessionId = startRes.sessionId;
// 3. Inject cookies
await stagehandFetch(`/v1/sessions/${sessionId}/cookies`, { cookies });
// 4. Navigate to platform
const targetUrl = PLATFORM_URLS[platform]; // e.g., "https://onlyfans.com/my/profile"
await stagehandFetch(`/v1/sessions/${sessionId}/navigate`, { url: targetUrl });
return { sessionId };
}
MCP Tool: extract
Parameters:
session_id (string): Active session ID
instruction (string): What to extract in natural language
schema (object, optional): JSON schema for structured output
Example: Scrape OnlyFans Profile Stats:
const result = await mcpClient.callTool('extract', {
session_id: sessionId,
instruction: "Extract the creator's follower count, earnings this month, and total posts",
schema: {
followers: "number",
monthly_earnings: "number",
total_posts: "number"
}
});
// Result:
{
followers: 12543,
monthly_earnings: 8723.50,
total_posts: 342
}
HTTP Equivalent:
curl -X POST http://127.0.0.1:3002/v1/sessions/sess_a3f8c2b1/extract \
-H "Content-Type: application/json" \
-d '{
"instruction": "Extract follower count, earnings, and total posts",
"schema": {
"followers": "number",
"monthly_earnings": "number",
"total_posts": "number"
}
}'
Example: OnlyFans Profile Scrape
Full Workflow (from seed_platform_scrape_flow.mjs:1):
const flow = {
slug: "onlyfans_scrape",
steps: [
{
type: "stagehand_cookie_login",
config: {
creator_profile_id: "{{user.creator_profile_id}}",
platform: "onlyfans"
}
},
{
type: "stagehand_extract",
config: {
sessionId: "{{prev.sessionId}}",
instruction: "Extract profile stats: followers, earnings, recent posts",
schema: {
followers: "number",
monthly_earnings: "number",
recent_posts: [{
post_id: "string",
caption: "string",
likes: "number",
comments: "number",
media_type: "string",
published_at: "string"
}]
}
}
},
{
type: "directus_create_items",
config: {
collection: "scraped_media",
items: "{{prev.recent_posts}}"
}
},
{
type: "stagehand_close",
config: {
sessionId: "{{steps[0].sessionId}}"
}
}
]
};
Publishing Workflows
Cross-Platform Posting
Stagehand also handles automated content publishing to creator platforms.
Supported Platforms:
- OnlyFans
- Fansly
- Instagram
- TikTok
- X (Twitter)
- Reddit
Publishing Flow
Worker Job: publish_post (media-worker queue)
Location: /home/daytona/workspace/source/media-worker/index.js:854
Steps:
- Fetch Post Data: Get
scheduled_posts record + media file
- Start Session: Create Stagehand session
- Inject Cookies: Load platform cookies from
platform_sessions
- Navigate to Upload: Go to platform’s upload page
- Upload Media: Use
act tool to interact with file input
- Set Caption: Type caption via
act tool
- Submit Post: Click publish button via
act tool
- Verify: Screenshot + extract post URL
- Update Record: Mark
scheduled_posts status as published
- Close Session: Clean up browser
Example: OnlyFans Post
Action Sequence:
// 1. Navigate to upload page
await stagehandFetch(`/v1/sessions/${sessionId}/navigate`, {
url: "https://onlyfans.com/my/profile/upload"
});
// 2. Upload media file
await stagehandFetch(`/v1/sessions/${sessionId}/act`, {
action: "Click the 'Upload Media' button and select the file"
});
// 3. Set caption
await stagehandFetch(`/v1/sessions/${sessionId}/act`, {
action: `Type the caption: "${caption}" into the text area`
});
// 4. Set price (if paid post)
await stagehandFetch(`/v1/sessions/${sessionId}/act`, {
action: "Click the price button and set price to $19.99"
});
// 5. Publish
await stagehandFetch(`/v1/sessions/${sessionId}/act`, {
action: "Click the Publish button"
});
// 6. Verify
const result = await stagehandFetch(`/v1/sessions/${sessionId}/extract`, {
instruction: "Extract the published post URL",
schema: { post_url: "string" }
});
Natural Language Actions
MCP Tool: act
Parameters:
session_id (string): Active session ID
action (string): Natural language description of action
Supported Actions:
- Click: “Click the Login button”, “Click the 3rd profile image”
- Type: “Type ‘hello@example.com’ into the email field”
- Scroll: “Scroll down 500 pixels”, “Scroll to the bottom of the page”
- Select: “Select ‘Premium’ from the subscription dropdown”
- Upload: “Upload the file ‘image.jpg’ to the file input”
- Wait: “Wait for the loading spinner to disappear”
Examples:
// Login flow
await mcpClient.callTool('act', {
session_id: sessionId,
action: "Type 'myusername' into the username field"
});
await mcpClient.callTool('act', {
session_id: sessionId,
action: "Type 'mypassword' into the password field"
});
await mcpClient.callTool('act', {
session_id: sessionId,
action: "Click the Login button"
});
// Wait for navigation
await mcpClient.callTool('act', {
session_id: sessionId,
action: "Wait for the dashboard to load"
});
Action Reliability
Factors Affecting Success:
- Page Complexity: Simple forms work better than complex SPAs
- Dynamic Content: React/Vue apps may need wait times
- CAPTCHA: Cannot bypass human verification
- Rate Limiting: Repeated actions may trigger bot detection
Best Practices:
- Use cookie injection instead of login automation when possible
- Add explicit waits after navigation:
"Wait 3 seconds"
- Use screenshots to debug failed actions
- Fallback to CSS selectors for critical paths
Cookie Management
Setting Cookies
MCP Tool: set-cookies
Parameters:
session_id (string): Active session ID
cookies (array): Cookie objects
Cookie Object Format:
{
name: "session_id",
value: "abc123...",
domain: ".onlyfans.com",
path: "/",
secure: true,
httpOnly: true,
sameSite: "Lax",
expirationDate: 1735689600
}
Example:
const cookies = [
{ name: "auth_token", value: "xyz789", domain: ".onlyfans.com" },
{ name: "sess_id", value: "sess_abc", domain: ".onlyfans.com" }
];
await mcpClient.callTool('set-cookies', {
session_id: sessionId,
cookies: cookies
});
Getting Cookies
MCP Tool: get-cookies
Parameters:
session_id (string): Active session ID
Example:
const result = await mcpClient.callTool('get-cookies', {
session_id: sessionId
});
// Result: array of cookie objects
[
{ name: "auth_token", value: "xyz789", domain: ".onlyfans.com", ... },
{ name: "sess_id", value: "sess_abc", domain: ".onlyfans.com", ... }
]
Use Case: Capture cookies after successful login automation for future reuse.
Screenshots
Capturing Screenshots
MCP Tool: screenshot
Parameters:
session_id (string): Active session ID
Example:
const result = await mcpClient.callTool('screenshot', {
session_id: sessionId
});
// Result: base64-encoded PNG
{
screenshot: "data:image/png;base64,iVBORw0KGgoAAAANS..."
}
Use Cases:
- Debug failed extractions
- Verify successful posts
- Archive page states
- HITL error reporting
Storing Screenshots
To save screenshots to Directus:
const { screenshot } = await mcpClient.callTool('screenshot', { session_id: sessionId });
// Decode base64 to buffer
const buffer = Buffer.from(screenshot.split(',')[1], 'base64');
// Upload to Directus
const formData = new FormData();
formData.append('file', buffer, 'screenshot.png');
const uploadRes = await directusApi.post('/files', formData);
const fileId = uploadRes.data.data.id;
// Link to media_jobs record
await directusApi.patch(`/items/media_jobs/${jobId}`, {
screenshot_file_id: fileId
});
Resource Usage
Per Session:
- RAM: ~300MB
- CPU: ~10% during active browsing, ~0% idle
- Disk: ~50MB temp files (auto-cleanup)
Server Limits (IONOS VPS):
- Total RAM: 10GB available for Stagehand
- Max Sessions: ~33 concurrent (10GB / 300MB)
- Current Usage: ~4-6 sessions during peak hours
Action Latency
Typical Timings:
start-session: 2-5 seconds
navigate: 1-3 seconds
act: 3-8 seconds (includes LLM inference)
extract: 5-15 seconds (depends on page complexity)
screenshot: 0.5-1 second
close-session: 0.5-1 second
Optimization Tips:
- Reuse sessions for multiple operations
- Cache extraction results (see
extractCache.js)
- Use parallel sessions for bulk scraping
- Disable screenshots unless debugging
Location: /home/daytona/workspace/source/server/utils/cache/extractCache.js:2
TTL: 5 minutes
Cache Key: Hash of sessionId + instruction + schema
Usage (Action Runner):
async stagehand_extract_cached(config, signal) {
const { sessionId, instruction, schema } = config;
const cacheKey = hash({ sessionId, instruction, schema });
// Check cache first
const cached = extractCache.get(cacheKey);
if (cached) return cached;
// Fallback to real extraction
const result = await stagehandFetch(
`/v1/sessions/${sessionId}/extract`,
{ instruction, schema }
);
// Store in cache
extractCache.set(cacheKey, result, 300); // 5 min TTL
return result;
}
Stealth & Anti-Detection
Browser Fingerprinting
Stagehand uses stealth techniques to avoid bot detection:
Launch Args (stagehand-mcp-server.mjs:50):
launchOptions: {
headless: true,
args: [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
"--disable-blink-features=AutomationControlled", // Hide webdriver flag
"--window-size=1920,1080" // Standard desktop size
]
}
User Agent Rotation
Cookie reuse includes original user agent:
Flow:
- Browser extension captures cookies +
navigator.userAgent
- Stored in
platform_sessions.user_agent
- Stagehand session sets matching user agent before cookie injection
Implementation (media-worker):
const session = await directusApi.get(
`/items/platform_sessions?filter[platform]=${platform}`
);
const userAgent = session.data.data[0].user_agent;
const startRes = await stagehandFetch("/v1/sessions/start", {
browser: {
launchOptions: {
args: [`--user-agent=${userAgent}`]
}
}
});
Rate Limiting
To avoid platform bans:
- Scrape Frequency: Configurable via
creator_profiles.scrape_frequency (cron)
- Recommended: Every 6 hours (
0 */6 * * *)
- Minimum: Every 1 hour (more frequent = higher risk)
Error Handling
Common Errors
1. Session Not Found
{
"error": "Session sess_abc123 not found"
}
Cause: Session expired or was closed.
Fix: Start a new session.
2. Action Failed
{
"error": "Could not find element matching: 'Login button'"
}
Cause: Page structure changed or action ambiguous.
Fix:
- Take a screenshot to verify page state
- Use more specific action description
- Add a wait before action:
"Wait 2 seconds then click Login"
3. Extraction Failed
{
"error": "Extraction timeout after 30s"
}
Cause: LLM took too long to parse page.
Fix:
- Simplify schema (fewer fields)
- Navigate to simpler sub-page
- Use
observe tool to verify content exists
Error Propagation
Stagehand MCP server surfaces HTTP errors to the agent:
MCP Error Handling (stagehand-mcp-server.mjs:26):
if (!res.ok) {
const errMsg = (typeof data === 'object' && data !== null)
? (data.error || data.message || JSON.stringify(data))
: String(data || res.statusText);
throw new Error(`Stagehand error ${res.status}: ${errMsg}`);
}
Action Runner catches and logs errors:
try {
const result = await executors.stagehand_extract(config, signal);
} catch (err) {
await directusApi.post('/items/agent_audits', {
action_slug: 'stagehand_extract',
status: 'error',
error_message: err.message
});
throw err; // Re-throw to fail the flow
}
Troubleshooting
Check Stagehand Service
pm2 status stagehand-server
pm2 logs stagehand-server --lines 50
Restart Stagehand
pm2 restart stagehand-server
Test Session Manually
# Start session
curl -X POST http://127.0.0.1:3002/v1/sessions/start \
-H "Content-Type: application/json" \
-d '{"modelName":"ollama/qwen-2.5","browser":{"type":"local"}}'
# Extract sessionId from response, then navigate
curl -X POST http://127.0.0.1:3002/v1/sessions/SESS_ID/navigate \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com"}'
# Take screenshot
curl -X POST http://127.0.0.1:3002/v1/sessions/SESS_ID/screenshot
- Take screenshot before extraction
- Use
observe tool to list available elements
- Simplify instruction to single field
- Check Ollama model is running:
curl http://localhost:11434/api/tags