# X app setup

> **Human required.** A person creates this app in a browser. An agent cannot click through the X developer portal for you.

Bookmarks are readable only with OAuth 2.0 user context. An app-only bearer token gets a 403 on that endpoint, which is why tweet-factory dropped the old OAuth 1.0a path.

## In the developer portal

1. Open [developer.x.com](https://developer.x.com/en/portal/dashboard) and create an app (the free tier is enough for read access).
2. Under **User authentication settings** set:
   - **App permissions:** Read
   - **Type of App:** Web App, Automated App or Bot (this is a confidential client, which is what returns a refresh token)
   - **Callback URI:** `http://127.0.0.1:8675/callback`
   - Website URL: any URL you control. The CLI does not call it.
3. Save.
4. Open **Keys and tokens** and copy the **OAuth 2.0 Client ID** and **Client Secret** into `.env` as `TWITTER_OAUTH2_CLIENT_ID` and `TWITTER_OAUTH2_CLIENT_SECRET`.

The callback must match exactly, including the port `8675` and the path `/callback`. `tf auth` binds `127.0.0.1` unless you change it.

## Scopes the CLI requests

`tweet.read`, `users.read`, `like.read`, `bookmark.read`, `offline.access`.

`offline.access` is the refresh token. Without it, the access token expires and every later fetch needs a new browser login. A public client (native app) often will not return a refresh token; use the confidential web-app type and keep the client secret.

## Authorize

```bash
./tf auth
```

A browser opens. Log in as the account whose likes and bookmarks you want, and click **Authorize app**. The CLI writes `.tokens.json` (mode `0600`) and prints the granted scopes plus whether a refresh token is present.

Headless:

```bash
./tf auth --no-browser
```

Open the printed URL on a machine with a browser. The callback still has to hit the process that is waiting. From another machine, set `OAUTH_BIND_HOST` and `OAUTH_REDIRECT_URI` to a reachable URL, register that exact URI on the app, and pass the same values (`--bind`, `--redirect-uri`). The default wait is 5 minutes (`--timeout` seconds).

> **Verify.** `.tokens.json` exists, is non-empty, and the auth log said `Has refresh token: true`. File mode is `600` (`stat -c '%a' .tokens.json`).

## One machine

The refresh token rotates every time it is used. Run `tf fetch` / `tf refresh` / `tf cron` on a single machine. A second checkout that refreshes the same token invalidates the first, and the next call returns 401. Fix that with one `tf auth` on the machine you intend to keep.

> **Verify.** After auth, `./tf fetch` prints a database summary with `like`, `bookmark`, and `own` counts (a brand-new account may legitimately have zeros).
