Skip to main content

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

Genie Helperโ€™s post scheduler polls the scheduled_posts collection every 60 seconds and automatically publishes posts to X, Reddit, Instagram, TikTok, and other platforms using Stagehand browser automation. Collection: scheduled_posts
Worker: Media worker (BullMQ)
Poll interval: 60 seconds (configurable via POST_SCHEDULER_MS)

Architecture

Scheduling Flow

Dashboard Content Calendar
  โ†“
User creates post (AI-generated or manual)
  โ†“
Scheduled_posts record created (status: draft)
  โ†“
User sets scheduled_time + clicks "Schedule Post"
  โ†“
Status changes to: scheduled
  โ†“
Post scheduler polls every 60s
  โ†“
When scheduled_time โ‰ค now:
  โ†“
Create media_jobs record (operation: publish_post)
  โ†“
Media worker picks job
  โ†“
Stagehand session starts
  โ†“
Inject cookies from platform_sessions
  โ†“
Navigate to platform compose page
  โ†“
Fill caption + attach media (if provided)
  โ†“
Click "Post" button
  โ†“
Update scheduled_posts: status=posted, posted_at=now

Collections

scheduled_posts

FieldTypeDescription
idUUIDPrimary key
platformStringTarget platform (x, reddit, instagram, tiktok)
content_textTextPost caption/body
scheduled_timeDateTimeWhen to publish (null = draft)
statusStringdraft | scheduled | publishing | posted | failed
media_idFKDirectus file ID (optional)
clip_job_idFKMedia job ID for platform-specific clip (optional)
platform_metaJSONPlatform-specific params (see below)
posted_atDateTimeActual publish timestamp
error_messageTextFailure reason (if status=failed)
creator_idFKDirectus user ID

platform_meta Examples

{
  "subreddit": "OnlyFans101",
  "post_type": "text",
  "title": "New content drop this weekend!"
}

Post Scheduler

Implementation: media-worker/index.js:1186-1241

Polling Logic

setInterval(async () => {
  try {
    const now = new Date().toISOString();
    
    // Find all posts due for publishing
    const res = await dAPI("GET",
      `/items/scheduled_posts?filter[status][_eq]=scheduled&filter[scheduled_time][_lte]=${now}&limit=10&fields=id,platform,content_text,platform_meta,media_id,clip_job_id,creator_id`
    );
    
    const due = res?.data || [];
    
    for (const post of due) {
      // Create publish_post job
      await dAPI("POST", "/items/media_jobs", {
        operation: "publish_post",
        status: "queued",
        params: {
          scheduled_post_id: post.id,
          platform: post.platform,
          content_text: post.content_text,
          platform_meta: post.platform_meta || {},
          creator_profile_id: post.creator_profile_id,
        },
        priority: 1,
      });
      
      console.log(`[post-scheduler] queued publish job for post=${post.id} platform=${post.platform}`);
    }
  } catch (err) {
    console.error(`[post-scheduler] poll error: ${err.message}`);
  }
}, POST_SCHEDULER_MS);

Publishing Operation

Operation: publish_post (media-worker)
Browser: Local Playwright (headless Chrome)
Vision LLM: ollama/qwen-2.5

Supported Platforms

PlatformStatusCompose URLNotes
X (Twitter)โœ… Fullhttps://x.com/compose/tweet280 char limit
Redditโœ… Fullhttps://reddit.com/r/{subreddit}/submitRequires subreddit + title
Instagram๐Ÿšง PartialWeb posting disabled by MetaAPI required
TikTok๐Ÿšง PartialDesktop upload limitedMobile flow needed
OnlyFans๐Ÿ“… PlannedCookie auth required
Fansly๐Ÿ“… PlannedCookie auth required

X (Twitter) Publishing

Implementation: media-worker/index.js:879-890
async function postToX(sid, content_text) {
  // Navigate to compose page
  await publishSPost(`/v1/sessions/${sid}/navigate`, { 
    url: "https://x.com/compose/tweet" 
  });
  
  // Fill tweet text
  await publishSPost(`/v1/sessions/${sid}/act`, {
    action: `Type the following text into the tweet compose box: "${content_text.replace(/"/g, '\\"')}"`,
    modelName: PUBLISH_STAGEHAND_MODEL,
  });
  
  // Submit
  await publishSPost(`/v1/sessions/${sid}/act`, {
    action: "Click the Post button to submit the tweet",
    modelName: PUBLISH_STAGEHAND_MODEL,
  });
  
  return { platform: "x", status: "posted" };
}

Reddit Publishing

Implementation: media-worker/index.js:893-920
async function postToReddit(sid, content_text, platform_meta = {}) {
  const subreddit = platform_meta.subreddit || "u_me";
  const post_type = platform_meta.post_type || "text";
  const url = `https://www.reddit.com/r/${subreddit}/submit?type=${post_type}`;
  
  await publishSPost(`/v1/sessions/${sid}/navigate`, { url });
  
  if (post_type === "text") {
    // Fill title
    await publishSPost(`/v1/sessions/${sid}/act`, {
      action: `Fill in the title field with: "${platform_meta.title || content_text.slice(0, 100)}"`,
      modelName: PUBLISH_STAGEHAND_MODEL,
    });
    
    // Fill body
    await publishSPost(`/v1/sessions/${sid}/act`, {
      action: `Fill in the body/text field with: "${content_text}"`,
      modelName: PUBLISH_STAGEHAND_MODEL,
    });
  }
  
  // Submit
  await publishSPost(`/v1/sessions/${sid}/act`, {
    action: "Click the Post button to submit",
    modelName: PUBLISH_STAGEHAND_MODEL,
  });
  
  return { platform: "reddit", subreddit, status: "posted" };
}
The worker automatically loads cookies from platform_sessions to bypass login:
if (creator_profile_id) {
  try {
    const sessRes = await dAPI("GET",
      `/items/platform_sessions?filter[creator_profile_id][_eq]=${creator_profile_id}&filter[platform][_eq]=${platform}&filter[status][_eq]=active&limit=1&fields=encrypted_cookies`
    );
    
    const sess = sessRes?.data?.[0];
    if (sess?.encrypted_cookies) {
      const plain = decryptCredentials(sess.encrypted_cookies);
      if (plain?.cookies) {
        cookies = plain.cookies;
      }
    }
  } catch { /* no cookies โ€” will fall back to plain navigation */ }
}

// Inject into Playwright session
if (cookies.length > 0) {
  await publishSPost(`/v1/sessions/${sid}/cookies`, { cookies });
}

Content Calendar UI

Page: dashboard/src/pages/ContentCalendar/index.jsx

Features

  1. AI Caption Generator (Modal 3A)
    • Platform selector (TikTok, Instagram, X, Reddit, OnlyFans)
    • Tone selector (flirty, playful, intimate, professional, casual, teasing)
    • Topic input (optional)
    • Length picker (short, medium, long)
    • Try Again feedback loop with adjustment notes
  2. Post Draft Editor (Modal 3B)
    • Platform specs validation (text limits, video dimensions)
    • Media attachment from scraped_media
    • Platform-specific clip generator (9:16 for TikTok/Snapchat)
    • Schedule time picker
    • Usage pill (plan limits)
  3. Media Picker (Modal 3C)
    • Grid view of scraped_media
    • Search filter
    • Video duration + codec display

Plan Limits

Tracked in: usage API (/api/usage/increment)
TierPosts/MonthQueue Size
Starter33
Creator30Unlimited
Pro150Unlimited
StudioUnlimitedUnlimited
Enforcement: dashboard/src/pages/ContentCalendar/index.jsx:220-227
async function handleSave(scheduleNow) {
  if (limitHit && scheduleNow) {
    return alert('Post limit reached for this month. Upgrade your plan.');
  }
  
  if (scheduleNow) {
    const inc = await incrementUsage('post_creations');
    if (!inc.ok) return alert('Post limit reached. Upgrade.');
  }
  
  await posts.create({ ...draftPost, status: scheduleNow ? 'scheduled' : 'draft' });
}

AI Caption Generation

Endpoint: /api/captions/generate
Model: dolphin-mistral:7b (uncensored content writer)

Request

const res = await fetch('/api/llm/api/captions/generate', {
  method: 'POST',
  headers: { 
    'Content-Type': 'application/json',
    Authorization: `Bearer ${token}` 
  },
  body: JSON.stringify({
    platform: 'tiktok',
    tone: 'flirty',
    topic: 'new photo set',
    length: 'medium'
  })
});

const { caption } = await res.json();
// "Can't wait to show you what I've been working on... ๐Ÿ˜ New set drops tonight. Link in bio ๐Ÿ’• #newcontent #exclusive"

Feedback Loop

After first generation, user can click โ€œTry Againโ€ โ†’ Enters feedback note โ†’ Re-generates with adjustment:
async function generateContent(feedbackNote) {
  setGenerating(true);
  
  const res = await fetch('/api/llm/api/captions/generate', {
    method: 'POST',
    body: JSON.stringify({
      platform: genPrompt.platform,
      tone: genPrompt.tone,
      topic: feedbackNote 
        ? `${genPrompt.topic} [Adjustment: ${feedbackNote}]`  // Inject feedback
        : genPrompt.topic,
      length: genPrompt.length,
    }),
  });
  
  const data = await res.json();
  setGeneratedText(data.caption);
}
Example feedback: โ€œMake it shorter, add a call to action, more emojisโ€

Status Flow

draft
  โ†“ (user clicks "Schedule Post")
scheduled
  โ†“ (scheduler picks up)
publishing
  โ†“ (Stagehand completes)
posted โœ“

  OR
  โ†“ (error)
failed โš ๏ธ
Error Messages: Stored in scheduled_posts.error_message Common failures:
  • HITL_REQUIRED โ€” No cookies available
  • Stagehand timeout โ€” Page load failed
  • Auto-posting to {platform} not yet implemented โ€” Platform stub

Logs & Debugging

pm2 logs media-worker | grep post-scheduler

# Watch polling in real-time
pm2 logs media-worker -f | grep -E "post-scheduler|publish_post"

Build docs developers (and LLMs) love