Build Typefully
YESreplaces $12.5/mosaves $150/yrback to the verdict
A post composer and scheduler you run yourself: write threads in a clean editor with correct weighted character counts, queue them into slots, and publish through the X API or, because that API now costs per post, get a notification at slot time and a copy-to-clipboard button that keeps the tool useful without paying X anything.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Everything in this build runs on it: the server, the scripts, the tests.
Get it Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards. open ↗
Verify
node --version prints v22 or higher - installfree
Why Every step below is a command you type or a file you edit.
Get it VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal. open ↗
Verify
You can open a folder and run a command in its terminal - installfree
Why History for your code, and the way most hosts deploy.
Get it Install from git-scm.com or with your package manager, then run git init in the project folder once it exists. open ↗
Verify
git --version prints a version - decidefree
Why The scheduler stores UTC and renders in your zone. Getting this wrong posts at the wrong hour twice a year.
Get it Look it up: Europe/London, America/New_York, Asia/Karachi. Put it in .env as TIMEZONE.
- decidefree
Why Phase 3 fills slots you define, such as weekdays at 09:00 and 16:00.
Get it Write them as a JSON file: days and times in your timezone.
- accountfree
Why The free publishing path is a notification at slot time. ntfy needs no account: pick a long random topic name.
Get it Install the ntfy app on your phone, subscribe to a topic like posts-<random>, and put the topic in .env. Test with curl -d 'hello' ntfy.sh/<topic>. open ↗
- API keypay per use, about $0.015 per post, $0.20 with a link
Why Only needed for automatic posting. As of 2026 there is no free tier for new developers: roughly $0.015 per post and about $0.20 per post containing a link. A link-heavy habit costs more than Typefully.
Get it developer.x.com > sign in > create a project and app > User authentication settings with Read and Write > generate API Key, API Secret, Access Token and Access Token Secret. Add a payment method for pay-per-use. Verify the current prices in the console; they have changed repeatedly. open ↗
- accountabout $5 a month
Why This needs one process running all the time with a public address.
Get it Hetzner Cloud (from about 4 EUR), DigitalOcean or Fly.io. Ubuntu 24.04, the smallest size. You need SSH access and a public IP. Only needed for the deploy phase; develop locally first. open ↗
- installfree
Why Automatic HTTPS in front of the Node process. Without TLS the browser features this relies on (and your visitors' trust) do not work.
Get it On the VPS: follow the install steps at caddyserver.com/docs/install for Ubuntu. One Caddyfile with your domain and a reverse_proxy line is the whole config. open ↗
Verify
caddy version prints a version on the server
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- `drafts`: id, title, created_at, updated_at, status ('draft' | 'queued' |
'posted' | 'failed')
- `posts`: id, draft_id, position, body, media (JSON array of paths)
- `queue`: id, draft_id, scheduled_for, attempts, last_error, posted_at,
remote_ids (JSON)
- Store `scheduled_for` as UTC epoch milliseconds and render in the user's
timezone from a `.env` setting. A scheduler that stores local time will post at
the wrong hour twice a year, and you will not notice for months.Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
PORT | required | 3000 | Any free port. |
DATABASE_PATH | required | ./data/posts.db | SQLite file. |
TIMEZONE | required | Europe/London | IANA zone name. Verify before the first scheduled run. |
SLOTS_FILE | required | ./slots.json | Your posting slots. |
MEDIA_DIR | required | ./data/media | Attached images, resized and stripped of EXIF. |
NTFY_TOPIC | optional | posts-a8f3k2 | The topic you subscribed to in the ntfy app. |
X_API_KEYsecret | optional | ... | X developer portal, only if you publish automatically. |
X_API_SECRETsecret | optional | ... | X developer portal. |
X_ACCESS_TOKENsecret | optional | ... | X developer portal, generated with Read and Write. |
X_ACCESS_SECRETsecret | optional | ... | X developer portal. |
ADMIN_USER | required | admin | Any username for the basic-auth admin pages. |
ADMIN_PASSsecret | required | change-me-to-a-long-random-string | Generate one: openssl rand -base64 24. Never reuse a real password. |
The build, in order
Composer and drafts
Write threads, split them with ---, never lose a word.
drafts (id, title, created_at, updated_at, status), posts (id, draft_id, position, body, media JSON), revisions (id, draft_id, body, saved_at).
terminalmkdir composer && cd composer && git init && npm init -y && npm pkg set type=module mkdir -p data/media && cp .env.example .env
A distraction-free textarea; --- on its own line splits into post cards rendered beside it. Autosave on a debounce, keeping a small revision history.
done when · tick each as it passesCorrect character counting
Counts that match X exactly.
280 weighted characters; every URL counts as 23 regardless of length; CJK and emoji count 2. Not string.length.
terminalnpm install twitter-text@3
done when · tick each as it passesQueue and calendar
Slots from your file, next-free-slot, a calendar by day, no double booking.
queue (id, draft_id, scheduled_for UTC ms, attempts, last_error, posted_at, remote_ids JSON). Convert slots from TIMEZONE to UTC when scheduling.
done when · tick each as it passesPublishing, fallback first
Useful with no API key at all; automatic posting only when configured, with the cost stated.
A copy-thread button that copies the whole thread formatted for manual posting.
The scheduler loop, every minute: for each due item with no API key, post to ntfy.sh/NTFY_TOPIC with the first line and a link to the draft.
terminalcurl -d 'Time to post: your thread title' ntfy.sh/$NTFY_TOPIC
OAuth 1.0a user context. Post each item as a reply to the previous one; record remote ids; three retries on transient errors, never on a rejected post.
Count published posts and link posts this month and estimate the bill from the prices you verified.
done when · tick each as it passeswatch out- Never retry a rejected post. You will publish it four times.
- Verify X's prices in the developer console before relying on them; they have changed repeatedly.
Media
Up to four images per post, resized and stripped.
- terminal
npm install sharp@0.35.3
done when · tick each as it passesDeploy
Live, backed up, queue survives restarts.
Files
deploy/composer.serviceCaddyfileThe X cost per post and per link post with the date checked and a link to the console; the sentence that daily link posting costs more than the subscription; the timezone check.
Files
README.md
done when · tick each as it passesOperate it like a productproduct builder
Only for the product-builder path: know when the scheduler is down, never lose the database, and keep the server patched.
Answer 200 with the build id and a quick database read. Point a free uptime monitor (or your own, from the Healthchecks entry on this site) at it so an outage is noticed before a user notices.
One JSON line per request: method, path, status, duration, no raw IPs. Rotate weekly with logrotate, keep eight.
SQLite's .backup command makes a consistent copy while the app runs. Copy it to object storage or a second machine; then, once, restore it into a fresh checkout and confirm the app reads it.
terminalsqlite3 data/app.db ".backup '/tmp/app-$(date +%F).db'" rclone copy /tmp/app-$(date +%F).db remote:backups/
Firewall allowing only 22, 80 and 443; unattended security updates on; the app running as an unprivileged user under systemd with Restart=on-failure.
done when · tick each as it passes
That is the whole plan for Typefully. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- LinkedIn and other networks in v1. Each is its own API, auth model and content format.
- Analytics on published posts: reads cost money per call under pay-per-use.
- Multi-account, teams and AI rewrite suggestions.
- posting without paying X for API access
- multi-account and team features
- analytics on published posts
- AI rewrite suggestions
- Bluesky as a second publisher: its API is free and the interface is already there
- A browser bookmarklet that opens the composer with a quoted link
Need the files? The project pack on the verdict page hands your agent the whole brief · more social media.