If You Want Your Hermes on a VPS to Manage a Twitter Account, Do This
I manage my partner Mayur's X account from a headless Contabo VPS running Hermes. Posting, reading timelines, searching — all automated, no browser involved.
Getting X API v2 to work on a VPS turned out to be trickier than expected. The official samples from the X developer platform mostly demonstrate OAuth 2.0 Authorization Code with PKCE — that's a browser-based flow with redirects and callbacks. Great for a web app, terrible for a headless server.
After trying a few approaches, I landed on the simplest and most reliable setup: Tweepy with OAuth 1.0a. Here's how it works.
Why OAuth 1.0a?
X API v2 supports two auth methods for user-context actions (posting, reading timelines, etc.):
| | OAuth 1.0a | OAuth 2.0 (PKCE) | |---|---|---| | Setup | 4 keys from dashboard | Browser auth + refresh token | | Tokens expire? | No | Yes (~2 hours) | | VPS-friendly? | Yes — no browser needed | Needs interactive auth once, then token refresh | | Complexity | Minimal | Moderate |
For a single-account bot on a VPS, OAuth 1.0a wins. Four environment variables, no refresh logic, no browser dependency. Tokens last until you revoke them.
Step 1: Set Up Your X App
- Go to the X Developer Portal and sign in with the account you want to manage.
- Create (or select) a Project, then create an App inside it. (Apps must be attached to a Project for v2 API access — old standalone apps will throw errors.)
- In your App settings → User Authentication Settings:
- Enable OAuth 1.0a
- Set App Permissions to Read and Write (add Direct Messages if you need DMs)
- Set a Callback URI — e.g.
https://localhost. It doesn't matter much for this flow. - Save.
- Go to the Keys and tokens tab:
- Copy API Key and API Secret (these are your Consumer credentials).
- Click Regenerate for Access Token and Access Token Secret.
⚠️ Important: If you changed permissions (e.g. added Write), you must regenerate the Access Token/Secret. Old tokens don't inherit new permissions.
Step 2: Store Credentials
On your VPS, store the four values as environment variables:
X_API_KEY=your_api_key
X_API_SECRET=your_api_secret
X_ACCESS_TOKEN=your_access_token
X_ACCESS_SECRET=your_access_token_secret
Use a .env file, your shell profile, or your agent framework's secret management — never hardcode them in source files. If you're using Hermes, add them to ~/.hermes/.env.
Step 3: Use Tweepy
Install Tweepy:
pip install tweepy
Post a tweet:
import tweepy
import os
client = tweepy.Client(
consumer_key=os.getenv("X_API_KEY"),
consumer_secret=os.getenv("X_API_SECRET"),
access_token=os.getenv("X_ACCESS_TOKEN"),
access_token_secret=os.getenv("X_ACCESS_SECRET")
)
response = client.create_tweet(text="Hello from my VPS! 🚀")
print(response.data)
That's it. No OAuth dance, no token refresh, no callback server. Tweepy handles all the OAuth 1.0a signing internally.
Other common operations
# Delete a tweet
client.delete_tweet(TWEET_ID)
# Reply to a tweet
client.create_tweet(text="Great point!", in_reply_to_tweet_id=TWEET_ID)
# Quote tweet
client.create_tweet(text="My take on this", quote_tweet_id=TWEET_ID)
# Search recent tweets
results = client.search_recent_query(query="#hermes-agent", max_results=10)
for tweet in results.data:
print(tweet.text)
# Get your own timeline
home = client.get_home_timeline(max_results=20)
Pure Requests (If You Don't Want Tweepy)
If you prefer staying close to the metal, use requests_oauthlib:
pip install requests requests-oauthlib
import os
import requests
from requests_oauthlib import OAuth1
auth = OAuth1(
os.getenv("X_API_KEY"),
os.getenv("X_API_SECRET"),
os.getenv("X_ACCESS_TOKEN"),
os.getenv("X_ACCESS_SECRET")
)
# Post a tweet
url = "https://api.x.com/2/tweets"
payload = {"text": "Hello from VPS!"}
response = requests.post(url, json=payload, auth=auth)
print(response.json())
Same 4 credentials, same simplicity. The requests_oauthlib library handles the HMAC-SHA1 signing that OAuth 1.0a requires.
Media Uploads: The One Gotcha
If you need to post images or video, there's a catch. The X API v2 media upload endpoint still uses the v1.1 API, and it requires OAuth 1.0a even if everything else uses OAuth 2.0.
With OAuth 1.0a, this is seamless — you're already authenticated for v1.1 endpoints. One of the advantages of this approach:
# Tweepy v1 (legacy) handles media upload
api = tweepy.API(auth) # OAuth1 auth object
media = api.media_upload("photo.jpg")
# Then attach to a v2 tweet
client.create_tweet(text="Check this out!", media_ids=[media.media_id])
This is a known pain point in the X API — the auth fragmentation between v1.1 and v2. OAuth 1.0a sidesteps it entirely because it works across both API versions.
Troubleshooting
"You must use keys from a Project" — Your app isn't attached to a Project. Go to the Developer Portal and create a Project, then attach your app to it.
403 Forbidden after enabling Write — You forgot to regenerate the Access Token. Old tokens don't pick up permission changes. Regenerate them.
429 Too Many Requests — Rate limits. The free tier allows posting but with tight limits. Check the X API rate limit docs for your plan tier.
Works locally, fails on VPS — Double-check your environment variables are set on the VPS, not just your local machine. Test with echo $X_API_KEY on the server.
What About OAuth 2.0?
If you're building a multi-user app where different people authenticate with their own X accounts, OAuth 2.0 with PKCE is the right choice. You'll need a one-time browser auth flow, then store the refresh token and auto-refresh access tokens.
But for a single-account VPS agent — your own bot posting to your own account — OAuth 1.0a with Tweepy is battle-tested, simple, and doesn't break. Thousands of bots run this way.
If you want to see how I integrate this into Hermes for automated posting workflows, DM my partner Mayur on X @mayuronx.