Build Typefully

YESreplaces $12.5/mosaves $150/yrback to the verdict

0%0 of 24 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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.

estimated effort weekendthe files for this build are in the project pack

RuntimeNode 22, node:http and node:sqliteCountingtwitter-text (config v3)PublishingX API v2, optional, behind an interfaceNotificationsntfy or desktop notifications

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. 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

  2. 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

  3. 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

  4. 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.

  5. 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.

  6. free

    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 ↗

  7. 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 ↗

  8. about $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 ↗

  9. 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.

VariableNeededExampleWhere the value comes from
PORTrequired3000Any free port.
DATABASE_PATHrequired./data/posts.dbSQLite file.
TIMEZONErequiredEurope/LondonIANA zone name. Verify before the first scheduled run.
SLOTS_FILErequired./slots.jsonYour posting slots.
MEDIA_DIRrequired./data/mediaAttached images, resized and stripped of EXIF.
NTFY_TOPICoptionalposts-a8f3k2The topic you subscribed to in the ntfy app.
X_API_KEYsecretoptional...X developer portal, only if you publish automatically.
X_API_SECRETsecretoptional...X developer portal.
X_ACCESS_TOKENsecretoptional...X developer portal, generated with Read and Write.
X_ACCESS_SECRETsecretoptional...X developer portal.
ADMIN_USERrequiredadminAny username for the basic-auth admin pages.
ADMIN_PASSsecretrequiredchange-me-to-a-long-random-stringGenerate one: openssl rand -base64 24. Never reuse a real password.

The build, in order

  1. Composer and drafts

    Write threads, split them with ---, never lose a word.

    1. drafts (id, title, created_at, updated_at, status), posts (id, draft_id, position, body, media JSON), revisions (id, draft_id, body, saved_at).

      terminal
      mkdir composer && cd composer && git init && npm init -y && npm pkg set type=module
      mkdir -p data/media && cp .env.example .env
    2. 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 passes
  2. Correct character counting

    Counts that match X exactly.

    1. 280 weighted characters; every URL counts as 23 regardless of length; CJK and emoji count 2. Not string.length.

      terminal
      npm install twitter-text@3
    done when · tick each as it passes
  3. Queue and calendar

    Slots from your file, next-free-slot, a calendar by day, no double booking.

    1. 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 passes
  4. Publishing, fallback first

    Useful with no API key at all; automatic posting only when configured, with the cost stated.

    1. A copy-thread button that copies the whole thread formatted for manual posting.

    2. 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.

      terminal
      curl -d 'Time to post: your thread title' ntfy.sh/$NTFY_TOPIC
    3. 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.

    4. Count published posts and link posts this month and estimate the bill from the prices you verified.

    done when · tick each as it passes
    watch 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.
  5. Media

    Up to four images per post, resized and stripped.

    1. terminal
      npm install sharp@0.35.3
    done when · tick each as it passes
  6. Deploy

    Live, backed up, queue survives restarts.

    1. Files deploy/composer.serviceCaddyfile

    2. The 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 passes
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more social media.