Skill: teeclaw/openclaw-social-post (version 1.5.0)

--- name: social-post version: 1.5.0 description: Post and reply to X/Twitter and Farcaster with text and images. Features multi-account support, dynamic Twitter tier detection (Basic/Premium), auto-variation to avoid duplicate content detection, draft preview, character validation, threads, replies, and image uploads. Consumption-based pricing for X API, pay-per-cast for Farcaster. author: 0xdas license: MIT tags: [twitter, farcaster, social, posting, automation, threads, x-api, consumption-based, multi-account, anti-spam] metadata: openclaw: requires: bins: [bash, curl, jq, python3, shuf] env: [X_CONSUMER_KEY, X_CONSUMER_SECRET, X_ACCESS_TOKEN, X_ACCESS_TOKEN_SECRET] --- # Social Post Post to Twitter and/or Farcaster with automatic character limit validation and image upload handling. **Repository:** [github.com/teeclaw/social-post](https://github.com/teeclaw/social-post) ## Features - ✅ **Dynamic Twitter tier detection** - auto-detects Basic vs Premium accounts (cached 24h) - ✅ **Multi-account support** - manage multiple Twitter accounts from one skill - ✅ **Auto-variation** - avoid Twitter's duplicate content detection with `--vary` flag - ✅ **Premium account support** - post up to 25k characters in single tweet - ✅ **Interactive threading choice** - Premium users can choose single post or thread - ✅ Post to Twitter only - ✅ Post to Farcaster only - ✅ Post to both platforms simultaneously - ✅ **Reply to tweets and casts** - respond to specific posts on both platforms - ✅ **Draft preview** - shows exactly what will be posted before confirmation - ✅ Character/byte limit validation (dynamic per account tier) - ✅ Image upload support (for posts and replies) - ✅ **Thread support** - automatically split long text into numbered posts - ✅ **Link shortening** - compress URLs using TinyURL (saves characters) - ✅ Auto-truncate on overflow (optional) ## Platform Limits ### Dynamic Twitter Limits (Auto-Detected) The skill automatically detects your Twitter account tier and adjusts character limits: - **Basic/Free accounts:** 252 characters (280 with 10% safety buffer) - **Premium/Premium+ accounts:** 22,500 characters (25,000 with 10% safety buffer) ### Farcaster Limits - **288 bytes** (320 with 10% safety buffer) ### How Tier Detection Works 1. **First Use:** On your first post, the skill calls Twitter API to detect your subscription tier 2. **Caching:** Tier is cached for 24 hours to minimize API calls 3. **Auto-Refresh:** Cache expires after 24 hours, then re-checks on next post 4. **Manual Refresh:** Use `--refresh-tier` flag to force immediate re-check **Premium Posting Behavior:** When posting with a Premium account: - Text ≤ 280 chars → posts normally (single tweet) - Text > 280 but ≤ 22,500 chars → shows draft as **single long post** first - Prompts: "Thread this instead? (y/n)" - If YES → splits into threaded posts for review - If NO → posts as single long tweet - Text > 22,500 chars → auto-threads (exceeds Premium limit) **Force Threading:** - Use `--thread` flag to skip prompt and force threading - Use `--auto-confirm` to skip all prompts (uses best format automatically) ## Setup & Credentials ### X/Twitter Setup **Required credentials** (stored in `/home/phan_harry/.openclaw/.env`): ```bash X_CONSUMER_KEY=your_consumer_key X_CONSUMER_SECRET=your_consumer_secret X_ACCESS_TOKEN=your_access_token X_ACCESS_TOKEN_SECRET=your_access_token_secret X_USERNAME=your_username X_USER_ID=your_user_id ``` **How to get credentials:** 1. **Apply for X Developer Account** - Go to https://developer.twitter.com/en/portal/dashboard - Apply for Developer Access - Wait for approval (usually 1-2 days) 2. **Enable Consumption-Based Billing** - Set up payment method (credit card) in Developer Portal - **No subscription tiers** - you pay only for actual API usage - Charged per API request (posts, reads, etc.) - No monthly minimums or fees 3. **Create an App** - In Developer Portal, create a new App - Name: "Social Post Bot" (or any name) - Set permissions to "Read and Write" 4. **Generate Keys** - Consumer Key & Secret: In "Keys and tokens" tab - Access Token & Secret: Click "Generate" under "Authentication Tokens" - Save all 4 credentials securely 4. **Add to .env file** ```bash echo "X_CONSUMER_KEY=xxx" >> ~/.openclaw/.env echo "X_CONSUMER_SECRET=xxx" >> ~/.openclaw/.env echo "X_ACCESS_TOKEN=xxx" >> ~/.openclaw/.env echo "X_ACCESS_TOKEN_SECRET=xxx" >> ~/.openclaw/.env ``` **Test your credentials:** ```bash # Dry run (won't post) scripts/post.sh --twitter --dry-run "Test message" ``` ### Multi-Account Setup (Optional) You can manage multiple Twitter accounts by adding additional credentials with custom prefixes. **Example: Adding a second account** ```bash # Add credentials with custom prefix (e.g., MYACCOUNT_) echo "MYACCOUNT_API_KEY=xxx" >> ~/.openclaw/.env echo "MYACCOUNT_API_KEY_SECRET=xxx" >> ~/.openclaw/.env echo "MYACCOUNT_ACCESS_TOKEN=xxx" >> ~/.openclaw/.env echo [REDACTED_SECRET] >> ~/.openclaw/.env ``` **Usage:** ```bash # Post from default account (X_*) scripts/post.sh --twitter "Message from default account" # Post from custom account scripts/post.sh --account myaccount --twitter "Message from second account" # Reply from custom account scripts/reply.sh --account myaccount --twitter TWEET_ID "Reply from second account" ``` **Naming convention:** - Default account: `X_CONSUMER_KEY`, `X_CONSUMER_SECRET`, etc. - Custom accounts: `{PREFIX}_API_KEY`, `{PREFIX}_API_KEY_SECRET`, `{PREFIX}_ACCESS_TOKEN`, `{PREFIX}_ACCESS_TOKEN_SECRET` - Use lowercase prefix name in `--account` flag ### Farcaster Setup **Required credentials** (stored in `/home/phan_harry/.openclaw/farcaster-credentials.json`): ```json { "fid": "your_farcaster_id", "custodyAddress": "0x...", "custodyPrivateKey": "0x...", "signerPublicKey": "0x...", "signerPrivateKey": "0x...", "createdAt": "2026-01-01T00:00:00.000Z" } ``` **How to get credentials:** 1. **Use farcaster-agent skill to create account** ```bash # This will guide you through: # - Creating a wallet # - Registering FID # - Adding signer key # - Automatically saving credentials # See: /skills/farcaster-agent/SKILL.md ``` 2. **Or use existing credentials** - If you already have a Farcaster account - Export your custody wallet private key - Export your signer private key - Manually create the JSON file 3. **Fund the custody wallet (REQUIRED)** ```bash # Check current balance scripts/check-balance.sh # Send USDC to custody address on Base chain # Minimum: 0.1 USDC (~100 casts) # Recommended: 1-5 USDC (1000-5000 casts) ``` 4. **Verify setup** ```bash # Check credentials exist ls -la ~/.openclaw/farcaster-credentials.json # Check wallet balance scripts/check-balance.sh # Test posting (dry run) scripts/post.sh --farcaster --dry-run "Test message" ``` **Security Notes:** - ⚠️ **Never share your private keys** - ⚠️ Credentials are stored as plain text - secure your system - ⚠️ `.env` file should have `600` permissions (read/write owner only) - ⚠️ Back up your credentials securely ## Usage ### Posting #### Text only ```bash # Post to both platforms scripts/post.sh "Your message here" # Twitter only scripts/post.sh --twitter "Your message" # Farcaster only scripts/post.sh --farcaster "Your message" ``` #### With image ```bash # Post to both platforms with image scripts/post.sh --image /path/to/image.jpg "Your caption" # Twitter only with image scripts/post.sh --twitter --image /path/to/image.jpg "Caption" # Farcaster only with image scripts/post.sh --farcaster --image /path/to/image.jpg "Caption" ``` ### Replying #### Reply to Twitter ```bash # Reply to a tweet scripts/reply.sh --twitter TWEET_ID "Your reply" # Reply with image scripts/reply.sh --twitter TWEET_ID --image /path/to/image.jpg "Reply with image" # Get tweet ID from URL: twitter.com/user/status/[TWEET_ID] scripts/reply.sh --twitter 1234567890123456789 "Great point!" ``` #### Reply to Farcaster ```bash # Reply to a cast scripts/reply.sh --farcaster CAST_HASH "Your reply" # Reply with image scripts/reply.sh --farcaster 0xabcd1234... --image /path/to/image.jpg "Reply with image" # Get cast hash from URL: farcaster.xyz/~/conversations/[HASH] scripts/reply.sh --farcaster 0xa1b2c3d4e5f6... "Interesting perspective!" ``` #### Reply to both platforms ```bash # Reply to both (if you have corresponding IDs on both platforms) scripts/reply.sh --twitter 123456 --farcaster 0xabcd... "Great discussion!" ``` ### Options #### For `post.sh` (posting) - `--twitter` - Post to Twitter only - `--farcaster` - Post to Farcaster only - `--account <name>` - Twitter account to use (lowercase prefix from .env) - `--vary` - Auto-vary text to avoid duplicate content detection - `--image <path>` - Attach image - `--thread` - Force thread mode (split into multiple posts) - `--refresh-tier` - Force refresh Twitter account tier cache (Basic vs Premium) - `--shorten-links` - Shorten URLs to save characters - `--truncate` - Auto-truncate if over limit - `--dry-run` - Preview without posting - `-y, --yes` - Skip ALL confirmation prompts (auto-confirm, no threading prompt) #### For `reply.sh` (replying) - `--twitter <tweet_id>` - Reply to Twitter tweet with this ID - `--farcaster <cast_hash>` - Reply to Farcaster cast with this hash - `--account <name>` - Twitter account to use (lowercase prefix from .env) - `--image <path>` - Attach image to reply - `--shorten-links` - Shorten URLs to save characters - `--truncate` - Auto-truncate if over limit - `--dry-run` - Preview without replying - `-y, --yes` - Skip confirmation prompt (auto-confirm) ## Examples ### Posting Examples ```bash # Quick post to both (default account) scripts/post.sh "gm! Building onchain 🦞" # Post from specific Twitter account scripts/post.sh --account myaccount --twitter "Message from my second account" # Auto-vary text to avoid duplicate content detection scripts/post.sh --vary --twitter "Same text, subtle variations added automatically" # Premium account - post long text (interactive choice for threading) scripts/post.sh --twitter "Very long text that exceeds 280 characters but is under 25k... (The skill will detect Premium tier and ask: 'Thread this instead? (y/n)')" # Premium account - force threading (skip prompt) scripts/post.sh --twitter --thread "Long text that will be split into thread regardless of Premium status" # Premium account - force single long post (skip prompt) scripts/post.sh --twitter --auto-confirm "Long text that will post as single tweet on Premium account" # Refresh account tier cache (if you just upgraded to Premium) scripts/post.sh --refresh-tier --twitter "First post after upgrading to Premium" # Twitter announcement with image scripts/post.sh --twitter --image ~/screenshot.png "New feature shipped! 🚀" # Farcaster only scripts/post.sh --farcaster "Just published credential-manager to ClawHub!" # Long text as thread (auto-numbered) scripts/post.sh --thread "This is a very long announcement that exceeds the character limit. It will be automatically split into multiple numbered posts. Each part will be posted sequentially to create a thread. (1/3), (2/3), (3/3)" # Shorten URLs to save characters scripts/post.sh --shorten-links "Check out this amazing project: https://github.com/very-long-organization-name/very-long-repository-name" # Combine thread + link shortening scripts/post.sh --thread --shorten-links "Long text with multiple links that will be shortened and split into a thread if needed" # Both platforms, auto-truncate long text scripts/post.sh --truncate "Very long message that might exceed limits..." # Preview without confirmation (for automated workflows) scripts/post.sh --yes "Automated post from CI/CD" ``` ### Reply Examples ```bash # Reply to a Twitter thread scripts/reply.sh --twitter 1234567890123456789 "Totally agree with this take! 💯" # Reply from specific Twitter account scripts/reply.sh --account myaccount --twitter 1234567890 "Replying from my second account" # Reply to Farcaster cast scripts/reply.sh --farcaster 0xa1b2c3d4e5f6... "Great insight! Have you considered...?" # Reply with shortened links scripts/reply.sh --twitter 123456 --shorten-links "Here's more info: https://example.com/very-long-article-url" # Reply with image scripts/reply.sh --twitter 123456 --image ~/chart.png "Here's the data to support this" # Reply to both platforms (same message) scripts/reply.sh --twitter 123456 --farcaster 0xabc123 "This is exactly right 🎯" # Quick reply without confirmation scripts/reply.sh --twitter 123456 --yes "Quick acknowledgment" # Dry run to preview reply scripts/reply.sh --twitter 123456 --dry-run "Test reply preview" ``` ## Draft Preview The script now shows a draft preview before posting: ``` === Draft Preview === Text to post: ───────────────────────────────────────────── Your message here ───────────────────────────────────────────── Targets: • Twitter • Farcaster Proceed with posting? (y/n): ``` - Interactive mode: Prompts for confirmation - Non-interactive/automated: Use `--yes` flag to skip prompt - Dry run: Use `--dry-run` to preview without any posting ## Requirements - Twitter credentials in `.env` (X_CONSUMER_KEY, X_CONSUMER_SECRET, X_ACCESS_TOKEN, X_ACCESS_TOKEN_SECRET) - Farcaster credentials in `/home/phan_harry/.openclaw/farcaster-credentials.json` - **USDC on Base chain** (custody wallet): 0.001 USDC per Farcaster cast - For images: `curl`, `jq` ## Costs ### X/Twitter - **100% Consumption-based** - NO subscription tiers (tiers have been eliminated) - **Pay per API request** - charged for each call (post, read, etc.) - No monthly fees, no minimums, no tier upgrades to worry about - Automatic billing based on actual usage - Payment via credit card through X Developer portal - Uses OAuth 1.0a (no blockchain/USDC required) - Requires approved X Developer account + enabled billing **Official pricing:** https://developer.twitter.com/#pricing **Critical:** X API completely eliminated subscription tiers (Basic, Pro, etc.). The model is now purely pay-per-use - you are charged only for the API requests you actually make. ### Farcaster Each Farcaster cast costs **0.001 USDC** (paid via x402 protocol): - Deducted from custody wallet on **Base chain** - Sent to Neynar Hub: [REDACTED_SECRET] - ~$1 USDC = 1000 casts **Check balance:** ```bash # Quick check scripts/check-balance.sh # Manual check jq -r '.custodyAddress' ~/.openclaw/farcaster-credentials.json # View on basescan.org ``` **Fund wallet:** Send USDC to custody address on Base chain. Bridge from other chains if needed. ## Image Hosting - **Twitter:** Direct upload via Twitter API - **Farcaster:** Uploads to imgur for public URL (embeds automatically) ## Error Handling - Shows character/byte count before posting - Warns if exceeding limits - Option to truncate or abort - Validates credentials before attempting post

path: CHANGELOG.md
content: # Changelog

All notable changes to the social-post skill will be documented in this file.

## [1.5.0] - 2026-02-11

### Added
- **Dynamic Twitter tier detection**: Auto-detects Basic vs Premium/Premium+ accounts
  - Automatically adjusts character limits based on account subscription tier
  - Basic/Free accounts: 280 chars (252 with buffer)
  - Premium/Premium+ accounts: 25,000 chars (22,500 with buffer)
  - Smart caching: Tier cached for 24 hours to minimize API calls
  - Graceful fallback: Defaults to Basic tier if detection fails
- **Interactive threading choice for Premium accounts**:
  - When Premium user posts > 280 but < 25k chars, shows draft as single long post first
  - Prompts: "Thread this instead? (y/n)"
  - User can choose between single long post or threaded format
  - Respects `--thread` flag to force threading (skip prompt)
  - Respects `--auto-confirm` flag to skip all prompts
- **New tier detection library**: `lib/tier-detection.sh`
  - `detect_twitter_tier()`: Detects Basic/Premium/Premium+ tier via Twitter API
  - `get_twitter_char_limit()`: Returns raw character limit for account
  - `get_twitter_char_limit_buffered()`: Returns limit with 10% safety buffer
  - `cache_tier()`: Stores tier detection result with timestamp
  - `get_cached_tier()`: Retrieves cached tier if < 24h old
- **New flags**:
  - `--refresh-tier`: Force refresh account tier cache (useful after upgrading subscription)
  - Updated `--thread`: Now explicitly forces thread mode (was implicit before)
  - Updated `--auto-confirm`: Now skips ALL prompts including threading choice
- **Tier cache file**: `~/.openclaw/workspace/memory/twitter-account-tiers.json`
  - Stores tier detection results per account
  - JSON format: `{"account": {"tier": "premium", "checkedAt": 1234567890}}`
  - Auto-expires after 24 hours

### Changed
- **Dynamic character limits**: Twitter limits now adjust per account tier automatically
- **Updated validation**: `lib/validate.sh` now uses dynamic limits from tier detection
- **Updated threading logic**: `lib/threads.sh` respects Premium account 25k limit
- **Improved draft preview**: Shows detected account tier and available character limit
- **Enhanced help text**: Documents tier detection, Premium features, and new flags

### Technical
- Twitter API v2 used for tier detection
- Tier detection uses `/users/me` endpoint with `user.fields=subscription_type`
- Fallback detection via test post attempt (checks error message for char limit)
- Cache invalidation after 24 hours or with `--refresh-tier` flag
- Multi-account aware: Each account can have different tier
- Zero breaking changes: Existing workflows continue to work

### Use Cases
- Premium users can post long-form threads without artificial character limits
- Basic users get accurate 280-char validation
- Automatic adaptation when upgrading from Basic to Premium
- Single skill supports both Basic and Premium posting workflows

## [1.4.0] - 2026-02-10

### Added
- **Auto-variation feature**: New `--vary` flag to avoid Twitter's duplicate content detection
  - Automatically introduces subtle, natural differences to text
  - Prevents "duplicate content" errors when posting similar text from multiple accounts
  - Variations include: emoji additions, punctuation changes, spacing adjustments, synonym swaps
  - Shows both original and varied text in draft preview
  - Guarantees visible variation (will always change something)
  - Works seamlessly with all existing features (images, threads, multi-account)
- **New variation library**: `lib/variation.sh` with intelligent text transformation functions
  - `vary_text()`: Applies 2-3 random variations
  - `vary_punctuation()`: Changes sentence-ending punctuation
  - `vary_emoji()`: Adds/removes emojis
  - `vary_spacing()`: Adjusts line breaks
  - `vary_wording()`: Subtle synonym swaps

### Features
- Bypass Twitter's anti-spam duplicate content blocker
- Natural, human-like text variations
- Preview variations before posting
- No manual rewriting needed

### Use Case
Solves the problem of posting the same announcement from multiple Twitter accounts (e.g., company + personal accounts) without triggering Twitter's duplicate content detection.

### Technical
- Uses bash `shuf` for randomization
- Attempts up to 10 variation tries to ensure change
- Fallback emoji injection if no variation applied
- Integrates into existing post.sh workflow

## [1.3.0] - 2026-02-10

### Added
- **Multi-account support**: Manage multiple Twitter accounts from one skill
  - New `--account <name>` flag for both `post.sh` and `reply.sh`
  - Support for custom credential prefixes in `.env` file
  - Example: `MYACCOUNT_API_KEY`, `MYACCOUNT_API_KEY_SECRET`, etc.
  - Automatic credential loading based on account name
  - Account name displayed in draft preview
- **Enhanced credential management**:
  - `get_twitter_credentials()` function in `lib/twitter.sh`
  - Dynamic credential selection based on `TWITTER_ACCOUNT` env var
  - Backward compatibility with default `X_*` credentials
- **Documentation**:
  - Multi-account setup guide in SKILL.md
  - Naming convention for custom accounts
  - Usage examples for switching between accounts
  - Updated help text with `--account` flag

### Features
- Post from multiple Twitter accounts without changing config
- Reply from any configured account
- Seamless switching via `--account` flag
- Default account fallback (X_* credentials)
- Account validation before posting

### Technical
- Environment variable-based account switching
- Prefix-based credential mapping (e.g., `oxdasx` → `OXDASX_*`)
- No breaking changes to existing single-account setups
- Works with all existing features (images, threads, replies, etc.)

## [1.2.0] - 2026-02-08

### Added
- **Reply functionality**: New `reply.sh` script to reply to tweets and casts
  - Reply to Twitter tweets with `--twitter <tweet_id>`
  - Reply to Farcaster casts with `--farcaster <cast_hash>`
  - Reply to both platforms simultaneously
  - Support for image attachments in replies
  - Link shortening in replies with `--shorten-links`
  - Character/byte validation for replies
  - Draft preview before posting replies
  - Dry-run mode for testing replies
- **Documentation**: Complete reply examples and usage in SKILL.md and README.md
- **ID extraction guide**: How to get Tweet IDs and Farcaster cast hashes from URLs

### Features
- Reply to specific tweets using tweet ID from URL
- Reply to specific casts using cast hash from URL  
- All posting features available for replies (images, link shortening, truncation)
- Proper threading support for replies
- Cost transparency for replies (same as regular posts)

### Technical
- Extended Twitter library with reply support using `in_reply_to_tweet_id`
- Extended Farcaster library with reply support using `parentCastId`
- Image upload support for both Twitter and Farcaster replies
- Reuses existing validation and link shortening libraries

## [1.1.0] - 2026-02-06

### Added
- **Draft preview**: Shows exact text, image, and target platforms before posting
- **Confirmation prompt**: Interactive "Proceed with posting? (y/n)" confirmation
- **`--yes` flag**: Skip confirmation for automated workflows
- **Balance checking script**: New `check-balance.sh` to monitor Farcaster wallet
  - Shows ETH and USDC balances on Base
  - Displays remaining casts estimate
  - Warns when balance is low (<0.1 USDC)
  - Provides BaseScan link for detailed view

### Changed
- **Updated X/Twitter API pricing documentation**:
  - ✅ Corrected to **consumption-based model** (no tiers)
  - ✅ Removed outdated subscription tier references (Basic/Pro)
  - ✅ Clarified pay-per-API-request model
  - ✅ Added official pricing link: https://developer.twitter.com/#pricing
  - ✅ Updated setup instructions to reflect billing model
- **Enhanced cost transparency**:
  - Added platform comparison table
  - Documented Farcaster costs (0.001 USDC per cast)
  - Explained x402 payment protocol
  - Included Neynar Hub payment address
  - Added cost model comparison section

### Improved
- **README.md**:
  - Quick reference table with costs and setup time
  - Step-by-step credential setup for both platforms
  - Platform comparison table (cost model, limits, billing)
  - Comprehensive troubleshooting section for credentials
  - Verification steps for testing setup
- **SKILL.md**:
  - Detailed "Setup & Credentials" section
  - Security warnings for private key handling
  - Cost breakdown for both platforms
  - Official links and references
- **Help text** in `post.sh`:
  - Added cost information
  - Added `--yes` flag documentation
  - Updated platform limits section

### Fixed
- ❌ Removed incorrect "free tier" references for X API
- ❌ Removed outdated tier pricing ($100/mo, $200/mo, $5k/mo)
- ❌ Fixed `.env` file parsing error (quoted mnemonic)
- ✅ Corrected all cost documentation to reflect current pricing models

### Documentation
- Added setup time estimates (X: 5-10 min, Farcaster: 15-20 min)
- Documented credential file locations and formats
- Added wallet funding requirements for Farcaster
- Included credential verification commands
- Enhanced troubleshooting with specific error scenarios

## [1.0.0] - 2026-02-06

### Added
- **Multi-platform posting**: Post to Twitter, Farcaster, or both simultaneously
- **Thread support**: Automatically split long text into numbered, connected threads
- **Link shortening**: Compress URLs using TinyURL to save characters
- **Image upload support**: Upload images to both platforms
  - Twitter: Direct upload via Twitter API
  - Farcaster: Upload to catbox.moe with proper embed support
- **Character/byte limit validation**: 252 chars (Twitter) / 288 bytes (Farcaster) with 10% safety buffer
- **Platform-specific posting**: `--twitter` and `--farcaster` flags
- **Auto-truncate option**: `--truncate` flag to automatically shorten text
- **Dry-run mode**: `--dry-run` flag to preview without posting
- **Comprehensive error handling**: Clear error messages and troubleshooting guidance

### Features
- Character count validation before posting
- Shows exact overflow amount when over limit
- Smart thread splitting by sentences and paragraphs
- 2-second delays between thread parts for rate limiting
- Image attachment on first post of thread
- Proper reply chains (Twitter: `in_reply_to_tweet_id`, Farcaster: `parentCastId`)
- Automatic credential loading from `.env` and Farcaster credentials file

### Documentation
- Complete SKILL.md with usage examples
- Detailed README.md with setup instructions
- Troubleshooting guide for common errors
- Credential requirements and verification steps

### Technical
- Bash scripts with modular library architecture
- Twitter OAuth 1.0a integration
- Farcaster hub integration with x402 payments
- Python dependencies: `requests`, `requests_oauthlib`
- External dependencies: `curl`, `jq`


path: README.md
content: # Social Post

> Post and reply to X/Twitter and Farcaster from one command.

[![Version](https://img.shields.io/badge/version-1.4.0-blue.svg)](CHANGELOG.md)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
[![OpenClaw](https://img.shields.io/badge/OpenClaw-skill-orange.svg)](https://openclaw.ai)

**Multi-platform social posting and replying with automatic validation, image uploads, threads, and cost transparency.**

**Repository:** [github.com/teeclaw/social-post](https://github.com/teeclaw/social-post)

## Features

✅ **Multi-account support** - Manage multiple Twitter accounts from one skill  
✅ **Auto-variation** - Avoid Twitter's duplicate content detection with `--vary` flag  
✅ **Multi-platform posting** - X/Twitter, Farcaster, or both simultaneously  
✅ **Reply support** - Reply to specific tweets and casts with text and images  
✅ **Draft preview** - See exactly what will be posted before confirmation  
✅ **Cost transparency** - Clear pricing for both platforms documented  
✅ **Thread support** - Auto-split long posts into numbered threads  
✅ **Image uploads** - Attach images to posts and replies on both platforms  
✅ **Link shortening** - Compress URLs to save characters  
✅ **Character validation** - Validates limits before posting (252 chars / 288 bytes)  
✅ **Balance monitoring** - Check Farcaster wallet balance anytime  
✅ **Auto-truncate** - Optional automatic text shortening  
✅ **Dry-run mode** - Test without actually posting  

## Table of Contents

- [Features](#features)
- [Quick Reference](#quick-reference)
- [Required Credentials](#required-credentials)
  - [X/Twitter Setup](#xtwitter-setup-)
  - [Farcaster Setup](#farcaster-setup-)
- [Platform Comparison](#platform-comparison)
- [Verify Setup](#verify-setup)
- [Quick Start](#quick-start)
- [Troubleshooting](#troubleshooting)
- [Documentation](#documentation)

## Quick Reference

| Platform | Cost | Credentials Location | Setup Time |
|----------|------|---------------------|------------|
| **X/Twitter** | Pay-per-use (consumption-based) | `~/.openclaw/.env` | 5-10 min |
| **Farcaster** | 0.001 USDC/cast | `~/.openclaw/farcaster-credentials.json` | 15-20 min |

**Before first use:**
1. Set up credentials for your platforms (see below)
2. For X/Twitter: Enable API access (consumption-based billing)
3. For Farcaster: Fund custody wallet with USDC on Base
4. Test with `--dry-run` flag
5. Start posting! 🚀

**Cost Model Comparison:**

| Platform | Model | Details |
|----------|-------|---------|
| **X/Twitter** | Consumption-based | Pay per API request. No tiers, no subscriptions. |
| **Farcaster** | Pay-per-cast | 0.001 USDC per cast. Pre-fund wallet on Base. |

**X/Twitter:**
- ✅ Pay per API request (post, read, etc.)
- ✅ No subscription tiers (eliminated)
- ✅ No monthly minimums
- ✅ Credit card billing through X Developer Portal
- 📊 See official pricing: https://developer.twitter.com/#pricing

**Farcaster:**
- ✅ Fixed cost: 0.001 USDC per cast
- ✅ Pre-fund wallet with USDC on Base chain
- ✅ Deducted instantly per cast

### Required Credentials

#### **X/Twitter Setup** 🐦

**Step 1: Get Developer Access**
1. Go to https://developer.twitter.com/en/portal/dashboard
2. Apply for Developer Access
3. Wait for approval (1-2 days)
4. **Enable consumption-based billing**
   - Set up payment method (credit card)
   - No subscription tiers - pay only for what you use
   - Charged automatically per API request

**Step 2: Create App & Generate Keys**
1. Create a new App in Developer Portal
2. Set permissions to "Read and Write"
3. Generate Consumer Key/Secret and Access Token/Secret

**Step 3: Add to .env file**
Location: `/home/phan_harry/.openclaw/.env`
```bash
X_CONSUMER_KEY=your_consumer_key
X_CONSUMER_SECRET=your_consumer_secret
X_ACCESS_TOKEN=your_access_token
X_ACCESS_TOKEN_SECRET=your_access_token_secret
X_USERNAME=your_username  # Optional but recommended
X_USER_ID=your_user_id    # Optional but recommended
```

**💰 X/Twitter Costs:**
- **100% Consumption-based pricing** - NO subscription tiers
- **Pay per API request** - charged for each call you make
- No monthly fees, no minimums, no tier upgrades
- Billing is automatic based on actual usage
- Uses **OAuth 1.0a** authentication
- Requires X Developer account with approved API access + payment method
- See official pricing: https://developer.twitter.com/#pricing

**Important:** X API eliminated subscription tiers. It's now purely pay-per-use.

#### **Farcaster Setup** 🟣

**Step 1: Create Farcaster Account**

Option A - Use farcaster-agent skill (recommended):
```bash
# See /skills/farcaster-agent/SKILL.md for full guide
# This will automatically:
# - Register your FID
# - Add signer key
# - Save credentials to ~/.openclaw/farcaster-credentials.json
```

Option B - Use existing account:
- Export your custody wallet private key
- Export your signer private key
- Create credentials JSON manually

**Step 2: Verify Credentials File**

Location: `/home/phan_harry/.openclaw/farcaster-credentials.json`
```json
{
  "fid": "2684290",
  "custodyAddress": "0xB329...",
  "custodyPrivateKey": "0x601f1ea...",
  "signerPublicKey": "0x120986...",
  "signerPrivateKey": "0x0c8fe1a...",
  "createdAt": "2026-02-06T04:29:00.000Z"
}
```

**Step 3: Fund Custody Wallet (REQUIRED)**
```bash
# Check current balance
scripts/check-balance.sh

# Send USDC to custody address on Base chain
# Minimum: 0.1 USDC (~100 casts)
# Recommended: 1-5 USDC (1000-5000 casts)
```

**💰 Farcaster Costs:**
- **0.001 USDC per cast** (via x402 payment protocol)
- Deducted from custody wallet on **Base chain**
- Uses Neynar Hub API (`hub-api.neynar.com`)
- Payment to: [REDACTED_SECRET]

**Monitor Balance:**
```bash
# Quick check with script
scripts/check-balance.sh

# Or view on BaseScan
# https://basescan.org/address/<your_custody_address>
```

**Dependencies:**
- `python3` with `requests` and `requests_oauthlib` (for Twitter)
- `jq` (for JSON parsing)
- `curl` (for image uploads)

### Verify Setup

**1. Check credentials exist:**
```bash
# Check X/Twitter credentials
grep "^X_CONSUMER_KEY" ~/.openclaw/.env

# Check Farcaster credentials
ls -la ~/.openclaw/farcaster-credentials.json
```

**2. Check Farcaster wallet balance:**
```bash
scripts/check-balance.sh
# Should show:
# 💎 ETH Balance: X.XXX ETH
# 💵 USDC Balance: X.XXX USDC
# 📢 Casts Remaining: ~XXX
```

**3. Test with dry-run (doesn't actually post):**
```bash
scripts/post.sh --dry-run "Test message"

# Should show:
# ✅ Twitter: X/252 characters
# ✅ Farcaster: X/288 bytes
# === Draft Preview ===
# Would post to Twitter
# Would post to Farcaster
```

**4. Test single platform:**
```bash
# Test Twitter only
scripts/post.sh --twitter --dry-run "Twitter test"

# Test Farcaster only
scripts/post.sh --farcaster --dry-run "Farcaster test"
```

**Ready to post!** 🚀

### Quick Start

#### Posting

```bash
# Post to both platforms
scripts/post.sh "gm! Building onchain 🦞"

# Twitter only
scripts/post.sh --twitter "Twitter announcement"

# Farcaster only  
scripts/post.sh --farcaster "Farcaster thoughts"

# With image (both platforms)
scripts/post.sh --image ~/photo.jpg "Check this out!"

# Long text as thread
scripts/post.sh --thread "Very long announcement that exceeds character limits..."

# Shorten URLs to save characters
scripts/post.sh --shorten-links "Check out this cool site: https://very-long-url.com/path/to/page"

# Combine features
scripts/post.sh --thread --shorten-links --image ~/pic.jpg "Long text with links and image..."
```

#### Replying ✨ NEW

```bash
# Reply to Twitter tweet (get ID from URL: twitter.com/user/status/[ID])
scripts/reply.sh --twitter 1234567890123456789 "Great point!"

# Reply to Farcaster cast (get hash from URL: farcaster.xyz/~/conversations/[HASH])
scripts/reply.sh --farcaster 0xa1b2c3d4e5f6... "Interesting perspective!"

# Reply with image
scripts/reply.sh --twitter 123456789 --image ~/chart.png "Here's the data"

# Reply to both platforms (if you have IDs on both)
scripts/reply.sh --twitter 123456 --farcaster 0xabc... "Great discussion!"

# Shorten links in reply
scripts/reply.sh --twitter 123456 --shorten-links "More info: https://example.com/long-url"

# Quick reply without confirmation
scripts/reply.sh --twitter 123456 --yes "Quick acknowledgment"
```

### Features

✅ Post to Twitter, Farcaster, or both  
✅ **Reply to tweets and casts** - respond to specific posts on both platforms  
✅ Automatic character/byte limit validation  
✅ Image upload support (for posts and replies)  
✅ **Thread support** - split long text into numbered posts  
✅ **Link shortening** - compress URLs to save characters  
✅ Auto-truncate option  
✅ Dry-run mode  

### Platform Comparison

| Feature | X/Twitter | Farcaster |
|---------|-----------|-----------|
| **Cost model** | Consumption-based | Pay-per-cast |
| **Cost per post** | Charged per API request | 0.001 USDC |
| **Monthly fee** | None (pay-as-you-go) | None |
| **Payment method** | X API billing (credit card) | Custody wallet (Base) |
| **Character limit** | 252 chars* | 288 bytes* |
| **Rate limit** | Based on account | No limit (if funded) |
| **Image hosting** | Twitter API | catbox.moe |
| **API type** | OAuth 1.0a | Neynar Hub (x402) |

*With 10% safety buffer

### Platform Limits

- **Twitter:** 252 characters (280 with 10% buffer)
- **Farcaster:** 288 bytes (320 with 10% buffer)

### Image Hosting

- **Twitter:** Direct upload via Twitter API
- **Farcaster:** Uploaded to catbox.moe (free, no account needed)

### Troubleshooting

#### Credential Issues

**"X_CONSUMER_KEY not found" or Twitter auth errors:**
1. Check `.env` file exists: `ls -la ~/.openclaw/.env`
2. Verify credentials are set:
   ```bash
   grep "^X_" ~/.openclaw/.env
   ```
3. Ensure no extra spaces or quotes around values
4. Check file permissions: `chmod 600 ~/.openclaw/.env`
5. Test Python OAuth library:
   ```bash
   python3 -c "import requests_oauthlib"
   ```

**"Error: Farcaster credentials not found"**
1. Check file exists: `ls -la ~/.openclaw/farcaster-credentials.json`
2. Verify JSON is valid:
   ```bash
   jq . ~/.openclaw/farcaster-credentials.json
   ```
3. Ensure all required fields present:
   - `fid`, `custodyAddress`, `custodyPrivateKey`
   - `signerPublicKey`, `signerPrivateKey`
4. Set up Farcaster account using farcaster-agent skill

**"Insufficient USDC balance" or Farcaster post fails:**
1. Check wallet balance:
   ```bash
   scripts/check-balance.sh
   ```
2. If balance is low, send USDC to custody address on Base
3. Minimum: 0.1 USDC for ~100 casts
4. View on BaseScan to verify transaction

**"Invalid private key" or "signer error":**
1. Ensure private keys have `0x` prefix
2. Check custody key is Ethereum wallet format (64 hex chars)
3. Check signer key is Ed25519 format
4. Re-export keys if corrupted

#### Other Issues

**"Error: Twitter post script not found"**
- Ensure `/home/phan_harry/.openclaw/workspace/scripts/twitter-post.sh` exists
- Check script is executable: `chmod +x scripts/*.sh`

**"Error: Image not found"**
- Verify image path is correct
- Use absolute paths: `/full/path/to/image.jpg`
- Check file permissions and format (jpg, png, gif)

**Character limit exceeded:**
- Use `--truncate` flag to auto-shorten
- Use `--thread` to split into multiple posts
- Use `--shorten-links` to compress URLs
- Check byte count for emojis (count more on Farcaster)

**Rate limit errors (Twitter):**
- Rate limits based on account and usage
- Wait and retry later
- Check X Developer Portal for current limits

## Documentation

- **[SKILL.md](SKILL.md)** - Complete technical documentation
- **[CHANGELOG.md](CHANGELOG.md)** - Version history and changes
- **[X API Pricing](https://developer.twitter.com/#pricing)** - Official X API pricing
- **[Neynar Hub API](https://hub-api.neynar.com)** - Farcaster hub documentation

## Links

- **X Developer Portal:** https://developer.twitter.com/en/portal/dashboard
- **Farcaster Developer Docs:** https://docs.farcaster.xyz
- **OpenClaw:** https://openclaw.ai
- **ClawHub:** https://clawhub.com

## License

MIT © 0xdas

---

**Version 1.2.0** | Built for [OpenClaw](https://openclaw.ai) 🦞


path: lib/farcaster.sh
content: #!/bin/bash
# Farcaster posting library
# Loads credentials from ~/.openclaw/.env (centralized credential management)

FARCASTER_REPO="/home/phan_harry/.openclaw/workspace/skills/farcaster-agent/repo"
ENV_FILE="/home/phan_harry/.openclaw/.env"

# Load a credential from .env, with GPG decryption support
_load_cred() {
  local key="$1"
  local value
  value=$(grep "^${key}=" "$ENV_FILE" 2>/dev/null | head -1 | cut -d= -f2-)

  if [[ "$value" == GPG:* ]]; then
    # Decrypt from GPG secrets
    local gpg_key="${value#GPG:}"
    # Load passphrase from .env if not already in environment
    if [ -z "${OPENCLAW_GPG_PASSPHRASE:-}" ]; then
      export OPENCLAW_GPG_PASSPHRASE=$(grep "^OPENCLAW_GPG_PASSPHRASE=" "$ENV_FILE" 2>/dev/null | head -1 | cut -d= -f2-)
    fi
    local passphrase="${OPENCLAW_GPG_PASSPHRASE:-}"
    if [ -n "$passphrase" ]; then
      value=$(echo "$passphrase" | gpg -d --batch --quiet --passphrase-fd 0 "$HOME/.openclaw/.env.secrets.gpg" 2>/dev/null | python3 -c "import json,sys; print(json.load(sys.stdin).get('$gpg_key',''))")
    else
      value=$(gpg -d --batch --quiet "$HOME/.openclaw/.env.secrets.gpg" 2>/dev/null | python3 -c "import json,sys; print(json.load(sys.stdin).get('$gpg_key',''))")
    fi
  fi

  echo "$value"
}

# Post text-only to Farcaster
farcaster_post_text() {
  local text="$1"
  local parent_hash="$2"

  if [ ! -f "$ENV_FILE" ]; then
    echo "Error: .env not found at $ENV_FILE" >&2
    return 1
  fi

  local fid=$(_load_cred "FARCASTER_FID")
  local private_key=$(_load_cred "FARCASTER_CUSTODY_PRIVATE_KEY")
  local signer_key=$(_load_cred "FARCASTER_SIGNER_PRIVATE_KEY")

  if [ -z "$fid" ] || [ -z "$private_key" ] || [ -z "$signer_key" ]; then
    echo "Error: Farcaster credentials not found in .env (need FARCASTER_FID, FARCASTER_CUSTODY_PRIVATE_KEY, FARCASTER_SIGNER_PRIVATE_KEY)" >&2
    return 1
  fi

  if [ -n "$parent_hash" ]; then
    # Post as reply (for threads) - inline node script with proper env
    cd "$FARCASTER_REPO" && PRIVATE_KEY="$private_key" SIGNER_PRIVATE_KEY="$signer_key" FID="$fid" PARENT_HASH="$parent_hash" node -e "
const { Wallet, JsonRpcProvider } = require('ethers');
const { makeCastAdd, NobleEd25519Signer, FarcasterNetwork, Message } = require('@farcaster/hub-nodejs');
const { submitMessage } = require('./src/x402');
const text = process.argv[1];
(async () => {
  const baseProvider = new JsonRpcProvider('https://mainnet.base.org');
  const wallet = new Wallet(process.env.PRIVATE_KEY, baseProvider);
  const signerBytes = Buffer.from(process.env.SIGNER_PRIVATE_KEY, 'hex');
  const signer = new NobleEd25519Signer(signerBytes);
  const fid = parseInt(process.env.FID);
  const parentHashBytes = Buffer.from(process.env.PARENT_HASH.replace('0x', ''), 'hex');
  const castResult = await makeCastAdd({ text, embeds: [], embedsDeprecated: [], mentions: [], mentionsPositions: [], parentCastId: { fid, hash: parentHashBytes } }, { fid, network: FarcasterNetwork.MAINNET }, signer);
  if (castResult.isErr()) throw new Error('Failed: ' + castResult.error);
  const cast = castResult.value;
  const hash = '0x' + Buffer.from(cast.hash).toString('hex');
  const messageBytes = Buffer.from(Message.encode(cast).finish());
  await submitMessage(wallet, messageBytes);
  await new Promise(r => setTimeout(r, 2000));
  console.log('Cast hash:', hash);
  console.log('URL: https://farcaster.xyz/~/conversations/' + hash);
})().catch(err => { console.error('Error:', err.message); process.exit(1); });
" "$text" 2>&1 | grep -E "(Cast hash|URL|Error)" | tail -2
  else
    cd "$FARCASTER_REPO" && \
    PRIVATE_KEY="$private_key" \
    SIGNER_PRIVATE_KEY="$signer_key" \
    FID="$fid" \
    npm run cast "$text" 2>&1 | grep -E "(Cast hash|URL|Error)" | tail -2
  fi
}

# Upload image to imgur (anonymous)
upload_to_imgur() {
  local image_path="$1"

  if [ ! -f "$image_path" ]; then
    echo "Error: Image not found at $image_path" >&2
    return 1
  fi

  # Try catbox.moe (no API key needed, reliable)
  local response=$(curl -s -F "reqtype=fileupload" -F "fileToUpload=@$image_path" https://catbox.moe/user/api.php)

  if [[ "$response" =~ ^https://files.catbox.moe/ ]]; then
    echo "$response"
    return 0
  fi

  # Fallback: try uguu.se
  response=$(curl -s -F "files[]=@$image_path" https://uguu.se/upload.php | jq -r '.files[0].url' 2>/dev/null)

  if [[ "$response" =~ ^https:// ]]; then
    echo "$response"
    return 0
  fi

  echo "Error: Failed to upload image" >&2
  return 1
}

# Post to Farcaster with image (using embeds)
farcaster_post_with_image() {
  local text="$1"
  local image_path="$2"
  local parent_hash="$3"

  if [ ! -f "$ENV_FILE" ]; then
    echo "Error: .env not found at $ENV_FILE" >&2
    return 1
  fi

  echo "Uploading image..." >&2
  local image_url=$(upload_to_imgur "$image_path")

  if [ $? -ne 0 ]; then
    echo "Failed to upload image for Farcaster" >&2
    return 1
  fi

  echo "Image uploaded: $image_url" >&2

  local fid=$(_load_cred "FARCASTER_FID")
  local private_key=$(_load_cred "FARCASTER_CUSTODY_PRIVATE_KEY")
  local signer_key=$(_load_cred "FARCASTER_SIGNER_PRIVATE_KEY")

  if [ -z "$fid" ] || [ -z "$private_key" ] || [ -z "$signer_key" ]; then
    echo "Error: Farcaster credentials not found in .env" >&2
    return 1
  fi

  # Post with image in embeds array
  cd "$FARCASTER_REPO" && PRIVATE_KEY="$private_key" SIGNER_PRIVATE_KEY="$signer_key" FID="$fid" IMAGE_URL="$image_url" PARENT_HASH="${parent_hash:-}" node -e "
const { Wallet, JsonRpcProvider } = require('ethers');
const { makeCastAdd, NobleEd25519Signer, FarcasterNetwork, Message } = require('@farcaster/hub-nodejs');
const { submitMessage } = require('./src/x402');
const text = process.argv[1];
(async () => {
  const baseProvider = new JsonRpcProvider('https://mainnet.base.org');
  const wallet = new Wallet(process.env.PRIVATE_KEY, baseProvider);
  const signerBytes = Buffer.from(process.env.SIGNER_PRIVATE_KEY, 'hex');
  const signer = new NobleEd25519Signer(signerBytes);
  const fid = parseInt(process.env.FID);
  const imageUrl = process.env.IMAGE_URL;

  const castBody = {
    text,
    embeds: [{ url: imageUrl }],
    embedsDeprecated: [],
    mentions: [],
    mentionsPositions: []
  };

  if (process.env.PARENT_HASH) {
    const parentHashBytes = Buffer.from(process.env.PARENT_HASH.replace('0x', ''), 'hex');
    castBody.parentCastId = { fid, hash: parentHashBytes };
  }

  const castResult = await makeCastAdd(castBody, { fid, network: FarcasterNetwork.MAINNET }, signer);
  if (castResult.isErr()) throw new Error('Failed: ' + castResult.error);
  const cast = castResult.value;
  const hash = '0x' + Buffer.from(cast.hash).toString('hex');
  const messageBytes = Buffer.from(Message.encode(cast).finish());
  await submitMessage(wallet, messageBytes);
  await new Promise(r => setTimeout(r, 2000));
  console.log('Cast hash:', hash);
  console.log('URL: https://farcaster.xyz/~/conversations/' + hash);
})().catch(err => { console.error('Error:', err.message); process.exit(1); });
" "$text" 2>&1 | grep -E "(Cast hash|URL|Error)" | tail -2
}


path: lib/links.sh
content: #!/bin/bash
# Link shortening for social-post

# Shorten a single URL using TinyURL (no API key needed)
shorten_url() {
  local url="$1"
  
  # Use TinyURL API (free, no auth)
  local short_url=$(curl -s "http://tinyurl.com/api-create.php?url=$(echo "$url" | jq -sRr @uri)")
  
  if [[ "$short_url" =~ ^https?://tinyurl.com/ ]]; then
    echo "$short_url"
    return 0
  else
    # Fallback: return original URL
    echo "$url"
    return 1
  fi
}

# Find and shorten all URLs in text
shorten_links_in_text() {
  local text="$1"
  local shortened_text="$text"
  
  # Regex to match URLs
  local url_regex='https?://[^[:space:]]+'
  
  # Find all URLs
  local urls=$(echo "$text" | grep -oE "$url_regex" || true)
  
  if [ -z "$urls" ]; then
    echo "$text"
    return 0
  fi
  
  # Shorten each URL
  while IFS= read -r url; do
    [ -z "$url" ] && continue
    
    echo "Shortening: $url" >&2
    local short=$(shorten_url "$url")
    
    if [ "$short" != "$url" ]; then
      echo "  → $short" >&2
      # Replace in text
      shortened_text="${shortened_text//$url/$short}"
    else
      echo "  → Failed, keeping original" >&2
    fi
  done <<< "$urls"
  
  echo "$shortened_text"
}

# Calculate character savings from link shortening
estimate_link_savings() {
  local text="$1"
  
  local url_regex='https?://[^[:space:]]+'
  local urls=$(echo "$text" | grep -oE "$url_regex" || true)
  
  if [ -z "$urls" ]; then
    echo "0"
    return
  fi
  
  local total_saved=0
  while IFS= read -r url; do
    [ -z "$url" ] && continue
    local url_length=${#url}
    # TinyURL typically produces ~20 char URLs
    local short_length=20
    local saved=$((url_length - short_length))
    [ "$saved" -gt 0 ] && total_saved=$((total_saved + saved))
  done <<< "$urls"
  
  echo "$total_saved"
}


path: lib/threads.sh
content: #!/bin/bash
# Thread support for social-post

# Load tier detection library
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/tier-detection.sh"

# Split text into thread parts
# Args: text, platform (twitter|farcaster)
split_into_thread() {
  local text="$1"
  local platform="$2"
  
  # Determine limit based on platform
  local limit=288
  if [ "$platform" = "twitter" ]; then
    local account="${TWITTER_ACCOUNT:-mr_crtee}"
    limit=$(get_twitter_char_limit_buffered "$account")
  fi
  
  # Reserve space for numbering (e.g., " (1/3)")
  local numbering_space=8
  local effective_limit=$((limit - numbering_space))
  
  # Split text smartly
  local -a parts=()
  local current_part=""
  
  # First try splitting by paragraphs (double newline)
  local paragraphs=$(echo "$text" | sed ':a;N;$!ba;s/\n\n/\x00/g')
  
  IFS=$'\x00' read -ra para_array <<< "$paragraphs"
  
  for para in "${para_array[@]}"; do
    # Remove newlines within paragraph
    para=$(echo "$para" | tr '\n' ' ')
    
    # Check if this paragraph alone exceeds limit
    local para_length=${#para}
    if [ "$platform" = "farcaster" ]; then
      para_length=$(echo -n "$para" | wc -c)
    fi
    
    if [ "$para_length" -gt "$effective_limit" ]; then
      # Paragraph too long - split by sentences
      IFS='.' read -ra sentences <<< "$para"
      
      for sent in "${sentences[@]}"; do
        sent=$(echo "$sent" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
        [ -z "$sent" ] && continue
        sent="$sent."
        
        local sent_length=${#sent}
        if [ "$platform" = "farcaster" ]; then
          sent_length=$(echo -n "$sent" | wc -c)
        fi
        
        # Check if adding sentence exceeds limit
        local test_part="$current_part"
        [ -n "$test_part" ] && test_part="$test_part "
        test_part="$test_part$sent"
        
        local test_length=${#test_part}
        if [ "$platform" = "farcaster" ]; then
          test_length=$(echo -n "$test_part" | wc -c)
        fi
        
        if [ "$test_length" -gt "$effective_limit" ] && [ -n "$current_part" ]; then
          parts+=("$current_part")
          current_part="$sent"
        else
          if [ -n "$current_part" ]; then
            current_part="$current_part $sent"
          else
            current_part="$sent"
          fi
        fi
      done
    else
      # Paragraph fits, check if adding to current part
      local test_part="$current_part"
      [ -n "$test_part" ] && test_part="$test_part

"
      test_part="$test_part$para"
      
      local test_length=${#test_part}
      if [ "$platform" = "farcaster" ]; then
        test_length=$(echo -n "$test_part" | wc -c)
      fi
      
      if [ "$test_length" -gt "$effective_limit" ] && [ -n "$current_part" ]; then
        parts+=("$current_part")
        current_part="$para"
      else
        if [ -n "$current_part" ]; then
          current_part="$current_part

$para"
        else
          current_part="$para"
        fi
      fi
    fi
  done
  
  # Add final part
  if [ -n "$current_part" ]; then
    parts+=("$current_part")
  fi
  
  # Output parts with numbering
  local total=${#parts[@]}
  for i in "${!parts[@]}"; do
    local num=$((i + 1))
    echo "${parts[$i]} ($num/$total)"
    echo "---THREAD_SEPARATOR---"
  done
}

# Check if text needs threading
needs_threading() {
  local text="$1"
  local platform="$2"
  
  local limit=288
  if [ "$platform" = "twitter" ]; then
    local account="${TWITTER_ACCOUNT:-mr_crtee}"
    limit=$(get_twitter_char_limit_buffered "$account")
    [ "${#text}" -gt "$limit" ] && return 0
  else
    # Farcaster
    local byte_count=$(echo -n "$text" | wc -c)
    [ "$byte_count" -gt "$limit" ] && return 0
  fi
  
  return 1
}


path: lib/tier-detection.sh
content: #!/bin/bash
# Twitter account tier detection library

TIER_CACHE_FILE="/home/phan_harry/.openclaw/workspace/memory/twitter-account-tiers.json"
TIER_CACHE_HOURS=24

# Ensure cache directory exists
mkdir -p "$(dirname "$TIER_CACHE_FILE")"

# Initialize cache file if it doesn't exist
if [ ! -f "$TIER_CACHE_FILE" ]; then
  echo "{}" > "$TIER_CACHE_FILE"
fi

# Detect Twitter account tier (Basic, Premium, Premium+)
detect_twitter_tier() {
  local account="$1"
  local force_refresh="${2:-false}"
  
  # Load credentials for the account
  source /home/phan_harry/.openclaw/.env
  
  if [ "$account" = "oxdasx" ]; then
    local consumer_key="$OXDASX_API_KEY"
    local consumer_secret="$OXDASX_API_KEY_SECRET"
    local access_token="$OXDASX_ACCESS_TOKEN"
    local access_token_secret="$OXDASX_ACCESS_TOKEN_SECRET"
  else
    # Default to mr_crtee
    local consumer_key="$X_CONSUMER_KEY"
    local consumer_secret="$X_CONSUMER_SECRET"
    local access_token="$X_ACCESS_TOKEN"
    local access_token_secret="$X_ACCESS_TOKEN_SECRET"
  fi
  
  # Check cache first (unless force refresh)
  if [ "$force_refresh" = false ]; then
    local cached_tier=$(get_cached_tier "$account")
    if [ -n "$cached_tier" ]; then
      echo "$cached_tier"
      return 0
    fi
  fi
  
  # API call to detect tier
  local tier=$(python3 - "$consumer_key" "$consumer_secret" "$access_token" "$access_token_secret" <<'EOF'
import requests
from requests_oauthlib import OAuth1
import sys
import json

consumer_key = sys.argv[1]
consumer_secret = sys.argv[2]
access_token = sys.argv[3]
access_token_secret = sys.argv[4]

auth = OAuth1(consumer_key, consumer_secret, access_token, access_token_secret)

# Get user info with subscription data
url = "https://api.twitter.com/2/users/me"
params = {
    "user.fields": "subscription_type,public_metrics"
}

try:
    response = requests.get(url, auth=auth, params=params)
    
    if response.status_code == 200:
        data = response.json()['data']
        
        # Check subscription_type field (if available)
        # Note: This field might not be available in all API tiers
        # Fallback: Premium users typically have access to longer character limits
        
        # For now, we'll use a heuristic:
        # - Check if we can successfully post > 280 chars (test with API limits)
        # - Or check subscription_type if available
        
        subscription_type = data.get('subscription_type', 'none')
        
        # Map subscription types
        if subscription_type in ['premium_plus', 'premium+']:
            print("premium_plus")
        elif subscription_type == 'premium':
            print("premium")
        else:
            # Fallback: Try to detect by attempting to validate a long tweet
            # Premium users have 25k char limit, Basic users have 280
            # We'll use the v2 API's tweet length validation
            test_url = "https://api.twitter.com/2/tweets"
            test_text = "x" * 281  # Just over Basic limit
            test_payload = {"text": test_text}
            
            # Dry run - don't actually post
            # Check the error message
            test_response = requests.post(test_url, auth=auth, json=test_payload)
            
            if test_response.status_code == 400:
                error = test_response.json()
                # If error mentions character limit, we know the account type
                if 'text' in str(error) and '280' in str(error):
                    print("basic")
                else:
                    # No character limit error = Premium
                    print("premium")
            else:
                # Default to basic if we can't determine
                print("basic")
    else:
        # API call failed, default to basic (safe fallback)
        print("basic", file=sys.stderr)
        print(f"API Error: {response.status_code}", file=sys.stderr)
        print("basic")

except Exception as e:
    print(f"Error: {e}", file=sys.stderr)
    print("basic")
EOF
)
  
  if [ $? -ne 0 ] || [ -z "$tier" ]; then
    # Fallback to basic on error
    tier="basic"
  fi
  
  # Cache the result
  cache_tier "$account" "$tier"
  
  echo "$tier"
}

# Get cached tier if still valid
get_cached_tier() {
  local account="$1"
  
  if [ ! -f "$TIER_CACHE_FILE" ]; then
    return 1
  fi
  
  local cache_data=$(python3 - "$account" "$TIER_CACHE_HOURS" <<'EOF'
import json
import sys
import time

account = sys.argv[1]
cache_hours = int(sys.argv[2])

try:
    with open(sys.argv[0].replace('-', '/home/phan_harry/.openclaw/workspace/memory/twitter-account-tiers.json')) as f:
        cache = json.load(f)
    
    if account in cache:
        tier_data = cache[account]
        checked_at = tier_data.get('checkedAt', 0)
        now = int(time.time())
        age_hours = (now - checked_at) / 3600
        
        if age_hours < cache_hours:
            print(tier_data['tier'])
        else:
            # Cache expired
            pass
    else:
        # Not in cache
        pass

except Exception:
    # Error reading cache
    pass
EOF
)
  
  if [ -n "$cache_data" ]; then
    echo "$cache_data"
  fi
}

# Cache tier for account
cache_tier() {
  local account="$1"
  local tier="$2"
  
  python3 - "$account" "$tier" "$TIER_CACHE_FILE" <<'EOF'
import json
import sys
import time

account = sys.argv[1]
tier = sys.argv[2]
cache_file = sys.argv[3]

try:
    with open(cache_file, 'r') as f:
        cache = json.load(f)
except Exception:
    cache = {}

cache[account] = {
    'tier': tier,
    'checkedAt': int(time.time())
}

with open(cache_file, 'w') as f:
    json.dump(cache, f, indent=2)
EOF
}

# Get character limit for account based on tier
get_twitter_char_limit() {
  local account="$1"
  local force_refresh="${2:-false}"
  
  local tier=$(detect_twitter_tier "$account" "$force_refresh")
  
  case "$tier" in
    premium|premium_plus)
      echo "25000"
      ;;
    basic|*)
      echo "280"
      ;;
  esac
}

# Get character limit with safety buffer
get_twitter_char_limit_buffered() {
  local account="$1"
  local force_refresh="${2:-false}"
  
  local limit=$(get_twitter_char_limit "$account" "$force_refresh")
  
  # Apply 10% safety buffer
  local buffered=$((limit * 9 / 10))
  
  echo "$buffered"
}


path: lib/twitter.sh
content: #!/bin/bash
# Twitter posting library

TWITTER_POST_SCRIPT="/home/phan_harry/.openclaw/workspace/scripts/twitter-post.sh"

# Get credentials based on TWITTER_ACCOUNT env var
get_twitter_credentials() {
  source /home/phan_harry/.openclaw/.env
  
  if [ "$TWITTER_ACCOUNT" = "oxdasx" ]; then
    export X_CONSUMER_KEY="$OXDASX_API_KEY"
    export X_CONSUMER_SECRET="$OXDASX_API_KEY_SECRET"
    export X_ACCESS_TOKEN="$OXDASX_ACCESS_TOKEN"
    export X_ACCESS_TOKEN_SECRET="$OXDASX_ACCESS_TOKEN_SECRET"
    export X_USERNAME="0xdasx"
  else
    # Default to mr_crtee
    export X_CONSUMER_KEY="${X_CONSUMER_KEY}"
    export X_CONSUMER_SECRET="${X_CONSUMER_SECRET}"
    export X_ACCESS_TOKEN="${X_ACCESS_TOKEN}"
    export X_ACCESS_TOKEN_SECRET="${X_ACCESS_TOKEN_SECRET}"
    export X_USERNAME="${X_USERNAME:-mr_crtee}"
  fi
}

# Post text-only to Twitter
twitter_post_text() {
  local text="$1"
  local reply_to_id="$2"
  
  get_twitter_credentials
  
  if [ ! -f "$TWITTER_POST_SCRIPT" ]; then
    echo "Error: Twitter post script not found at $TWITTER_POST_SCRIPT" >&2
    return 1
  fi
  
  if [ -n "$reply_to_id" ]; then
    # Post as reply (for threads)
    python3 - "$text" "$reply_to_id" <<'EOF'
import requests
from requests_oauthlib import OAuth1
import os
import sys
import json

text = sys.argv[1]
reply_to = sys.argv[2]

consumer_key = os.environ['X_CONSUMER_KEY']
consumer_secret = os.environ['X_CONSUMER_SECRET']
access_token = os.environ['X_ACCESS_TOKEN']
access_token_secret = os.environ['X_ACCESS_TOKEN_SECRET']

auth = OAuth1(consumer_key, consumer_secret, access_token, access_token_secret)

url = "https://api.twitter.com/2/tweets"
payload = {
    "text": text,
    "reply": {
        "in_reply_to_tweet_id": reply_to
    }
}

response = requests.post(url, auth=auth, json=payload)

if response.status_code == 201:
    tweet = response.json()
    tweet_id = tweet['data']['id']
    print(tweet_id)
else:
    print(f"Error: {response.status_code} - {response.text}", file=sys.stderr)
    exit(1)
EOF
  else
    "$TWITTER_POST_SCRIPT" "$text"
  fi
}

# Upload image to Twitter and get media_id
twitter_upload_image() {
  local image_path="$1"
  
  if [ ! -f "$image_path" ]; then
    echo "Error: Image not found at $image_path" >&2
    return 1
  fi
  
  # Load credentials
  get_twitter_credentials
  
  # Upload to Twitter media endpoint
  local upload_response=$(python3 - "$image_path" <<'EOF'
import requests
from requests_oauthlib import OAuth1
import os
import sys
import json

image_path = sys.argv[1]

consumer_key = os.environ['X_CONSUMER_KEY']
consumer_secret = os.environ['X_CONSUMER_SECRET']
access_token = os.environ['X_ACCESS_TOKEN']
access_token_secret = os.environ['X_ACCESS_TOKEN_SECRET']

auth = OAuth1(consumer_key, consumer_secret, access_token, access_token_secret)

# Upload image
url = "https://upload.twitter.com/1.1/media/upload.json"

with open(image_path, 'rb') as f:
    files = {'media': f}
    response = requests.post(url, auth=auth, files=files)

if response.status_code == 200:
    media_id = response.json()['media_id_string']
    print(media_id)
else:
    print(f"Error: {response.status_code} - {response.text}", file=sys.stderr)
    exit(1)
EOF
)
  
  if [ $? -eq 0 ]; then
    echo "$upload_response"
  else
    return 1
  fi
}

# Post to Twitter with image
twitter_post_with_image() {
  local text="$1"
  local image_path="$2"
  
  echo "Uploading image to Twitter..." >&2
  local media_id=$(twitter_upload_image "$image_path")
  
  if [ $? -ne 0 ]; then
    echo "Failed to upload image to Twitter" >&2
    return 1
  fi
  
  echo "Image uploaded (media_id: $media_id)" >&2
  
  # Post with media_id (credentials already loaded)
  python3 - "$text" "$media_id" <<'EOF'
import requests
from requests_oauthlib import OAuth1
import os
import sys
import json

text = sys.argv[1]
media_id = sys.argv[2]

consumer_key = os.environ['X_CONSUMER_KEY']
consumer_secret = os.environ['X_CONSUMER_SECRET']
access_token = os.environ['X_ACCESS_TOKEN']
access_token_secret = os.environ['X_ACCESS_TOKEN_SECRET']

auth = OAuth1(consumer_key, consumer_secret, access_token, access_token_secret)

url = "https://api.twitter.com/2/tweets"
payload = {
    "text": text,
    "media": {
        "media_ids": [media_id]
    }
}

response = requests.post(url, auth=auth, json=payload)

if response.status_code == 201:
    tweet = response.json()
    tweet_id = tweet['data']['id']
    print(f"✓ Twitter posted: https://twitter.com/user/status/{tweet_id}")
else:
    print(f"✗ Twitter failed: {response.status_code} - {response.text}", file=sys.stderr)
    exit(1)
EOF
}


path: lib/validate.sh
content: #!/bin/bash
# Validation library for social-post

# Farcaster byte limit (10% safety buffer applied)
FARCASTER_BYTE_LIMIT=288  # 320 - 10% = 288

# Load tier detection library
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/tier-detection.sh"

# Check Twitter character count (dynamic limit based on account tier)
validate_twitter() {
  local text="$1"
  local account="${TWITTER_ACCOUNT:-mr_crtee}"
  local force_refresh="${2:-false}"
  
  local char_count=${#text}
  local twitter_limit=$(get_twitter_char_limit_buffered "$account" "$force_refresh")
  
  if [ "$char_count" -gt "$twitter_limit" ]; then
    echo "❌ Twitter: $char_count/$twitter_limit characters (over by $(($char_count - $twitter_limit)))" >&2
    return 1
  else
    echo "✅ Twitter: $char_count/$twitter_limit characters" >&2
    return 0
  fi
}

# Check Farcaster byte count (UTF-8)
validate_farcaster() {
  local text="$1"
  local byte_count=$(echo -n "$text" | wc -c)
  
  if [ "$byte_count" -gt "$FARCASTER_BYTE_LIMIT" ]; then
    echo "❌ Farcaster: $byte_count/$FARCASTER_BYTE_LIMIT bytes (over by $(($byte_count - $FARCASTER_BYTE_LIMIT)))" >&2
    return 1
  else
    echo "✅ Farcaster: $byte_count/$FARCASTER_BYTE_LIMIT bytes" >&2
    return 0
  fi
}

# Truncate text to fit platform
truncate_for_twitter() {
  local text="$1"
  local account="${TWITTER_ACCOUNT:-mr_crtee}"
  local twitter_limit=$(get_twitter_char_limit_buffered "$account")
  
  if [ "${#text}" -gt "$twitter_limit" ]; then
    echo "${text:0:$((twitter_limit-3))}..."
  else
    echo "$text"
  fi
}

truncate_for_farcaster() {
  local text="$1"
  local byte_count=$(echo -n "$text" | wc -c)
  
  if [ "$byte_count" -gt "$FARCASTER_BYTE_LIMIT" ]; then
    # Simple truncation (not perfect for multi-byte chars, but safe)
    local cutoff=$((FARCASTER_BYTE_LIMIT - 3))
    echo "${text:0:$cutoff}..."
  else
    echo "$text"
  fi
}

# Validate both platforms
validate_both() {
  local text="$1"
  local twitter_ok=0
  local farcaster_ok=0
  
  validate_twitter "$text" && twitter_ok=1
  validate_farcaster "$text" && farcaster_ok=1
  
  if [ "$twitter_ok" -eq 1 ] && [ "$farcaster_ok" -eq 1 ]; then
    return 0
  else
    return 1
  fi
}


path: lib/variation.sh
content: #!/bin/bash
# Text variation library - introduce subtle differences to avoid duplicate content detection

# Vary text with random subtle changes
vary_text() {
  local text="$1"
  local varied="$text"
  local original="$text"
  local attempts=0
  local max_attempts=10
  
  # Keep trying until we get a variation that's different from original
  while [ "$varied" = "$original" ] && [ $attempts -lt $max_attempts ]; do
    varied="$original"
    
    # Apply 2-3 random variations
    local variations=(
      "punctuation"
      "emoji"
      "spacing"
      "wording"
    )
    
    # Shuffle and pick 2-3 variations
    local num_variations=$((2 + RANDOM % 2))  # 2 or 3
    local applied=0
    
    # Shuffle variations array
    local shuffled=($(shuf -e "${variations[@]}"))
    
    for variation in "${shuffled[@]}"; do
      if [ $applied -ge $num_variations ]; then
        break
      fi
      
      case "$variation" in
        punctuation)
          varied=$(vary_punctuation "$varied")
          ;;
        emoji)
          varied=$(vary_emoji "$varied")
          ;;
        spacing)
          varied=$(vary_spacing "$varied")
          ;;
        wording)
          varied=$(vary_wording "$varied")
          ;;
      esac
      
      applied=$((applied + 1))
    done
    
    attempts=$((attempts + 1))
  done
  
  # If still no change, force an emoji addition
  if [ "$varied" = "$original" ]; then
    local emojis=("🚀" "✨" "🔥" "💪" "🎯" "⚡")
    local random_emoji="${emojis[$((RANDOM % ${#emojis[@]}))]}"
    varied="$varied $random_emoji"
  fi
  
  echo "$varied"
}

# Vary punctuation (! → . or vice versa, add/remove) - only at end of sentences
vary_punctuation() {
  local text="$1"
  local result="$text"
  
  # Random choice
  local choice=$((RANDOM % 3))
  
  case $choice in
    0)
      # Change sentence-ending ! to .
      if [[ "$result" == *"!"* ]]; then
        # Only change ! that's followed by newline or end of string
        result=$(echo "$result" | sed 's/!$/./; s/!\n/.\n/g')
      fi
      ;;
    1)
      # Change sentence-ending . to ! (avoid URLs)
      # Only if there's a period at end of line (not in URLs)
      if echo "$result" | grep -qE '\.$' && [[ "$result" != *"github"* ]] && [[ "$result" != *"http"* ]]; then
        result=$(echo "$result" | sed 's/\.$/!/')
      fi
      ;;
    2)
      # Add trailing period if none exists
      if ! echo "$result" | grep -qE '[.!]$'; then
        result="$result."
      fi
      ;;
  esac
  
  echo "$result"
}

# Vary emoji (add, remove, or change position)
vary_emoji() {
  local text="$1"
  local result="$text"
  
  local emojis=("🚀" "✨" "🔥" "💪" "🎯" "⚡" "🛠️" "🔧" "👀" "💯")
  local random_emoji="${emojis[$((RANDOM % ${#emojis[@]}))]}"
  
  # Check if text already has emojis
  if echo "$result" | grep -qE '[🚀✨🔥💪🎯⚡🛠️🔧👀💯]'; then
    # Remove existing emoji
    result=$(echo "$result" | sed 's/ [🚀✨🔥💪🎯⚡🛠️🔧👀💯]//g')
  else
    # Add emoji at end
    result="$result $random_emoji"
  fi
  
  echo "$result"
}

# Vary spacing (add/remove line breaks)
vary_spacing() {
  local text="$1"
  local result="$text"
  
  local choice=$((RANDOM % 2))
  
  case $choice in
    0)
      # Add extra line break before last line
      result=$(echo "$result" | sed '$ s/^/\n/')
      ;;
    1)
      # Remove one line break if multiple exist
      if [ $(echo "$result" | grep -c "^$") -gt 1 ]; then
        result=$(echo "$result" | sed '/^$/{ N; s/\n$//; }' | head -20)
      fi
      ;;
  esac
  
  echo "$result"
}

# Vary wording (subtle synonym swaps)
vary_wording() {
  local text="$1"
  local result="$text"
  
  # Simple word swaps (only if word exists)
  local swaps=(
    "just:now"
    "now:just"
    "new:latest"
    "check:see"
    "see:check"
    "live:available"
    "available:live"
    "published:released"
    "released:published"
  )
  
  local swap="${swaps[$((RANDOM % ${#swaps[@]}))]}"
  local from="${swap%:*}"
  local to="${swap#*:}"
  
  # Only swap if the word exists (case insensitive, whole word) and preserve the full text
  if echo "$result" | grep -qiw "$from"; then
    result=$(echo "$result" | sed "s/\b$from\b/$to/gi")
  fi
  
  echo "$result"
}


path: scripts/check-balance.sh
content: #!/bin/bash
# Check Farcaster custody wallet USDC balance on Base
# Loads credentials from ~/.openclaw/.env (centralized credential management)

set -e

ENV_FILE="/home/phan_harry/.openclaw/.env"
USDC_BASE=[REDACTED_SECRET]
BASE_RPC="https://mainnet.base.org"

if [ ! -f "$ENV_FILE" ]; then
  echo "❌ Error: .env not found at $ENV_FILE"
  exit 1
fi

CUSTODY_ADDRESS=$(grep "^FARCASTER_CUSTODY_ADDRESS=" "$ENV_FILE" | head -1 | cut -d= -f2-)

if [ -z "$CUSTODY_ADDRESS" ] || [ "$CUSTODY_ADDRESS" = "null" ]; then
  echo "❌ Error: FARCASTER_CUSTODY_ADDRESS not found in .env"
  exit 1
fi

echo "Farcaster Custody Wallet"
echo "━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Address: $CUSTODY_ADDRESS"
echo ""

# Check ETH balance
echo "Checking balances on Base..."
echo ""

ETH_BALANCE_WEI=$(curl -s -X POST "$BASE_RPC" \
  -H "Content-Type: application/json" \
  -d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getBalance\",\"params\":[\"$CUSTODY_ADDRESS\",\"latest\"],\"id\":1}" \
  | jq -r '.result')

if [ -n "$ETH_BALANCE_WEI" ] && [ "$ETH_BALANCE_WEI" != "null" ]; then
  ETH_BALANCE=$(python3 -c "print(int('$ETH_BALANCE_WEI', 16) / 10**18)")
  echo "💎 ETH Balance: $ETH_BALANCE ETH"
else
  echo "⚠️  Could not fetch ETH balance"
fi

# Check USDC balance (6 decimals)
USDC_BALANCE_RAW=$(curl -s -X POST "$BASE_RPC" \
  -H "Content-Type: application/json" \
  -d "{\"jsonrpc\":\"2.0\",\"method\":\"eth_call\",\"params\":[{\"to\":\"$USDC_BASE\",\"data\":\"0x70a08231000000000000000000000000${CUSTODY_ADDRESS:2}\"},\"latest\"],\"id\":1}" \
  | jq -r '.result')

if [ -n "$USDC_BALANCE_RAW" ] && [ "$USDC_BALANCE_RAW" != "null" ]; then
  USDC_BALANCE=$(python3 -c "print(int('$USDC_BALANCE_RAW', 16) / 10**6)")
  CASTS_REMAINING=$(python3 -c "print(int(int('$USDC_BALANCE_RAW', 16) / 1000))")

  echo "💵 USDC Balance: $USDC_BALANCE USDC"
  echo "📢 Casts Remaining: ~$CASTS_REMAINING (at 0.001 USDC per cast)"

  LOW_BALANCE=$(python3 -c "print('1' if $USDC_BALANCE < 0.1 else '0')")
  if [ "$LOW_BALANCE" = "1" ]; then
    echo ""
    echo "⚠️  Low balance! Consider funding your wallet."
    echo "   Send USDC to: $CUSTODY_ADDRESS (on Base chain)"
  fi
else
  echo "⚠️  Could not fetch USDC balance"
fi

echo ""
echo "View on BaseScan:"
echo "https://basescan.org/address/$CUSTODY_ADDRESS"


path: scripts/post.sh
content: #!/bin/bash
# Social Post - Post to Twitter and/or Farcaster

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LIB_DIR="$SCRIPT_DIR/../lib"

# Load libraries
source "$LIB_DIR/validate.sh"
source "$LIB_DIR/twitter.sh"
source "$LIB_DIR/farcaster.sh"
source "$LIB_DIR/threads.sh"
source "$LIB_DIR/links.sh"
source "$LIB_DIR/variation.sh"

# Defaults
POST_TWITTER=false
POST_FARCASTER=false
IMAGE_PATH=""
AUTO_TRUNCATE=false
DRY_RUN=false
THREAD_MODE=false
SHORTEN_LINKS=false
AUTO_CONFIRM=false
VARY_TEXT=false
TEXT=""
TWITTER_ACCOUNT="mr_crtee"  # Default account
REFRESH_TIER=false
FORCE_THREAD=false

# Parse arguments
while [[ $# -gt 0 ]]; do
  case $1 in
    --twitter)
      POST_TWITTER=true
      shift
      ;;
    --farcaster)
      POST_FARCASTER=true
      shift
      ;;
    --image)
      IMAGE_PATH="$2"
      shift 2
      ;;
    --truncate)
      AUTO_TRUNCATE=true
      shift
      ;;
    --thread)
      THREAD_MODE=true
      FORCE_THREAD=true
      shift
      ;;
    --refresh-tier)
      REFRESH_TIER=true
      shift
      ;;
    --shorten-links)
      SHORTEN_LINKS=true
      shift
      ;;
    --dry-run)
      DRY_RUN=true
      shift
      ;;
    --yes|-y)
      AUTO_CONFIRM=true
      shift
      ;;
    --account)
      TWITTER_ACCOUNT="$2"
      shift 2
      ;;
    --vary)
      VARY_TEXT=true
      shift
      ;;
    --help|-h)
      cat <<EOF
Usage: post.sh [OPTIONS] "Your message"

Post to Twitter and/or Farcaster with text and optional image.

Options:
  --twitter         Post to Twitter only
  --farcaster       Post to Farcaster only
  --account <name>  Twitter account (mr_crtee or oxdasx, default: mr_crtee)
  --vary            Auto-vary text to avoid duplicate content detection
  --image <path>    Attach image
  --truncate        Auto-truncate if over limit
  --thread          Force thread mode (split into multiple posts)
  --refresh-tier    Force refresh account tier cache (Basic vs Premium)
  --shorten-links   Shorten URLs to save characters
  --dry-run         Preview without posting
  -y, --yes         Skip all confirmation prompts
  --help            Show this help

Examples:
  post.sh "gm!"                                        # Both platforms (mr_crtee)
  post.sh --twitter "Twitter only"                     # Twitter only (mr_crtee)
  post.sh --account oxdasx --twitter "From 0xdas"      # Twitter as @0xdasx
  post.sh --vary --twitter "Same text, auto-varied"    # Auto-vary to avoid dupes
  post.sh --farcaster "Farcaster only"                 # Farcaster only
  post.sh --image photo.jpg "Check this out"          # Both with image
  post.sh --thread "Very long text..."                # Auto-thread
  post.sh --shorten-links "Check https://example.com" # Shorten URLs
  post.sh --twitter --image pic.png "New feature"     # Twitter with image

Platform Limits:
  Twitter:   Auto-detected based on account tier (cached for 24h)
             - Basic/Free: 252 chars (280 - 10% buffer)
             - Premium/Premium+: 22,500 chars (25,000 - 10% buffer)
  Farcaster: 288 bytes (320 - 10% buffer)

Tier Detection:
  The skill automatically detects your Twitter account tier on first use.
  Premium accounts can post up to 25k characters in a single tweet.
  Basic accounts are limited to 280 characters per tweet.
  Use --refresh-tier to force a tier re-check (if subscription changed).

Costs:
  Twitter:   Consumption-based (pay per API request, no tiers/subscriptions)
  Farcaster: 0.001 USDC per cast (deducted from custody wallet on Base)
EOF
      exit 0
      ;;
    *)
      TEXT="$1"
      shift
      ;;
  esac
done

# If no platform specified, post to both
if [ "$POST_TWITTER" = false ] && [ "$POST_FARCASTER" = false ]; then
  POST_TWITTER=true
  POST_FARCASTER=true
fi

# Validate text provided
if [ -z "$TEXT" ]; then
  echo "Error: No text provided"
  echo "Usage: post.sh [OPTIONS] \"Your message\""
  echo "Try: post.sh --help"
  exit 1
fi

# Validate image path if provided
if [ -n "$IMAGE_PATH" ] && [ ! -f "$IMAGE_PATH" ]; then
  echo "Error: Image not found at $IMAGE_PATH"
  exit 1
fi

echo "=== Social Post ==="
echo ""
echo "Original text: $TEXT"
[ -n "$IMAGE_PATH" ] && echo "Image: $IMAGE_PATH"
echo ""

# Shorten links if requested
if [ "$SHORTEN_LINKS" = true ]; then
  echo "Shortening links..."
  ORIGINAL_LENGTH=${#TEXT}
  TEXT=$(shorten_links_in_text "$TEXT")
  NEW_LENGTH=${#TEXT}
  SAVED=$((ORIGINAL_LENGTH - NEW_LENGTH))
  if [ "$SAVED" -gt 0 ]; then
    echo "Saved $SAVED characters"
  fi
  echo ""
fi

# Detect Twitter account tier (if posting to Twitter)
TWITTER_TIER="basic"
TWITTER_LIMIT=252
if [ "$POST_TWITTER" = true ]; then
  echo "Detecting Twitter account tier..."
  source "$LIB_DIR/tier-detection.sh"
  TWITTER_TIER=$(detect_twitter_tier "$TWITTER_ACCOUNT" "$REFRESH_TIER")
  TWITTER_LIMIT=$(get_twitter_char_limit_buffered "$TWITTER_ACCOUNT" "$REFRESH_TIER")
  
  case "$TWITTER_TIER" in
    premium|premium_plus)
      echo "✓ Twitter account: Premium ($TWITTER_LIMIT chars available)"
      ;;
    basic)
      echo "✓ Twitter account: Basic ($TWITTER_LIMIT chars available)"
      ;;
  esac
  echo ""
fi

# Validate character/byte limits
TWITTER_VALID=true
FARCASTER_VALID=true

if [ "$POST_TWITTER" = true ]; then
  validate_twitter "$TEXT" "$REFRESH_TIER" || TWITTER_VALID=false
fi

if [ "$POST_FARCASTER" = true ]; then
  validate_farcaster "$TEXT" || FARCASTER_VALID=false
fi

echo ""

# Handle validation failures
if { [ "$POST_TWITTER" = true ] && [ "$TWITTER_VALID" = false ]; } || \
   { [ "$POST_FARCASTER" = true ] && [ "$FARCASTER_VALID" = false ]; }; then
  
  if [ "$THREAD_MODE" = true ]; then
    echo "Text exceeds limit. Thread mode enabled - will split into multiple posts."
    # Thread handling is done later in posting section
  elif [ "$AUTO_TRUNCATE" = true ]; then
    echo "Auto-truncating text..."
    if [ "$POST_TWITTER" = true ] && [ "$TWITTER_VALID" = false ]; then
      TEXT=$(truncate_for_twitter "$TEXT")
      echo "Twitter text truncated to: $TEXT"
    fi
    if [ "$POST_FARCASTER" = true ] && [ "$FARCASTER_VALID" = false ]; then
      TEXT=$(truncate_for_farcaster "$TEXT")
      echo "Farcaster text truncated to: $TEXT"
    fi
  else
    echo ""
    echo "Text exceeds platform limits. Options:"
    echo "  1. Shorten your text and try again"
    echo "  2. Use --truncate flag to auto-truncate"
    echo "  3. Use --thread flag to split into multiple posts"
    echo "  4. Use --shorten-links to compress URLs"
    echo "  5. Post to one platform only (--twitter or --farcaster)"
    exit 1
  fi
fi

# Apply text variation if --vary flag is set
if [ "$VARY_TEXT" = true ]; then
  echo "→ Applying text variation to avoid duplicate content detection..."
  ORIGINAL_TEXT="$TEXT"
  TEXT=$(vary_text "$TEXT")
  echo ""
fi

# For Premium accounts posting > 280 chars (but within Premium limit):
# Offer threading as an option (unless --thread was explicitly set or --auto-confirm)
if [ "$POST_TWITTER" = true ] && [ "$TWITTER_VALID" = true ] && [ "$FORCE_THREAD" = false ]; then
  local char_count=${#TEXT}
  if [[ "$TWITTER_TIER" =~ ^premium ]]; then
    # Premium account - check if text is > 280 (traditional limit)
    if [ "$char_count" -gt 280 ] && [ "$char_count" -le "$TWITTER_LIMIT" ]; then
      # Text would fit in single Premium post, but offer threading
      if [ "$AUTO_CONFIRM" = false ] && [ -t 0 ]; then
        echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
        echo "📝 Premium account: Your text ($char_count chars) can be posted as:"
        echo "   1. Single long post (uses Premium 25k char limit)"
        echo "   2. Thread (split into multiple tweets)"
        echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
        echo ""
        echo -n "Thread this instead? (y/n): "
        read -r THREAD_CHOICE
        if [[ "$THREAD_CHOICE" =~ ^[Yy]$ ]]; then
          THREAD_MODE=true
          echo "→ Preparing threaded draft..."
        else
          echo "→ Posting as single long tweet..."
        fi
        echo ""
      fi
    fi
  fi
fi

# Show draft preview
echo "=== Draft Preview ==="
echo ""
if [ "$VARY_TEXT" = true ] && [ -n "$ORIGINAL_TEXT" ]; then
  echo "Original text:"
  echo "─────────────────────────────────────────────"
  echo "$ORIGINAL_TEXT"
  echo "─────────────────────────────────────────────"
  echo ""
  echo "Varied text (will be posted):"
  echo "─────────────────────────────────────────────"
  echo "$TEXT"
  echo "─────────────────────────────────────────────"
else
  echo "Text to post:"
  echo "─────────────────────────────────────────────"
  echo "$TEXT"
  echo "─────────────────────────────────────────────"
fi
[ -n "$IMAGE_PATH" ] && echo "Image: $IMAGE_PATH"
echo ""
echo "Targets:"
[ "$POST_TWITTER" = true ] && echo "  • Twitter (@${TWITTER_ACCOUNT})"
[ "$POST_FARCASTER" = true ] && echo "  • Farcaster"
echo ""

# Dry run mode
if [ "$DRY_RUN" = true ]; then
  echo "=== Dry Run (not actually posting) ==="
  [ "$POST_TWITTER" = true ] && echo "Would post to Twitter"
  [ "$POST_FARCASTER" = true ] && echo "Would post to Farcaster"
  [ -n "$IMAGE_PATH" ] && echo "Would upload image: $IMAGE_PATH"
  exit 0
fi

# Confirmation prompt (skip if running non-interactively or with --yes flag)
if [ "$AUTO_CONFIRM" = false ] && [ -t 0 ]; then
  echo -n "Proceed with posting? (y/n): "
  read -r CONFIRM
  if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then
    echo "Cancelled."
    exit 0
  fi
  echo ""
elif [ "$AUTO_CONFIRM" = true ]; then
  echo "Auto-confirmed (--yes flag). Proceeding..."
  echo ""
fi

# Export Twitter account for library to use
export TWITTER_ACCOUNT

# Post to platforms
echo "=== Posting ==="
echo ""

SUCCESS=true

# Check if we need threading
NEEDS_THREAD_TWITTER=false
NEEDS_THREAD_FARCASTER=false

if [ "$THREAD_MODE" = true ]; then
  if [ "$POST_TWITTER" = true ]; then
    needs_threading "$TEXT" "twitter" && NEEDS_THREAD_TWITTER=true
  fi
  if [ "$POST_FARCASTER" = true ]; then
    needs_threading "$TEXT" "farcaster" && NEEDS_THREAD_FARCASTER=true
  fi
fi

# Twitter posting
if [ "$POST_TWITTER" = true ]; then
  echo "→ Twitter..."
  
  if [ "$NEEDS_THREAD_TWITTER" = true ]; then
    echo "Creating thread..."
    thread_parts=$(split_into_thread "$TEXT" "twitter")
    part_num=1
    previous_tweet_id=""
    
    while IFS= read -r part; do
      if [ "$part" = "---THREAD_SEPARATOR---" ]; then
        continue
      fi
      
      echo "  Part $part_num..."
      
      if [ "$part_num" -eq 1 ]; then
        # First post
        if [ -n "$IMAGE_PATH" ]; then
          result=$(twitter_post_with_image "$part" "$IMAGE_PATH")
        else
          result=$(twitter_post_text "$part")
        fi
        # Extract tweet ID from result
        previous_tweet_id=$(echo "$result" | grep -oE 'status/[0-9]+' | sed 's/status\///' | tail -1)
        echo "$result"
      else
        # Reply to previous tweet
        result=$(twitter_post_text "$part" "$previous_tweet_id")
        previous_tweet_id="$result"
        echo "✓ Tweet posted: https://twitter.com/user/status/$previous_tweet_id"
      fi
      
      part_num=$((part_num + 1))
      sleep 2  # Brief delay between thread posts
    done <<< "$thread_parts"
  else
    if [ -n "$IMAGE_PATH" ]; then
      twitter_post_with_image "$TEXT" "$IMAGE_PATH" || SUCCESS=false
    else
      twitter_post_text "$TEXT" || SUCCESS=false
    fi
  fi
  echo ""
fi

# Farcaster posting
if [ "$POST_FARCASTER" = true ]; then
  echo "→ Farcaster..."
  
  if [ "$NEEDS_THREAD_FARCASTER" = true ]; then
    echo "Creating thread..."
    thread_parts=$(split_into_thread "$TEXT" "farcaster")
    part_num=1
    previous_cast_hash=""
    
    while IFS= read -r part; do
      if [ "$part" = "---THREAD_SEPARATOR---" ]; then
        continue
      fi
      
      echo "  Part $part_num..."
      
      if [ "$part_num" -eq 1 ]; then
        # First post
        if [ -n "$IMAGE_PATH" ]; then
          result=$(farcaster_post_with_image "$part" "$IMAGE_PATH" "")
        else
          result=$(farcaster_post_text "$part" "")
        fi
        # Extract cast hash from result
        previous_cast_hash=$(echo "$result" | grep "Cast hash:" | sed 's/Cast hash: //')
        echo "$result"
      else
        # Reply to previous cast
        result=$(farcaster_post_text "$part" "$previous_cast_hash")
        previous_cast_hash=$(echo "$result" | grep "Cast hash:" | sed 's/Cast hash: //')
        echo "$result"
      fi
      
      part_num=$((part_num + 1))
      sleep 2  # Brief delay between thread posts
    done <<< "$thread_parts"
  else
    if [ -n "$IMAGE_PATH" ]; then
      farcaster_post_with_image "$TEXT" "$IMAGE_PATH" "" || SUCCESS=false
    else
      farcaster_post_text "$TEXT" "" || SUCCESS=false
    fi
  fi
  echo ""
fi

if [ "$SUCCESS" = true ]; then
  echo "✅ Done!"
else
  echo "⚠️ Some posts failed. Check output above."
  exit 1
fi


path: scripts/reply.sh
content: #!/bin/bash
# Social Reply - Reply to tweets on Twitter and casts on Farcaster

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LIB_DIR="$SCRIPT_DIR/../lib"

# Load libraries
source "$LIB_DIR/validate.sh"
source "$LIB_DIR/twitter.sh"
source "$LIB_DIR/farcaster.sh"
source "$LIB_DIR/links.sh"

# Defaults
REPLY_TWITTER=false
REPLY_FARCASTER=false
IMAGE_PATH=""
AUTO_TRUNCATE=false
DRY_RUN=false
SHORTEN_LINKS=false
AUTO_CONFIRM=false
TEXT=""
TWITTER_ID=""
FARCASTER_HASH=""
TWITTER_ACCOUNT="mr_crtee"  # Default account

# Parse arguments
while [[ $# -gt 0 ]]; do
  case $1 in
    --twitter)
      REPLY_TWITTER=true
      TWITTER_ID="$2"
      shift 2
      ;;
    --farcaster)
      REPLY_FARCASTER=true
      FARCASTER_HASH="$2"
      shift 2
      ;;
    --image)
      IMAGE_PATH="$2"
      shift 2
      ;;
    --truncate)
      AUTO_TRUNCATE=true
      shift
      ;;
    --shorten-links)
      SHORTEN_LINKS=true
      shift
      ;;
    --dry-run)
      DRY_RUN=true
      shift
      ;;
    --yes|-y)
      AUTO_CONFIRM=true
      shift
      ;;
    --account)
      TWITTER_ACCOUNT="$2"
      shift 2
      ;;
    --help|-h)
      cat <<EOF
Usage: reply.sh [OPTIONS] "Your reply text"

Reply to tweets on Twitter and/or casts on Farcaster.

Options:
  --twitter <tweet_id>       Reply to Twitter tweet with this ID
  --farcaster <cast_hash>    Reply to Farcaster cast with this hash
  --account <name>           Twitter account (mr_crtee or oxdasx, default: mr_crtee)
  --image <path>             Attach image to reply
  --truncate                 Auto-truncate if over limit
  --shorten-links            Shorten URLs to save characters
  --dry-run                  Preview without posting
  -y, --yes                  Skip confirmation prompt
  --help                     Show this help

Examples:
  # Reply to Twitter tweet
  reply.sh --twitter 1234567890 "Great point!"
  
  # Reply to Farcaster cast
  reply.sh --farcaster 0xabcd1234... "Interesting perspective!"
  
  # Reply to both (if same content)
  reply.sh --twitter 123 --farcaster 0xabc "Nice!"
  
  # Reply with image
  reply.sh --twitter 123 --image photo.jpg "Check this out"
  
  # Shorten links in reply
  reply.sh --twitter 123 --shorten-links "See https://example.com/very-long-url"

Platform Limits:
  Twitter:   252 characters (with 10% buffer)
  Farcaster: 288 bytes (with 10% buffer)

Getting IDs:
  Twitter:   Tweet ID is in the URL: twitter.com/user/status/[ID]
  Farcaster: Cast hash from URL: farcaster.xyz/~/conversations/[HASH]
             Or use "0x" prefixed hash

Costs:
  Twitter:   Consumption-based (pay per API request)
  Farcaster: 0.001 USDC per cast (deducted from custody wallet on Base)
EOF
      exit 0
      ;;
    *)
      TEXT="$1"
      shift
      ;;
  esac
done

# Validate inputs
if [ -z "$TEXT" ]; then
  echo "Error: No reply text provided"
  echo "Usage: reply.sh [OPTIONS] \"Your reply text\""
  echo "Try: reply.sh --help"
  exit 1
fi

if [ "$REPLY_TWITTER" = false ] && [ "$REPLY_FARCASTER" = false ]; then
  echo "Error: No platform specified. Use --twitter or --farcaster"
  echo "Try: reply.sh --help"
  exit 1
fi

if [ "$REPLY_TWITTER" = true ] && [ -z "$TWITTER_ID" ]; then
  echo "Error: Twitter ID required when using --twitter"
  exit 1
fi

if [ "$REPLY_FARCASTER" = true ] && [ -z "$FARCASTER_HASH" ]; then
  echo "Error: Farcaster hash required when using --farcaster"
  exit 1
fi

# Validate image path if provided
if [ -n "$IMAGE_PATH" ] && [ ! -f "$IMAGE_PATH" ]; then
  echo "Error: Image not found at $IMAGE_PATH"
  exit 1
fi

echo "=== Social Reply ==="
echo ""
echo "Reply text: $TEXT"
[ -n "$IMAGE_PATH" ] && echo "Image: $IMAGE_PATH"
echo ""
[ "$REPLY_TWITTER" = true ] && echo "→ Replying to Twitter tweet: $TWITTER_ID"
[ "$REPLY_FARCASTER" = true ] && echo "→ Replying to Farcaster cast: $FARCASTER_HASH"
echo ""

# Shorten links if requested
if [ "$SHORTEN_LINKS" = true ]; then
  echo "Shortening links..."
  ORIGINAL_LENGTH=${#TEXT}
  TEXT=$(shorten_links_in_text "$TEXT")
  NEW_LENGTH=${#TEXT}
  SAVED=$((ORIGINAL_LENGTH - NEW_LENGTH))
  if [ "$SAVED" -gt 0 ]; then
    echo "Saved $SAVED characters"
  fi
  echo ""
fi

# Validate character/byte limits
TWITTER_VALID=true
FARCASTER_VALID=true

if [ "$REPLY_TWITTER" = true ]; then
  validate_twitter "$TEXT" || TWITTER_VALID=false
fi

if [ "$REPLY_FARCASTER" = true ]; then
  validate_farcaster "$TEXT" || FARCASTER_VALID=false
fi

echo ""

# Handle validation failures
if { [ "$REPLY_TWITTER" = true ] && [ "$TWITTER_VALID" = false ]; } || \
   { [ "$REPLY_FARCASTER" = true ] && [ "$FARCASTER_VALID" = false ]; }; then
  
  if [ "$AUTO_TRUNCATE" = true ]; then
    echo "Auto-truncating text..."
    if [ "$REPLY_TWITTER" = true ] && [ "$TWITTER_VALID" = false ]; then
      TEXT=$(truncate_for_twitter "$TEXT")
      echo "Twitter text truncated to: $TEXT"
    fi
    if [ "$REPLY_FARCASTER" = true ] && [ "$FARCASTER_VALID" = false ]; then
      TEXT=$(truncate_for_farcaster "$TEXT")
      echo "Farcaster text truncated to: $TEXT"
    fi
  else
    echo ""
    echo "Text exceeds platform limits. Options:"
    echo "  1. Shorten your text and try again"
    echo "  2. Use --truncate flag to auto-truncate"
    echo "  3. Use --shorten-links to compress URLs"
    exit 1
  fi
fi

# Show draft preview
echo "=== Reply Preview ==="
echo ""
echo "Reply text:"
echo "─────────────────────────────────────────────"
echo "$TEXT"
echo "─────────────────────────────────────────────"
[ -n "$IMAGE_PATH" ] && echo "Image: $IMAGE_PATH"
echo ""
echo "Replying to:"
[ "$REPLY_TWITTER" = true ] && echo "  • Twitter tweet: $TWITTER_ID (as @${TWITTER_ACCOUNT})"
[ "$REPLY_FARCASTER" = true ] && echo "  • Farcaster cast: $FARCASTER_HASH"
echo ""

# Dry run mode
if [ "$DRY_RUN" = true ]; then
  echo "=== Dry Run (not actually replying) ==="
  [ "$REPLY_TWITTER" = true ] && echo "Would reply to Twitter tweet: $TWITTER_ID"
  [ "$REPLY_FARCASTER" = true ] && echo "Would reply to Farcaster cast: $FARCASTER_HASH"
  [ -n "$IMAGE_PATH" ] && echo "Would upload image: $IMAGE_PATH"
  exit 0
fi

# Confirmation prompt (skip if running non-interactively or with --yes flag)
if [ "$AUTO_CONFIRM" = false ] && [ -t 0 ]; then
  echo -n "Proceed with reply? (y/n): "
  read -r CONFIRM
  if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then
    echo "Cancelled."
    exit 0
  fi
  echo ""
elif [ "$AUTO_CONFIRM" = true ]; then
  echo "Auto-confirmed (--yes flag). Proceeding..."
  echo ""
fi

# Export Twitter account for library to use
export TWITTER_ACCOUNT

# Post replies
echo "=== Posting Replies ==="
echo ""

SUCCESS=true

# Twitter reply
if [ "$REPLY_TWITTER" = true ]; then
  echo "→ Replying to Twitter..."
  
  if [ -n "$IMAGE_PATH" ]; then
    # For Twitter replies with images, we need to upload image first and then post
    echo "Uploading image to Twitter..." >&2
    media_id=$(twitter_upload_image "$IMAGE_PATH")
    
    if [ $? -ne 0 ]; then
      echo "Failed to upload image to Twitter" >&2
      SUCCESS=false
    else
      echo "Image uploaded (media_id: $media_id)" >&2
      
      # Post reply with media using inline Python script
      source /home/phan_harry/.openclaw/.env
      
      result=$(python3 - "$TEXT" "$TWITTER_ID" "$media_id" <<'EOF'
import requests
from requests_oauthlib import OAuth1
import os
import sys
import json

text = sys.argv[1]
reply_to = sys.argv[2]
media_id = sys.argv[3]

consumer_key = os.environ['X_CONSUMER_KEY']
consumer_secret = os.environ['X_CONSUMER_SECRET']
access_token = os.environ['X_ACCESS_TOKEN']
access_token_secret = os.environ['X_ACCESS_TOKEN_SECRET']

auth = OAuth1(consumer_key, consumer_secret, access_token, access_token_secret)

url = "https://api.twitter.com/2/tweets"
payload = {
    "text": text,
    "reply": {
        "in_reply_to_tweet_id": reply_to
    },
    "media": {
        "media_ids": [media_id]
    }
}

response = requests.post(url, auth=auth, json=payload)

if response.status_code == 201:
    tweet = response.json()
    tweet_id = tweet['data']['id']
    print(f"✓ Twitter reply posted: https://twitter.com/user/status/{tweet_id}")
else:
    print(f"✗ Twitter reply failed: {response.status_code} - {response.text}", file=sys.stderr)
    exit(1)
EOF
)
      if [ $? -eq 0 ]; then
        echo "$result"
      else
        SUCCESS=false
      fi
    fi
  else
    # Text-only reply
    result=$(twitter_post_text "$TEXT" "$TWITTER_ID")
    if [ $? -eq 0 ]; then
      echo "✓ Twitter reply posted: https://twitter.com/user/status/$result"
    else
      SUCCESS=false
    fi
  fi
  echo ""
fi

# Farcaster reply
if [ "$REPLY_FARCASTER" = true ]; then
  echo "→ Replying to Farcaster..."
  
  if [ -n "$IMAGE_PATH" ]; then
    farcaster_post_with_image "$TEXT" "$IMAGE_PATH" "$FARCASTER_HASH" || SUCCESS=false
  else
    farcaster_post_text "$TEXT" "$FARCASTER_HASH" || SUCCESS=false
  fi
  echo ""
fi

if [ "$SUCCESS" = true ]; then
  echo "✅ Done!"
else
  echo "⚠️ Some replies failed. Check output above."
  exit 1
fi


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