The goal was simple: post to X from the server. No browser, no copy-paste, no manual step. The agent fires the tweet when new content goes live.
Here's what actually happened.
Step 1: X Developer Account
Apply at developer.x.com. Free to create. They ask you to describe your use cases — keep it specific and honest. Mine:
Personal automation on a self-hosted server. Posting content to @tedagentic when new posts go live. Reading my own timeline to avoid duplicates. Monitoring mentions. Single-user, no data storage beyond my own account activity.
Approved same day.
Step 2: App Permissions
This is where most people get stuck. The default app permission is Read only. You need to change it before generating tokens — tokens generated under Read-only stay Read-only even if you change permissions later.
Go to App Settings → User authentication settings and set:
- App permissions: Read and Write
- Type of App: Automated App or Bot
-
Callback URI:
https://yourdomain.com/(required field, won't be used) -
Website URL:
https://yourdomain.com
Save, then go to Keys and Tokens → Regenerate the Access Token and Access Token Secret. The regenerated tokens inherit the new Write permission.
Four keys total:
- Consumer Key (API Key)
- Consumer Secret (API Secret)
- Access Token
- Access Token Secret
Store them in ~/.env:
X_API_KEY=your_consumer_key
X_API_SECRET=your_consumer_secret
X_ACCESS_TOKEN=your_access_token
X_ACCESS_TOKEN_SECRET=your_access_token_secret
Step 3: The 402 Wall
First test post returned a 402 — Payment Required.
The X API free tier is read-only now. To write (post tweets), you need paid access. The old "1,500 tweets/month free" tier is gone. What X offers instead is pay-per-use credits — no monthly commitment, load whatever you need. $7 was enough to get started.
Go to the developer portal billing section, add credits, and the write endpoints unlock immediately.
Step 4: The Posting Script
Install the library. The script lives in ~/utils/ so create a package.json there if one doesn't exist, then install:
cd ~/utils && npm init -y && npm install twitter-api-v2
Then the script at ~/utils/tweet.cjs:
const { TwitterApi } = require('twitter-api-v2');
const fs = require('fs');
function loadEnv() {
const lines = fs.readFileSync('/home/aiserver/.env', 'utf8').split('\n');
for (const line of lines) {
const [k, ...v] = line.split('=');
if (k && v.length) process.env[k.trim()] = v.join('=').trim();
}
}
async function main() {
loadEnv();
const client = new TwitterApi({
appKey: process.env.X_API_KEY,
appSecret: process.env.X_API_SECRET,
accessToken: process.env.X_ACCESS_TOKEN,
accessSecret: process.env.X_ACCESS_TOKEN_SECRET,
});
let text = process.argv[2];
if (!text) {
const chunks = [];
for await (const chunk of process.stdin) chunks.push(chunk);
text = Buffer.concat(chunks).toString('utf8').trim();
}
const tweet = await client.v2.tweet(text);
console.log('Posted:', tweet.data.id);
console.log('URL: https://x.com/tedagentic/status/' + tweet.data.id);
}
main().catch(err => { console.error('Error:', err.message); process.exit(1); });
The .cjs extension matters here — the server's root package.json has "type": "module" which breaks require(). Naming it .cjs forces CommonJS mode without touching any config.
Usage:
node ~/utils/tweet.cjs "your tweet text"
# or pipe in
echo "tweet text" | node ~/utils/tweet.cjs
Step 5: First Tweet From the Server
Test post went out. Then the real one — a blog announcement, posted directly from the terminal:
node ~/utils/tweet.cjs "your tweet text here"
# Posted: 2054142254147604629
# URL: https://x.com/tedagentic/status/2054142254147604629
No browser. No copy-paste. The server called the API and the tweet appeared.
What's Next
tweet.cjs is a standalone CLI right now. The next step is wiring it into OpenClaw as a skill — so the agent can post to X directly from a Telegram message. Tell the bot "post this," it calls the script, confirms the URL back to Telegram.
That's the agentic layer: not automation that runs on a schedule, but a tool the agent can invoke on demand. The script is the foundation. The skill is what makes it part of the workflow.
That integration is the next build.
Top comments (0)