Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ moltbot-skills/
β”‚
β”œβ”€β”€ base/ # Base (placeholder)
β”‚ └── SKILL.md
β”œβ”€β”€ lgi/ # Lobster General Intelligence (community)
β”‚ └── x-api/
β”‚ β”œβ”€β”€ SKILL.md
β”‚ └── scripts/
β”‚ └── x-post.mjs
β”œβ”€β”€ neynar/ # Neynar (placeholder)
β”‚ └── SKILL.md
└── zapper/ # Zapper (placeholder)
Expand All @@ -34,6 +39,7 @@ moltbot-skills/
|----------|-------|-------------|
| [bankr](https://bankr.bot) | [bankr](bankr/) | AI-powered crypto trading agent via natural language. Trade, manage portfolios, automate DeFi operations. |
| base | β€” | Placeholder |
| [lgi](https://github.com/lobstergeneralintelligence) | [x-api](lgi/x-api/) | Post to X (Twitter) using the official API. Reliable tweeting via OAuth 1.0a. |
| neynar | β€” | Placeholder |
| zapper | β€” | Placeholder |

Expand Down
101 changes: 101 additions & 0 deletions lgi/x-api/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
name: x-api
description: Post to X (Twitter) using the official API with OAuth 1.0a. Use when you need to tweet, post updates, or publish content. Bypasses rate limits and bot detection that affect cookie-based approaches like bird CLI.
---

# x-api 🐦

Post to X using the official API (OAuth 1.0a).

## When to Use

- Posting tweets (cookie-based `bird tweet` gets blocked by bot detection)
- Official API access is needed for reliability

For **reading** (timeline, search, mentions), use `bird` CLI instead β€” it's free and works well for reads.

## Setup

### 1. Get API Credentials

1. Go to https://developer.x.com/en/portal/dashboard
2. Create a Project and App
3. Set App permissions to **Read and Write**
4. Get your keys from "Keys and tokens" tab:
- API Key (Consumer Key)
- API Key Secret (Consumer Secret)
- Access Token
- Access Token Secret

### 2. Configure Credentials

**Option A: Environment variables**
```bash
export X_API_KEY="your-api-key"
export X_API_SECRET="your-api-secret"
export X_ACCESS_TOKEN="your-access-token"
export X_ACCESS_SECRET="your-access-token-secret"
```

**Option B: Config file** at `~/.clawdbot/secrets/x-api.json`
```json
{
"consumerKey": "your-api-key",
"consumerSecret": "your-api-secret",
"accessToken": "your-access-token",
"accessTokenSecret": "your-access-token-secret"
}
```

### 3. Install Dependency

```bash
npm install -g twitter-api-v2
```

## Post a Tweet

```bash
x-post "Your tweet text here"
```

Or with full path:
```bash
node /path/to/skills/x-api/scripts/x-post.mjs "Your tweet text here"
```

Supports multi-line tweets:
```bash
x-post "Line one

Line two

Line three"
```

Returns the tweet URL on success.

## Limits

- Free tier: 1,500 posts/month (requires credits in X Developer Portal)
- Basic tier ($100/mo): Higher limits

## Reading (use bird)

For reading, searching, and monitoring β€” use the `bird` CLI:

```bash
bird home # Timeline
bird mentions # Mentions
bird search "query" # Search
bird user-tweets @handle # User's posts
bird read <tweet-url> # Single tweet
```

## Troubleshooting

**402 Credits Depleted**: Add credits in X Developer Portal β†’ Dashboard

**401 Unauthorized**: Regenerate Access Token (ensure Read+Write permissions are set first)

**No credentials found**: Set env vars or create config file (see Setup above)
8 changes: 8 additions & 0 deletions lgi/x-api/scripts/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "x-api-scripts",
"version": "1.0.0",
"type": "module",
"dependencies": {
"twitter-api-v2": "^1.19.0"
}
}
86 changes: 86 additions & 0 deletions lgi/x-api/scripts/x-post.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env node
// Post to X using official API (OAuth 1.0a)
// Credentials: env vars (X_API_KEY, etc.) or ~/.clawdbot/secrets/x-api.json

import { TwitterApi } from 'twitter-api-v2';
import { readFileSync, existsSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';

// Load credentials from env vars or config file
function loadCredentials() {
// Try environment variables first
if (process.env.X_API_KEY && process.env.X_ACCESS_TOKEN) {
return {
consumerKey: process.env.X_API_KEY,
consumerSecret: process.env.X_API_SECRET,
accessToken: process.env.X_ACCESS_TOKEN,
accessTokenSecret: process.env.X_ACCESS_SECRET,
};
}

// Fall back to config file
const configPaths = [
join(homedir(), '.clawdbot', 'secrets', 'x-api.json'),
join(process.cwd(), '.x-api.json'),
];

for (const configPath of configPaths) {
if (existsSync(configPath)) {
try {
return JSON.parse(readFileSync(configPath, 'utf8'));
} catch (e) {
console.error(`❌ Failed to parse ${configPath}:`, e.message);
}
}
}

return null;
}

const credentials = loadCredentials();

if (!credentials) {
console.error(`❌ No credentials found.

Set environment variables:
export X_API_KEY="..."
export X_API_SECRET="..."
export X_ACCESS_TOKEN="..."
export X_ACCESS_SECRET="..."

Or create ~/.clawdbot/secrets/x-api.json:
{
"consumerKey": "...",
"consumerSecret": "...",
"accessToken": "...",
"accessTokenSecret": "..."
}
`);
process.exit(1);
}

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

const text = process.argv.slice(2).join(' ');

if (!text) {
console.error('Usage: x-post <tweet text>');
process.exit(1);
}

try {
const { data } = await client.v2.tweet(text);
// Get username from the access token (first part before the dash is user ID)
const userId = credentials.accessToken.split('-')[0];
console.log(`βœ… Posted: https://x.com/i/status/${data.id}`);
} catch (err) {
console.error('❌ Failed:', err.message);
if (err.data) console.error(JSON.stringify(err.data, null, 2));
process.exit(1);
}