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
Platform Connections enable Genie Helper to authenticate with creator platforms for scraping stats and publishing content. Credentials are encrypted with AES-256-GCM and stored in the Directus creator_profiles collection.
Supported Platforms:
- OnlyFans
- Fansly
- Pornhub
- XVideos
- Instagram
- TikTok
- X (Twitter)
- Reddit
- YouTube
Connection Methods
1. Cookie Capture (Recommended)
Flow:
- User logs into platform via browser
- GenieHelper browser extension captures session cookies
- Cookies encrypted and stored in
platform_sessions collection
- Stagehand injects cookies to bypass login screens
Advantages:
- No credential storage required
- Works with 2FA/SSO platforms
- Session reuse reduces bot detection risk
- No password exposure
See: Browser Extension for setup.
2. Username/Password (Legacy)
Flow:
- User enters credentials in dashboard
/app/platforms
- Credentials encrypted with AES-256-GCM
- Stored in
creator_profiles.credentials field
- Stagehand uses credentials for automated login
Disadvantages:
- Fails on platforms with 2FA
- Requires password exposure
- Higher bot detection risk
- Manual login automation brittle
When to Use:
- Platforms without 2FA
- Initial setup before cookie capture
- Fallback if cookies expire
3. OAuth (Planned)
Roadmap: Phase 9E
Supported Platforms:
- Google (YouTube)
- X (Twitter)
Flow:
- User clicks “Connect with OAuth” in dashboard
- Redirected to platform OAuth consent screen
- Platform issues access token + refresh token
- Tokens encrypted and stored in
creator_profiles.credentials
- Dashboard polls for OAuth callback
Status: Not implemented (see README TODO section).
Credential Storage
Creator Profiles Collection
Collection: creator_profiles
| Field | Type | Description |
|---|
id | UUID | Primary key |
user_id | M2O (directus_users) | Creator who owns this connection |
platform | String | Platform identifier (e.g., “onlyfans”) |
username | String | Platform username (plaintext) |
credentials | JSON | Encrypted credential object |
scrape_enabled | Boolean | Enable automated scraping |
scrape_frequency | String | Cron expression (e.g., “0 */6 * * *“) |
last_scraped_at | Timestamp | Last successful scrape |
scrape_status | String | idle, running, success, error |
profile_data | JSON | Cached stats (followers, earnings) |
Unencrypted Payload (before encryption):
{
"type": "password",
"username": "creator123",
"password": "mySecretPassword"
}
Encrypted Storage (in Directus):
{
"enc": "v1:k8sQn2FpL...iv_base64:xR9pL...tag_base64:mZ3cV...ciphertext_base64"
}
Envelope Format: v1:<iv_b64>:<tag_b64>:<ciphertext_b64>
Collection: platform_sessions
| Field | Type | Description |
|---|
id | UUID | Primary key |
creator_profile_id | M2O | Link to creator profile |
platform | String | Platform name (e.g., “onlyfans”) |
cookies | JSON | Encrypted cookie array |
user_agent | String | Browser user agent |
captured_at | Timestamp | Cookie capture time |
expires_at | Timestamp | Estimated expiration (90 days) |
last_used_at | Timestamp | Last Stagehand injection |
Cookie Format (before encryption):
[
{
"name": "session_id",
"value": "abc123...",
"domain": ".onlyfans.com",
"path": "/",
"secure": true,
"httpOnly": true,
"sameSite": "Lax",
"expirationDate": 1735689600
}
]
Encryption Implementation
AES-256-GCM
Genie Helper uses AES-256-GCM (Galois/Counter Mode) for authenticated encryption:
Security Properties:
- Confidentiality: Credentials unreadable without key
- Integrity: Tampered ciphertext rejected during decryption
- Authentication: Additional Authenticated Data (AAD) prevents context swapping
Key Material:
- Encryption Key: 32 bytes (256 bits)
- IV (Initialization Vector): 12 bytes (96 bits), random per encryption
- Auth Tag: 16 bytes (128 bits), generated during encryption
- AAD: “agentx-v1” (fixed context string)
Encryption Module
Location: /home/daytona/workspace/source/server/utils/credentialsCrypto.js:1
Functions:
encryptJSON(obj): Encrypts object → returns envelope
decryptJSON(envelope): Decrypts envelope → returns object
Environment Variables:
CREDENTIALS_ENC_KEY_B64=<base64-encoded-32-bytes>
CREDENTIALS_ENC_AAD="agentx-v1" # Optional, defaults to agentx-v1
Encryption Process
encryptJSON(obj) Flow:
const crypto = require("crypto");
function encryptJSON(obj) {
// 1. Load 32-byte key from env
const key = Buffer.from(process.env.CREDENTIALS_ENC_KEY_B64, "base64");
// 2. Generate random 12-byte IV
const iv = crypto.randomBytes(12);
// 3. Create cipher
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
// 4. Set AAD (prevents envelope reuse in wrong context)
cipher.setAAD(Buffer.from("agentx-v1", "utf8"));
// 5. Encrypt JSON-stringified object
const plaintext = Buffer.from(JSON.stringify(obj), "utf8");
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
// 6. Get authentication tag
const tag = cipher.getAuthTag();
// 7. Return envelope (Directus JSON-safe)
return {
enc: `v1:${iv.toString("base64")}:${tag.toString("base64")}:${ciphertext.toString("base64")}`
};
}
Decryption Process
decryptJSON(envelope) Flow:
function decryptJSON(envelope) {
const key = Buffer.from(process.env.CREDENTIALS_ENC_KEY_B64, "base64");
// 1. Extract envelope string (handles both {enc:"v1:..."} and raw "v1:...")
const s = typeof envelope === "object" ? envelope.enc : envelope;
const [version, ivB64, tagB64, ctB64] = s.split(":");
if (version !== "v1") throw new Error("Invalid credential envelope version");
// 2. Decode base64 components
const iv = Buffer.from(ivB64, "base64");
const tag = Buffer.from(tagB64, "base64");
const ciphertext = Buffer.from(ctB64, "base64");
// 3. Create decipher
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
decipher.setAAD(Buffer.from("agentx-v1", "utf8"));
decipher.setAuthTag(tag);
// 4. Decrypt
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
// 5. Parse JSON
return JSON.parse(plaintext.toString("utf8"));
}
Key Generation
To generate a new encryption key:
# Generate 32 random bytes and encode as base64
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
# Example output:
# k8sQn2FpLxR9pLmZ3cVa1bN4dE5fG6hI7jK8lM9nO0p=
Add to .env:
CREDENTIALS_ENC_KEY_B64="k8sQn2FpLxR9pLmZ3cVa1bN4dE5fG6hI7jK8lM9nO0p="
CRITICAL: Store this key securely. Loss of key = permanent credential loss.
Dashboard Integration
Page: /app/platforms
Flow:
- User clicks “Add Platform”
- Selects platform from dropdown
- Enters username + password (or clicks “Capture Cookies”)
- Dashboard calls
/api/credentials/store-platform-credentials
- Server encrypts credentials via
encryptJSON()
- Inserts
creator_profiles record with encrypted credentials field
- Dashboard shows success + scrape configuration options
Frontend Code (dashboard/src/pages/PlatformConnect/index.jsx):
import { platformConnections } from '../../utils/api';
const handleConnect = async (platform, username, password) => {
const response = await platformConnections.store({
platform,
username,
credentials: { type: "password", username, password }
});
if (response.data.success) {
alert(`Connected to ${platform}`);
}
};
Credentials API
Endpoint: /api/credentials/store-platform-credentials
Location: /home/daytona/workspace/source/server/endpoints/api/credentials.js:1
Request:
{
"platform": "onlyfans",
"username": "creator123",
"credentials": {
"type": "password",
"username": "creator123",
"password": "mySecretPassword"
}
}
Response:
{
"success": true,
"creator_profile_id": "a3f8c2b1-..."
}
Server Implementation:
const { encryptJSON } = require('../utils/credentialsCrypto');
router.post('/store-platform-credentials', async (req, res) => {
const { platform, username, credentials } = req.body;
const userId = req.user.id; // From Directus JWT
// Encrypt credentials
const encrypted = encryptJSON(credentials);
// Insert into Directus
const result = await directusApi.post('/items/creator_profiles', {
user_id: userId,
platform,
username,
credentials: encrypted,
scrape_enabled: false
});
res.json({ success: true, creator_profile_id: result.data.data.id });
});
Scraping Configuration
Enabling Auto-Scrape
After connecting a platform, configure automated scraping:
Dashboard UI:
- Scrape Enabled: Toggle to enable/disable
- Frequency: Cron expression or preset (every 6 hours, daily, etc.)
- Last Scraped: Timestamp of last successful scrape
- Status:
idle, running, success, error
PATCH Request:
await directusApi.patch(`/items/creator_profiles/${profileId}`, {
scrape_enabled: true,
scrape_frequency: "0 */6 * * *" // Every 6 hours
});
Cron Expressions
Common Patterns:
| Expression | Meaning |
|---|
0 */6 * * * | Every 6 hours |
0 0 * * * | Daily at midnight |
0 */12 * * * | Every 12 hours |
0 0 * * 0 | Weekly on Sunday |
0 0 1 * * | Monthly on 1st |
Scheduler: The post_scheduler worker (media-worker) polls creator_profiles for enabled scrapes.
Implementation (media-worker/index.js):
setInterval(async () => {
const profiles = await directusApi.get('/items/creator_profiles', {
params: {
filter: { scrape_enabled: { _eq: true } },
fields: ['id', 'platform', 'scrape_frequency', 'last_scraped_at']
}
});
for (const profile of profiles.data.data) {
const shouldScrape = cronMatch(profile.scrape_frequency, new Date());
if (shouldScrape) {
await scrapeQueue.add('scrape_profile', {
creator_profile_id: profile.id,
platform: profile.platform
});
}
}
}, 60000); // Check every 60 seconds
Manual Scraping
Trigger from Dashboard
Button: “Scrape Now” on /app/dashboard
Flow:
- User clicks “Scrape Now”
- Dashboard checks for valid
platform_sessions record
- If no cookies: creates
hitl_sessions record → shows yellow banner
- If cookies exist: enqueues
scrape_profile job via /api/queue/enqueue
- Dashboard polls
media_jobs collection for job status
- On completion: shows scraped media count + updates
creator_profiles.last_scraped_at
Frontend Code:
import { queue } from '../../utils/api';
const handleScrapeNow = async (profileId, platform) => {
// 1. Check for cookies
const sessions = await directusApi.get('/items/platform_sessions', {
params: {
filter: {
creator_profile_id: { _eq: profileId },
platform: { _eq: platform }
},
limit: 1
}
});
if (sessions.data.data.length === 0) {
// No cookies: create HITL session
await directusApi.post('/items/hitl_sessions', {
creator_profile_id: profileId,
platform,
status: 'pending'
});
alert('Please log in via browser extension first');
return;
}
// 2. Enqueue scrape job
const job = await queue.enqueue('scrape-jobs', 'scrape_profile', {
creator_profile_id: profileId,
platform
});
// 3. Poll for completion
const jobId = job.data.media_job_id;
const interval = setInterval(async () => {
const jobStatus = await directusApi.get(`/items/media_jobs/${jobId}`);
if (jobStatus.data.data.status === 'completed') {
clearInterval(interval);
alert('Scrape complete!');
window.location.reload();
}
}, 2000);
};
Security Best Practices
Key Rotation
To rotate the encryption key:
-
Generate New Key:
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
-
Re-encrypt All Credentials:
const profiles = await directusApi.get('/items/creator_profiles');
for (const profile of profiles.data.data) {
// Decrypt with old key
const oldCreds = decryptJSON(profile.credentials);
// Update env with new key
process.env.CREDENTIALS_ENC_KEY_B64 = NEW_KEY;
// Re-encrypt
const newCreds = encryptJSON(oldCreds);
// Update record
await directusApi.patch(`/items/creator_profiles/${profile.id}`, {
credentials: newCreds
});
}
-
Update
.env on Server
-
Restart Services:
Backup & Recovery
Backup Encryption Key:
- Store
CREDENTIALS_ENC_KEY_B64 in password manager
- Add to encrypted
.env.backup file
- Store in separate location from server
Recovery:
If key is lost:
- All encrypted credentials are permanently unrecoverable
- Users must re-connect all platforms
- No decryption method exists without original key
Access Control
Environment Permissions:
# .env file should be readable only by server user
chmod 600 /path/to/agentx/.env
chown agentx:agentx /path/to/agentx/.env
Directus Permissions:
creator_profiles.credentials: Never expose via API to frontend
- Dashboard should only display connection status, never raw credentials
- Admin role can view encrypted envelope (but not decrypt without key)
Server-Side Only:
- Encryption/decryption always happens server-side
- Browser extension POSTs cookies to server (server encrypts)
- Dashboard never receives decrypted credentials
Troubleshooting
”Invalid Credential Envelope” Error
Cause: Encryption key mismatch or corrupted data.
Fix:
- Verify
CREDENTIALS_ENC_KEY_B64 matches key used during encryption
- Check envelope format starts with
v1:
- Test decryption manually:
const { decryptJSON } = require('./server/utils/credentialsCrypto');
const envelope = { enc: "v1:..." };
console.log(decryptJSON(envelope));
Scrape Job Stuck in “Running”
Cause: Stagehand session crashed or hung.
Fix:
- Check Stagehand logs:
pm2 logs stagehand-server
- Restart Stagehand:
pm2 restart stagehand-server
- Manually update job status:
await directusApi.patch(`/items/media_jobs/${jobId}`, {
status: 'failed',
error: 'Session timeout'
});
“No Cookies Found” During Scrape
Cause: platform_sessions record missing or expired.
Fix:
- Check
platform_sessions collection for matching creator_profile_id + platform
- If missing: create HITL session (dashboard does this automatically)
- If expired: re-capture cookies via browser extension
- Verify cookie
expires_at is in the future
Cause: Cookies expired, platform session invalidated, or IP change.
Fix:
- Log into platform manually in browser
- Re-capture cookies via extension
- Trigger scrape again
- If still fails: platform may have changed auth mechanism (check for OAuth migration)