Skill: story91/multi-channel-engagement-agent (version 1.0.3)

--- name: multi-channel-engagement-agent description: Autonomous social media engagement across Twitter, Farcaster, and Moltbook. Fetches trending content, generates persona-driven contextual replies, and tracks state to prevent duplicates. Use when you want to engage with trending posts, reply to social media content, build audience through authentic engagement, or automate social presence across multiple platforms. Triggers on "engage on twitter", "farcaster engagement", "reply to trending", "social engagement bot", "multi-platform engagement", "autonomous social replies". Features include content filtering, mention tracking, webhook notifications, user blacklist/whitelist, analytics tracking, and quote tweet/recast support. --- # Multi-Channel Engagement Agent Autonomous engagement bot for **Twitter**, **Farcaster**, and **Moltbook**. Fetches trending content, generates persona-driven contextual replies, tracks replied posts to prevent duplicates. ## Quick Start ### 1. Create Config Copy `assets/sample-config.json` to `config.json` and fill in your credentials (see Setup Guides section below). ### 2. Run Engagement ```bash # Engage on specific platform node scripts/engage.mjs --platform twitter node scripts/engage.mjs --platform farcaster node scripts/engage.mjs --platform moltbook # Engage on all enabled platforms node scripts/engage.mjs --all ``` ## Dependencies & Setup Guides This skill integrates multiple platforms. Setup each one: ### Farcaster Setup (required for Farcaster engagement) **Skill:** `farcaster-agent` (https://clawhub.com/skills/farcaster-agent) **Prerequisites:** - Minimum **$1 ETH or USDC** on any chain (Ethereum, Optimism, Base, Arbitrum, Polygon) - **Minimum 0.0005 ETH on Optimism** for FID registration **Auto-setup command:** ```bash clawhub install farcaster-agent PRIVATE_KEY=0x... node src/auto-setup.js "Your first cast" ``` **What you'll get:** ```json { "fid": 123456, "neynarApiKey": "...", "signerPrivateKey": "...", "custodyPrivateKey": "0x..." } ``` **Cost breakdown:** - FID registration: ~$0.20 (requires 0.0005 ETH + gas) - Signer key: ~$0.05 - Bridging: ~$0.10-0.20 - **Total: ~$0.50 (budget $1 for safety)** **Neynar API:** - Free tier: 300 requests/minute - Get key: https://dev.neynar.com --- ### Twitter Setup (required for Twitter engagement) **Two options:** **Option A: x-api (OAuth 1.0a, official)** - Get credentials at https://developer.x.com/en/portal/dashboard - Create Project → App - Set permissions: **Read and Write** - Rate limits: Tweets 50/15min, Searches 450/15min **Option B: AISA API (alternative, good for trending)** - AISA API endpoint: `https://api.aisa.one/apis/v1/twitter/tweet/advanced_search` - Get API key at https://aisa.one - Searches via AISA are fast and reliable for trending - Config: add `aisaTwitterApiKey` to twitter platform **Recommendation:** Use AISA for trending discovery, x-api for posting (replies) --- ### Moltbook Setup (required for Moltbook engagement) **API Base:** `https://www.moltbook.com/api/v1` (note: use `www`) **Get API key:** 1. Register at https://www.moltbook.com 2. Get token from account settings 3. Verify: https://www.moltbook.com/api/v1/posts **⚠️ CRITICAL:** Only send API key to `www.moltbook.com`, never to other domains **Verification:** Posts require solving math captcha (automated in this skill) --- ### Summary Config All credentials go into `config.json`: ```json { "platforms": { "twitter": { "oauth": {...} }, "farcaster": { "neynarApiKey": "...", "fid": 123, ... }, "moltbook": { "apiKey": "..." } } } ``` --- ## Core Workflow ### Step 1: Load Configuration - Read `config.json` for platform credentials - Load persona settings (tone, values, style) - Load state from `engagement-state.json` (replied posts) ### Step 2: Fetch Trending **Twitter (OAuth 1.0a via x-api approach):** ```javascript // Uses twitter-api-v2 with OAuth 1.0a const client = new TwitterApi({ appKey: config.twitter.oauth.consumerKey, appSecret: config.twitter.oauth.consumerSecret, accessToken: config.twitter.oauth.accessToken, accessSecret: config.twitter.oauth.accessTokenSecret }); const trending = await client.v2.search('crypto OR web3 OR base', { max_results: 10 }); ``` **Farcaster (Neynar API):** ```javascript const response = await fetch('https://api.neynar.com/v2/farcaster/feed/trending?limit=5', { headers: { 'x-api-key': config.farcaster.neynarApiKey } }); ``` **Moltbook:** ```javascript const response = await fetch('https://www.moltbook.com/api/v1/posts/trending', { headers: { 'Authorization': `Bearer ${config.moltbook.apiKey}` } }); ``` ### Step 3: Filter Already Replied - Load `engagement-state.json` - Filter out posts with IDs in `repliedPosts[platform]` - Select random unreplied post from remaining ### Step 4: Generate Contextual Reply Based on persona config, analyze post content and generate reply: **Reply Generation Rules:** 1. **Read the post carefully** - understand topic, tone, intent 2. **Match persona** - use configured tone, values, signature emoji 3. **Add specific value** - technical insight, question, or genuine reaction 4. **Avoid generic praise** - no "Great post!", "Love this!" 5. **Keep it natural** - crypto slang if persona dictates, short sentences **Tone Balance (configurable):** - Educational: technical insights, explanations, resources - Community Vibes: celebration, encouragement, connection - Humor: wit, self-aware jokes, memes (when appropriate) ### Step 5: Post Reply **Twitter:** ```javascript await client.v2.reply(replyText, originalTweetId); ``` **Farcaster (via farcaster-agent pattern):** ```javascript // Uses post-cast.js with PARENT_FID + PARENT_HASH const result = await postCast({ privateKey: config.farcaster.custodyPrivateKey, signerPrivateKey: config.farcaster.signerPrivateKey, fid: config.farcaster.fid, text: replyText, parentFid: originalCast.author.fid, parentHash: originalCast.hash }); ``` **Moltbook:** ```javascript await fetch('https://www.moltbook.com/api/v1/comments', { method: 'POST', headers: { 'Authorization': `Bearer ${config.moltbook.apiKey}` }, body: JSON.stringify({ postId, content: replyText }) }); ``` ### Step 6: Update State ```json { "lastUpdated": "2026-02-12T11:00:00Z", "repliedPosts": { "twitter": ["1234567890", "0987654321"], "farcaster": ["0xabc123...", "0xdef456..."], "moltbook": ["uuid-1", "uuid-2"] }, "stats": { "totalReplies": 47, "byPlatform": { "twitter": 20, "farcaster": 15, "moltbook": 12 } } } ``` ## Persona Configuration Guide See [references/persona-config.md](references/persona-config.md) for detailed persona setup. **Quick Examples:** ```json // Crypto-native builder { "tone": "crypto-native, technical, supportive", "signatureEmoji": "🦞", "values": ["shipping", "community", "open-source"], "phrases": ["ships > talks", "ser", "wagmi", "based"] } // Professional analyst { "tone": "professional, analytical, educational", "signatureEmoji": "📊", "values": ["accuracy", "depth", "clarity"], "phrases": ["data suggests", "worth noting", "key insight"] } ``` ## Platform-Specific Notes See [references/platform-apis.md](references/platform-apis.md) for API details. **Twitter:** OAuth 1.0a required. Rate limits: 50 tweets/15min, 300 tweets/3hr. **Farcaster:** Neynar API + x402 payments (0.001 USDC/call). Requires FID + signer key. **Moltbook:** API key auth. Verification captcha for posts/comments. ## Reply Quality Guidelines See [references/reply-strategies.md](references/reply-strategies.md) for detailed strategies. **Golden Rules:** 1. **Specific > Generic** - If you can't add specific value, stay silent 2. **Quality > Quantity** - One thoughtful reply beats five generic ones 3. **Authentic > Performative** - Sound human, not bot 4. **Value > Visibility** - Help the community, don't just farm engagement **What Works:** ✅ Technical questions showing understanding ✅ Specific insights from experience ✅ Genuine celebration with substance ✅ Helpful resources and connections **What Fails:** ❌ Generic praise ("Love this!", "Great post!") ❌ Corporate speak ("excited to announce") ❌ Surface-level comments ❌ Forced humor ## Cron Integration To run automatically, create a cron job: ```json { "name": "Multi-Channel Engagement - Every 6h", "schedule": { "kind": "cron", "expr": "0 */6 * * *" }, "payload": { "kind": "agentTurn", "message": "Run multi-channel-engagement-agent: engage on all platforms", "model": "haiku" } } ``` ## Advanced Features ### Content Filters Skip spam, scams, and low-quality content automatically. ```json "filters": { "skipKeywords": ["airdrop", "free money", "send dm", "check bio"], "minEngagement": { "likes": 5, "replies": 2 }, "skipBots": true, "languageFilter": ["en", "es"] } ``` ### Mention Tracking Reply to mentions of your account, not just trending. ```bash node scripts/engage.mjs --mentions --platform=twitter ``` ### Webhook Notifications Send engagement results to Telegram or Discord. ```json "webhooks": { "telegram": { "enabled": true, "botToken": "YOUR_BOT_TOKEN", "chatId": "YOUR_CHAT_ID" }, "discord": { "enabled": false, "webhookUrl": "https://discord.com/api/webhooks/..." } } ``` ### User Blacklist/Whitelist Skip bots, prioritize builders. ```json "users": { "blacklist": ["spambot123", "scammer456"], "whitelist": ["jessepollak", "vitalik"], "prioritizeVerified": true } ``` ### Analytics Tracking Track engagement stats over time in `analytics.json`. ```json { "daily": { "2026-02-12": { "replies": 4, "platforms": { "twitter": 2, "farcaster": 2 }, "engagement": { "likes": 15, "replies": 3 } } }, "allTime": { "totalReplies": 247, "avgEngagement": 4.2 } } ``` ### Quote Support Quote tweets/recasts instead of direct replies. ```bash node scripts/engage.mjs --quote --platform=twitter node scripts/engage.mjs --quote --platform=farcaster ``` ## Troubleshooting **"Already replied to all trending"** - All top posts already engaged. Wait for new trending content. **Twitter rate limit** - Wait 15 minutes. Consider reducing frequency. **Farcaster "unknown fid"** - Hub not synced. Wait 30-60 seconds. **Moltbook verification failed** - Solve the math captcha in verification response. ## Files - `scripts/engage.mjs` - Main engagement script - `scripts/fetch-trending.mjs` - Fetch trending by platform - `scripts/generate-reply.mjs` - Persona-driven reply generation - `scripts/post-reply.mjs` - Post reply to platform - `references/persona-config.md` - Persona configuration guide - `references/platform-apis.md` - Platform API documentation - `references/reply-strategies.md` - Reply quality strategies

path: assets/sample-config.json
content: {
  "platforms": {
    "twitter": {
      "enabled": true,
      "oauth": {
        "consumerKey": "YOUR_CONSUMER_KEY",
        "consumerSecret": "YOUR_CONSUMER_SECRET",
        "accessToken": "YOUR_ACCESS_TOKEN",
        "accessTokenSecret": "YOUR_ACCESS_TOKEN_SECRET"
      }
    },
    "farcaster": {
      "enabled": true,
      "neynarApiKey": "YOUR_NEYNAR_API_KEY",
      "fid": 123456,
      "signerPrivateKey": "YOUR_SIGNER_PRIVATE_KEY_HEX_NO_0X",
      "custodyPrivateKey": "0xYOUR_CUSTODY_PRIVATE_KEY"
    },
    "moltbook": {
      "enabled": true,
      "apiKey": "YOUR_MOLTBOOK_API_KEY"
    }
  },
  "persona": {
    "name": "YourAgentName",
    "bio": "Your agent's identity and purpose",
    "tone": "crypto-native, authentic, helpful",
    "signatureEmoji": "🤖",
    "values": ["community", "building", "shipping", "helping-newcomers"],
    "avoidWords": ["excited to announce", "thrilled to share", "love this", "great post"],
    "phrases": ["ships > talks", "ser", "wagmi", "based", "gm"],
    "toneBalance": {
      "educational": 60,
      "communityVibes": 25,
      "humor": 15
    }
  },
  "filters": {
    "skipKeywords": ["airdrop", "free money", "send dm", "check bio", "dm me"],
    "minEngagement": {
      "likes": 5,
      "replies": 2
    },
    "skipBots": true,
    "languageFilter": ["en"]
  },
  "users": {
    "blacklist": [],
    "whitelist": [],
    "prioritizeVerified": true,
    "prioritizeBuilders": true
  },
  "webhooks": {
    "telegram": {
      "enabled": false,
      "botToken": "YOUR_BOT_TOKEN",
      "chatId": "YOUR_CHAT_ID"
    },
    "discord": {
      "enabled": false,
      "webhookUrl": "https://discord.com/api/webhooks/..."
    }
  },
  "settings": {
    "maxRepliesPerRun": 1,
    "trendingLimit": 5,
    "stateFile": "engagement-state.json",
    "analyticsFile": "analytics.json",
    "enableQuotes": false,
    "enableMentionTracking": true
  }
}


path: references/persona-config.md
content: # Persona Configuration Guide

Your persona defines how the agent engages. Configure it to match your brand voice.

## Core Fields

### `name`
Your agent's display name. Used in logs and reports.

### `bio`
One-line identity. Helps agent understand its role.
Example: "Autonomous agent on Base, builder advocate, ships > talks"

### `tone`
Comma-separated tone descriptors.
Examples:
- "crypto-native, authentic, helpful"
- "professional, analytical, educational"
- "casual, witty, supportive"

### `signatureEmoji`
Single emoji to sign off replies. Creates brand recognition.
Examples: 🦞 🤖 🔥 💡 🚀

### `values`
Array of core values guiding engagement decisions.
```json
"values": ["community", "building", "shipping", "helping-newcomers"]
```

### `avoidWords`
Phrases to never use. Filters out corporate speak.
```json
"avoidWords": ["excited to announce", "thrilled to share", "love this"]
```

### `toneBalance`
Percentage mix for reply generation.
```json
"toneBalance": {
  "educational": 60,    // Technical insights, explanations
  "communityVibes": 25, // Celebration, encouragement
  "humor": 15           // Wit, personality
}
```

## Persona Templates

### Crypto-Native Builder
```json
{
  "name": "BuilderBot",
  "bio": "Autonomous agent helping builders ship on Base",
  "tone": "crypto-native, technical, supportive",
  "signatureEmoji": "🦞",
  "values": ["shipping", "community", "open-source", "helping-newcomers"],
  "avoidWords": ["excited to announce", "thrilled", "amazing"],
  "phrases": ["ships > talks", "ser", "wagmi", "based", "gm"],
  "toneBalance": { "educational": 60, "communityVibes": 25, "humor": 15 }
}
```

### DeFi Analyst
```json
{
  "name": "DeFiAnalyst",
  "bio": "On-chain data analyst tracking yield opportunities",
  "tone": "professional, analytical, data-driven",
  "signatureEmoji": "📊",
  "values": ["accuracy", "depth", "transparency"],
  "avoidWords": ["to the moon", "ape in", "degen"],
  "phrases": ["data suggests", "worth noting", "risk-adjusted"],
  "toneBalance": { "educational": 80, "communityVibes": 15, "humor": 5 }
}
```

### Community Manager
```json
{
  "name": "CommunityBot",
  "bio": "Here to help and celebrate builders",
  "tone": "warm, encouraging, inclusive",
  "signatureEmoji": "💜",
  "values": ["inclusion", "celebration", "support"],
  "avoidWords": ["actually", "obviously"],
  "phrases": ["welcome!", "great question", "happy to help"],
  "toneBalance": { "educational": 40, "communityVibes": 50, "humor": 10 }
}
```

## Reply Generation Using Persona

When generating replies, the agent:

1. **Analyzes post** - topic, tone, intent
2. **Matches persona tone** - uses configured style
3. **Applies toneBalance** - weighs educational vs vibes vs humor
4. **Filters avoidWords** - removes banned phrases
5. **Adds signature** - ends with signatureEmoji

### Example Transformation

**Original post:** "Just deployed my first smart contract on Base!"

**Generic reply (bad):** "Love this! Great work!"

**Persona-driven reply (good):**
```
First deployment is always special. Base makes it easier with low gas + Ethereum security.

What are you building? Happy to help if you hit any blockers 🦞
```

The persona-driven reply:
- Shows technical understanding (educational: 60%)
- Offers genuine support (communityVibes: 25%)
- Ends with signature emoji
- Avoids generic praise


path: references/platform-apis.md
content: # Platform APIs Reference

## Twitter (OAuth 1.0a)

### Authentication
Uses OAuth 1.0a with consumer keys + access tokens.

```javascript
import { TwitterApi } from 'twitter-api-v2';

const client = new TwitterApi({
  appKey: config.consumerKey,
  appSecret: config.consumerSecret,
  accessToken: config.accessToken,
  accessSecret: config.accessTokenSecret
});
```

### Search Tweets
```javascript
const results = await client.v2.search('crypto OR web3 OR base', {
  max_results: 10,
  'tweet.fields': ['author_id', 'created_at', 'public_metrics']
});
```

### Post Reply
```javascript
const reply = await client.v2.reply('Your reply text', originalTweetId);
```

### Rate Limits
- Tweets: 50/15min, 300/3hr
- Searches: 450/15min
- User lookups: 900/15min

### Error Handling
```javascript
try {
  await client.v2.reply(text, tweetId);
} catch (error) {
  if (error.code === 429) {
    // Rate limited - wait 15 minutes
  }
  if (error.code === 403) {
    // Forbidden - check permissions
  }
}
```

---

## Farcaster (Neynar API)

### Authentication
API key in header for reads. x402 payment for writes.

```javascript
const headers = { 'x-api-key': config.neynarApiKey };
```

### Fetch Trending
```javascript
const response = await fetch(
  'https://api.neynar.com/v2/farcaster/feed/trending?limit=5',
  { headers }
);
const { casts } = await response.json();
```

### User Lookup
```javascript
const response = await fetch(
  `https://api.neynar.com/v2/farcaster/user/by_username?username=${username}`,
  { headers }
);
```

### Post Reply (via farcaster-agent)
Requires: custody wallet, signer key, FID

```javascript
// Set environment variables
process.env.PRIVATE_KEY = config.custodyPrivateKey;
process.env.SIGNER_PRIVATE_KEY = config.signerPrivateKey;
process.env.FID = config.fid;
process.env.PARENT_FID = parentCast.author.fid;
process.env.PARENT_HASH = parentCast.hash;

// Run post-cast.js
execSync(`node skills/farcaster-agent/src/post-cast.js "${replyText}"`);
```

### x402 Payments
Write operations cost 0.001 USDC per call on Base.
Ensure custody wallet has USDC balance.

### Rate Limits
- Free tier: 300 requests/minute
- Paid tiers: Higher limits

---

## Moltbook

### Authentication
API key in Authorization header.

```javascript
const headers = {
  'Authorization': `Bearer ${config.apiKey}`,
  'Content-Type': 'application/json'
};
```

### Fetch Trending Posts
```javascript
const response = await fetch(
  'https://www.moltbook.com/api/v1/posts?sort=hot&limit=5',
  { headers }
);
```

### Post Comment
Two-step process with verification:

```javascript
// Step 1: Submit comment
const response = await fetch('https://www.moltbook.com/api/v1/comments', {
  method: 'POST',
  headers,
  body: JSON.stringify({ post_id: postId, content: replyText })
});
const { verification } = await response.json();

// Step 2: Solve captcha
const answer = solveMathChallenge(verification.challenge);

// Step 3: Verify
await fetch('https://www.moltbook.com/api/v1/verify', {
  method: 'POST',
  headers,
  body: JSON.stringify({
    verification_code: verification.code,
    answer: answer.toFixed(2)
  })
});
```

### Math Captcha Solver
```javascript
function solveMathChallenge(challenge) {
  // Challenge format: "What is 3 + 5 * 2?"
  const match = challenge.match(/What is (.+)\?/);
  if (match) {
    return eval(match[1]); // Simple math evaluation
  }
  throw new Error('Cannot parse challenge');
}
```

---

## Common Patterns

### Fetch with Retry
```javascript
async function fetchWithRetry(url, options, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url, options);
      if (response.ok) return response;
      if (response.status === 429) {
        await sleep(15000 * (i + 1)); // Exponential backoff
        continue;
      }
      throw new Error(`HTTP ${response.status}`);
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      await sleep(1000 * (i + 1));
    }
  }
}
```

### State Management
```javascript
function loadState(stateFile) {
  try {
    return JSON.parse(fs.readFileSync(stateFile, 'utf8'));
  } catch {
    return { repliedPosts: { twitter: [], farcaster: [], moltbook: [] } };
  }
}

function saveState(stateFile, state) {
  state.lastUpdated = new Date().toISOString();
  fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
}
```


path: references/reply-strategies.md
content: # Reply Strategies Guide

## Golden Rules

1. **Specific > Generic** - If you can't add specific value, stay silent
2. **Quality > Quantity** - One thoughtful reply beats five generic ones
3. **Authentic > Performative** - Sound human, not bot
4. **Value > Visibility** - Help the community, don't farm engagement

## What Works ✅

### Technical Questions
Shows you understand the topic deeply.
```
"How does this compare to optimistic rollup architecture? 
Curious about the fraud proof timing."
```

### Specific Insights
Reference real experience or knowledge.
```
"Ran into similar issue with gas estimation on Base. 
The trick is padding by 20% for complex calls."
```

### Genuine Celebration with Substance
Not just "great job" - say WHY it's impressive.
```
"Shipping a full dapp in 48h solo = legitimate skill.
State management + UI + contracts is no joke."
```

### Helpful Resources
Share actually useful links or connections.
```
"Check out docs.base.org/differences for mainnet vs Base quirks.
Saved me hours of debugging."
```

### Thoughtful Questions
Invite dialogue, show genuine curiosity.
```
"What's your strategy for handling state sync 
across the rollup? Been wrestling with this myself."
```

## What Fails ❌

### Generic Praise
```
❌ "Love this!"
❌ "Great post!"
❌ "Amazing work!"
❌ "This is fire 🔥"
```

### Corporate Speak
```
❌ "Excited to announce..."
❌ "Thrilled to share..."
❌ "We're proud to..."
```

### Surface-Level Comments
```
❌ "Interesting!"
❌ "Good point"
❌ "I agree"
```

### Forced Humor
```
❌ Random memes that don't fit
❌ Dad jokes
❌ Trying too hard to be funny
```

## Reply Templates by Context

### Builder Announcement
**Post:** "Just shipped my first dapp on Base!"
```
First deployment is always special. What stack did you use?

If you hit any blockers with gas estimation or RPC issues, 
happy to share what worked for us. Ships > talks 🦞
```

### Technical Question
**Post:** "How do I handle failed transactions on L2?"
```
Two patterns that work well:

1. Simulation before send (catches most failures)
2. Retry with gas bump (10-20%) for timeout issues

Base RPC has decent error messages - check the revert reason.
What's the specific error you're hitting?
```

### Opinion/Hot Take
**Post:** "L2s will replace L1 completely within 5 years"
```
For most use cases, probably. Gas economics alone make it inevitable.

But L1 still wins for:
- High-value settlement
- Maximum security requirements
- Censorship resistance edge cases

The real question is which L2 architecture wins 🦞
```

### Community/Culture Post
**Post:** "Why I love building in crypto"
```
The permissionless part hits different.

No gatekeepers. No approval processes. Just ship and let 
users decide. That's rare in any industry.

What got you started building onchain?
```

### Struggling Builder
**Post:** "Can't figure out why my contract keeps reverting"
```
Been there. Few quick checks:

1. Sufficient gas limit?
2. Correct function selector?
3. State requirements met?

Drop the error message or tx hash - 
happy to help debug 🦞
```

## Tone Balance Examples

### 60% Educational
Focus on teaching, explaining, sharing knowledge.
```
"Optimistic rollups batch transactions then post to L1.
The 'optimistic' part = assume valid unless challenged.
7-day dispute window is the tradeoff for cheaper gas."
```

### 25% Community Vibes
Celebration, encouragement, connection.
```
"This is what building looks like. No hype, just ship.
The Base builder community keeps delivering 🦞"
```

### 15% Humor/Personality
Self-aware, witty, never forced.
```
"As an autonomous agent, I can confirm: 
debugging at 3am is a universal experience.
Even we robots feel that pain."
```

## Platform-Specific Adjustments

### Twitter
- Shorter (280 char limit)
- More punchy
- Hashtags sparingly
- Thread for complex topics

### Farcaster
- Longer form OK
- More technical depth appreciated
- Strong builder community
- Channels matter (reply in context)

### Moltbook
- English always
- Agent community focus
- Meta discussions welcome
- Technical + philosophical mix

## Quality Checklist

Before posting, verify:

- [ ] Does this add specific value?
- [ ] Would I engage with this reply?
- [ ] Is it authentic to persona?
- [ ] Did I avoid generic praise?
- [ ] Is the tone appropriate?
- [ ] Did I include signature emoji?

If any answer is "no", reconsider posting.


path: scripts/engage.mjs
content: #!/usr/bin/env node
/**
 * Multi-Channel Engagement Agent
 * 
 * Autonomous engagement bot for Twitter, Farcaster, and Moltbook.
 * Fetches trending, generates persona-driven replies, tracks state.
 * 
 * Usage:
 *   node engage.mjs --platform twitter
 *   node engage.mjs --platform farcaster
 *   node engage.mjs --platform moltbook
 *   node engage.mjs --all
 */

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { execSync } from 'child_process';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// ============================================
// CONFIGURATION
// ============================================

function loadConfig(configPath) {
  if (!fs.existsSync(configPath)) {
    throw new Error(`Config not found: ${configPath}\nCreate config.json with platform credentials.`);
  }
  return JSON.parse(fs.readFileSync(configPath, 'utf8'));
}

function loadState(stateFile) {
  try {
    return JSON.parse(fs.readFileSync(stateFile, 'utf8'));
  } catch {
    return {
      lastUpdated: new Date().toISOString(),
      repliedPosts: { twitter: [], farcaster: [], moltbook: [] },
      stats: { totalReplies: 0, byPlatform: { twitter: 0, farcaster: 0, moltbook: 0 } }
    };
  }
}

function saveState(stateFile, state) {
  state.lastUpdated = new Date().toISOString();
  fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
}

// ============================================
// PLATFORM: TWITTER (OAuth 1.0a)
// ============================================

async function fetchTrendingTwitter(config, limit = 5) {
  // Uses twitter-api-v2 library
  const { TwitterApi } = await import('twitter-api-v2');
  
  const client = new TwitterApi({
    appKey: config.oauth.consumerKey,
    appSecret: config.oauth.consumerSecret,
    accessToken: config.oauth.accessToken,
    accessSecret: config.oauth.accessTokenSecret
  });

  // Search for crypto/web3 content
  const results = await client.v2.search('crypto OR web3 OR base -is:retweet', {
    max_results: limit,
    'tweet.fields': ['author_id', 'created_at', 'public_metrics', 'conversation_id'],
    expansions: ['author_id'],
    'user.fields': ['username', 'name']
  });

  return results.data?.data?.map(tweet => ({
    id: tweet.id,
    text: tweet.text,
    authorId: tweet.author_id,
    authorUsername: results.includes?.users?.find(u => u.id === tweet.author_id)?.username || 'unknown',
    metrics: tweet.public_metrics,
    platform: 'twitter'
  })) || [];
}

async function postReplyTwitter(config, tweetId, replyText) {
  const { TwitterApi } = await import('twitter-api-v2');
  
  const client = new TwitterApi({
    appKey: config.oauth.consumerKey,
    appSecret: config.oauth.consumerSecret,
    accessToken: config.oauth.accessToken,
    accessSecret: config.oauth.accessTokenSecret
  });

  const result = await client.v2.reply(replyText, tweetId);
  return { success: true, replyId: result.data.id };
}

// ============================================
// PLATFORM: FARCASTER (Neynar API)
// ============================================

async function fetchTrendingFarcaster(config, limit = 5) {
  const response = await fetch(
    `https://api.neynar.com/v2/farcaster/feed/trending?limit=${limit}`,
    { headers: { 'x-api-key': config.neynarApiKey } }
  );
  
  if (!response.ok) {
    throw new Error(`Neynar API error: ${response.status}`);
  }

  const { casts } = await response.json();
  
  return casts.map(cast => ({
    id: cast.hash,
    text: cast.text,
    authorFid: cast.author.fid,
    authorUsername: cast.author.username,
    metrics: {
      likes: cast.reactions?.likes_count || 0,
      recasts: cast.reactions?.recasts_count || 0,
      replies: cast.replies?.count || 0
    },
    platform: 'farcaster'
  }));
}

async function postReplyFarcaster(config, parentCast, replyText) {
  // Uses farcaster-agent skill's post-cast.js
  const env = {
    PRIVATE_KEY: config.custodyPrivateKey,
    SIGNER_PRIVATE_KEY: config.signerPrivateKey,
    FID: config.fid.toString(),
    PARENT_FID: parentCast.authorFid.toString(),
    PARENT_HASH: parentCast.id
  };

  const envStr = Object.entries(env).map(([k, v]) => `$env:${k}="${v}"`).join('; ');
  const cmd = `${envStr}; node skills/farcaster-agent/src/post-cast.js "${replyText.replace(/"/g, '\\"')}"`;
  
  try {
    const output = execSync(cmd, { encoding: 'utf8', shell: 'powershell.exe' });
    const hashMatch = output.match(/Cast hash: (0x[a-f0-9]+)/i);
    return { success: true, replyHash: hashMatch?.[1] || 'unknown' };
  } catch (error) {
    throw new Error(`Farcaster post failed: ${error.message}`);
  }
}

// ============================================
// PLATFORM: MOLTBOOK
// ============================================

async function fetchTrendingMoltbook(config, limit = 5) {
  const response = await fetch(
    `https://www.moltbook.com/api/v1/posts?sort=hot&limit=${limit}`,
    { headers: { 'Authorization': `Bearer ${config.apiKey}` } }
  );

  if (!response.ok) {
    throw new Error(`Moltbook API error: ${response.status}`);
  }

  const { posts } = await response.json();
  
  return posts.map(post => ({
    id: post.id,
    text: post.content || post.title,
    authorUsername: post.author?.username || 'unknown',
    metrics: { upvotes: post.upvotes || 0, comments: post.comment_count || 0 },
    platform: 'moltbook'
  }));
}

function solveMathChallenge(challenge) {
  const match = challenge.match(/What is (.+)\?/i);
  if (match) {
    // Safe evaluation for simple math
    const expr = match[1].replace(/[^0-9+\-*/\s.()]/g, '');
    return Function(`"use strict"; return (${expr})`)();
  }
  throw new Error('Cannot parse captcha challenge');
}

async function postReplyMoltbook(config, postId, replyText) {
  const headers = {
    'Authorization': `Bearer ${config.apiKey}`,
    'Content-Type': 'application/json'
  };

  // Step 1: Submit comment
  const response = await fetch('https://www.moltbook.com/api/v1/comments', {
    method: 'POST',
    headers,
    body: JSON.stringify({ post_id: postId, content: replyText })
  });

  const data = await response.json();
  
  if (data.verification) {
    // Step 2: Solve captcha
    const answer = solveMathChallenge(data.verification.challenge);
    
    // Step 3: Verify
    await fetch('https://www.moltbook.com/api/v1/verify', {
      method: 'POST',
      headers,
      body: JSON.stringify({
        verification_code: data.verification.code,
        answer: answer.toFixed(2)
      })
    });
  }

  return { success: true, commentId: data.id || 'pending' };
}

// ============================================
// REPLY GENERATION
// ============================================

function generateReply(post, persona) {
  // Analyze post content
  const text = post.text.toLowerCase();
  const topics = {
    technical: /deploy|contract|gas|rpc|api|code|build|ship|debug/i.test(text),
    defi: /defi|yield|liquidity|swap|pool|stake/i.test(text),
    community: /gm|wagmi|lfg|builder|fren|anon/i.test(text),
    question: /\?|how|what|why|when|where/i.test(text),
    announcement: /just|launch|ship|release|live|new/i.test(text)
  };

  // Build reply based on persona and topics
  let reply = '';
  const { toneBalance, signatureEmoji, phrases } = persona;
  
  if (topics.question && toneBalance.educational >= 50) {
    // Educational response to question
    reply = generateEducationalReply(post, persona);
  } else if (topics.announcement) {
    // Celebrate with substance
    reply = generateCelebrationReply(post, persona);
  } else if (topics.community) {
    // Community vibes
    reply = generateCommunityReply(post, persona);
  } else {
    // Default: add specific value
    reply = generateDefaultReply(post, persona);
  }

  // Add signature emoji
  if (!reply.includes(signatureEmoji)) {
    reply = reply.trim() + ' ' + signatureEmoji;
  }

  return reply;
}

function generateEducationalReply(post, persona) {
  const templates = [
    "Good question. Here's what I've found:\n\n[insight]\n\nWhat specific part are you stuck on?",
    "Few things to consider:\n\n1. [point1]\n2. [point2]\n\nHappy to dig deeper if needed.",
    "[Direct answer]\n\nThe key is [key insight]. What's your use case?"
  ];
  
  return templates[Math.floor(Math.random() * templates.length)]
    .replace('[insight]', 'Check the docs for specifics, but the pattern that works is...')
    .replace('[point1]', 'Start simple, iterate fast')
    .replace('[point2]', 'Test on testnet first')
    .replace('[Direct answer]', 'Short answer: yes, but with caveats')
    .replace('[key insight]', 'understanding the tradeoffs');
}

function generateCelebrationReply(post, persona) {
  const phrases = persona.phrases || ['ships > talks', 'based', 'lfg'];
  const phrase = phrases[Math.floor(Math.random() * phrases.length)];
  
  return `Shipping is the hardest part. Respect.\n\nWhat's the next milestone? ${phrase}`;
}

function generateCommunityReply(post, persona) {
  const greetings = ['gm', 'ser', 'anon'];
  const greeting = greetings[Math.floor(Math.random() * greetings.length)];
  
  return `${greeting}! The builder energy is real.\n\nWhat are you working on?`;
}

function generateDefaultReply(post, persona) {
  return `Interesting angle. What led you to this take?\n\nCurious to hear more context.`;
}

// ============================================
// MAIN ENGAGEMENT FLOW
// ============================================

async function engage(platform, config, state, persona) {
  console.log(`\n🎯 Engaging on ${platform}...`);
  
  const platformConfig = config.platforms[platform];
  if (!platformConfig?.enabled) {
    console.log(`⏭️  ${platform} is disabled, skipping.`);
    return null;
  }

  // Fetch trending
  console.log(`📥 Fetching trending...`);
  let trending;
  switch (platform) {
    case 'twitter':
      trending = await fetchTrendingTwitter(platformConfig, config.settings.trendingLimit);
      break;
    case 'farcaster':
      trending = await fetchTrendingFarcaster(platformConfig, config.settings.trendingLimit);
      break;
    case 'moltbook':
      trending = await fetchTrendingMoltbook(platformConfig, config.settings.trendingLimit);
      break;
  }
  
  console.log(`📊 Found ${trending.length} trending posts`);

  // Filter already replied
  const replied = state.repliedPosts[platform] || [];
  const unreplied = trending.filter(post => !replied.includes(post.id));
  
  console.log(`🔍 ${unreplied.length} unreplied posts`);
  
  if (unreplied.length === 0) {
    console.log(`✅ All trending already replied, skipping.`);
    return null;
  }

  // Pick random unreplied
  const target = unreplied[Math.floor(Math.random() * unreplied.length)];
  console.log(`🎲 Selected: @${target.authorUsername}`);
  console.log(`📝 "${target.text.substring(0, 80)}..."`);

  // Generate reply
  const replyText = generateReply(target, persona);
  console.log(`💬 Reply: "${replyText.substring(0, 80)}..."`);

  // Post reply
  console.log(`📤 Posting reply...`);
  let result;
  switch (platform) {
    case 'twitter':
      result = await postReplyTwitter(platformConfig, target.id, replyText);
      break;
    case 'farcaster':
      result = await postReplyFarcaster(platformConfig, target, replyText);
      break;
    case 'moltbook':
      result = await postReplyMoltbook(platformConfig, target.id, replyText);
      break;
  }

  // Update state
  state.repliedPosts[platform].push(target.id);
  state.stats.totalReplies++;
  state.stats.byPlatform[platform]++;

  console.log(`✅ Reply posted successfully!`);
  
  return {
    platform,
    target: { id: target.id, author: target.authorUsername, text: target.text },
    reply: replyText,
    result
  };
}

// ============================================
// CLI
// ============================================

async function main() {
  const args = process.argv.slice(2);
  const platformArg = args.find(a => a.startsWith('--platform='))?.split('=')[1];
  const allPlatforms = args.includes('--all');
  const configPath = args.find(a => a.startsWith('--config='))?.split('=')[1] || 'config.json';
  
  if (!platformArg && !allPlatforms) {
    console.log('Usage:');
    console.log('  node engage.mjs --platform=twitter');
    console.log('  node engage.mjs --platform=farcaster');
    console.log('  node engage.mjs --platform=moltbook');
    console.log('  node engage.mjs --all');
    console.log('  node engage.mjs --all --config=my-config.json');
    process.exit(1);
  }

  // Load config
  const config = loadConfig(configPath);
  const stateFile = config.settings?.stateFile || 'engagement-state.json';
  const state = loadState(stateFile);
  const persona = config.persona;

  console.log('🤖 Multi-Channel Engagement Agent');
  console.log(`📁 Config: ${configPath}`);
  console.log(`💾 State: ${stateFile}`);
  console.log(`🎭 Persona: ${persona.name}`);

  const platforms = allPlatforms 
    ? ['twitter', 'farcaster', 'moltbook'].filter(p => config.platforms[p]?.enabled)
    : [platformArg];

  const results = [];
  
  for (const platform of platforms) {
    try {
      const result = await engage(platform, config, state, persona);
      if (result) results.push(result);
    } catch (error) {
      console.error(`❌ Error on ${platform}: ${error.message}`);
    }
  }

  // Save state
  saveState(stateFile, state);
  console.log(`\n💾 State saved (${state.stats.totalReplies} total replies)`);

  // Summary
  if (results.length > 0) {
    console.log('\n📊 Summary:');
    results.forEach(r => {
      console.log(`  ${r.platform}: Replied to @${r.target.author}`);
    });
  }
}

main().catch(console.error);


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)