nostrX
Self-hosted Python tool to automatically sync Nostr posts to Twitter/X with media support and state tracking.
File Browser:
- 📄 env.example
- 📄 LICENSE
- 📄 nostrx.py
- 📄 README.md
- 📄 requirements.txt
README
NostrX
A local, self-hosted tool to automatically sync your Nostr posts to Twitter/X.
Features
- Runs locally - No need to trust third-party services
- One-Way Sync - Reads from Nostr, posts to Twitter
- Stateful - Remembers where it left off (runs efficiently via cron)
- Media Support - Downloads images/videos from Nostr and uploads them to Twitter natively
- Filters replies - Only posts top-level notes
Setup
1. Install Dependencies
# Create virtual environment
python3 -m venv venv
source venv/bin/activate
# Install requirements
pip install -r requirements.txt
2. Configure Twitter/X Developer App (Payment Required)
To get API keys you need an X Developer account. Go to developer.twitter.com and sign in.
As of February 2026, X shifted its Developer API to a pay-as-you-go model. To post tweets, you must have a funded developer account.
API Pricing & Costs: - You must add a payment method and top up credits (usually a $5 or $10 minimum) under the Billing section of your portal. - Writing a standard tweet costs roughly $0.01 per post. - A $10 top-up covers between 500 to 1,000 tweets, providing months of automated syncing for most users. - NostrX is highly optimized to only consume credits when publishing.
Setup Instructions:
1. Navigate to Billing / Usage and ensure you have added a credit balance.
2. Create a project/app.
4. Configure Permissions (IMPORTANT):
* Go to Projects & Apps > [Your project name] > User authentication settings.
* Click Edit.
* App permissions: Change to "Read and Write".
* Type of App: Select "Web App, Automated App or Bot".
* Callback URI / Redirect URL: Enter http://localhost.
* Website URL: Enter your Twitter profile URL (e.g., https://twitter.com/yourhandle).
* Click Save.
5. Get Your Keys:
* Go to the Keys and Tokens tab.
* API Key and Secret: Copy these.
* Access Token and Secret: IMPORTANT: If you just changed permissions, you MUST click Regenerate to get new tokens with "Write" access. Old tokens will fail.
* Note: You can ignore "OAuth 2.0 Client ID" and "Client Secret".
3. Configure Environment Variables
Copy the example file to create your own configuration:
cp env.example .env
Open .env and paste your keys:
# .env file
TWITTER_API_KEY=your_api_key
TWITTER_API_SECRET=your_api_secret
TWITTER_ACCESS_TOKEN=your_access_token
TWITTER_ACCESS_SECRET=your_access_secret
4. Configure Your Nostr Account
Add your npub to the .env file:
NOSTR_NPUBS=npub1...
You can add multiple npubs by separating them with commas:
NOSTR_NPUBS=npub1...,npub1...
Optional: Custom Relays
By default, NostrX connects to 4 popular relays. You can add your own by uncommenting and editing the NOSTR_RELAYS line in .env:
NOSTR_RELAYS=wss://relay.damus.io,wss://nos.lol,wss://your-relay.com
Add as many relays as you want (comma-separated). More relays = better chance of finding your posts.
Usage
Run the script manually or via cron:
./venv/bin/python nostrx.py
First Run: It will check the last 24 hours of posts. Subsequent Runs: It will only check for new posts since the last sync.
Sync Behavior
First Run
When you run the tool for the very first time (and sync_state.json doesn't exist):
- It defaults to syncing posts from the last 24 hours only.
- This is a safety feature to prevent spamming your Twitter account with years of history.
Want to sync more history?
Open nostr_crossposter.py and find this line (around line 60):
"last_synced_timestamp": int(time.time()) - 86400,
Change 86400 (24 hours in seconds) to a larger number.
- 604800 = 7 days
- 2592000 = 30 days
Subsequent Runs
- The tool creates a file called
sync_state.json. - It records the exact timestamp of the last post it successfully synced.
- Next time you run it, it continues exactly where it left off, ensuring no posts are missed and no duplicates are created.
How it Works (Technical)
- State Tracking: Uses
sync_state.jsonto store the last synced Unix timestamp and a list of recent event IDs for deduplication. - Media: Automatically detects image/video URLs in your Nostr notes, downloads them to a temporary file, and uploads them to Twitter as native media attachments.
- One-Way: Strictly reads from Nostr and writes to Twitter. It does not read your Tweets.
- Character Limit: Posts longer than 280 characters are automatically truncated to 277 characters with "..." appended.
Credits & Inspiration
This tool was inspired by nos-crossposting-service by Planetary Social.
While that project is a centralized web service written in Go, this tool is a lightweight, self-hosted Python alternative designed for personal use.