---
cursor:
  subagentId: "bc-aa78a5a6-fd6d-5cbf-b881-c5d0493e3d85"
---

# @const_reborn tweet pull: history and final result

## Resolved at 16:54 UTC with ArbosApp (Project "Arbos", id 2098811811495579656)

**3,085 unique tweets pulled, 2023-02-16 to 2026-09-11.** Outputs in
`docs/sources/tweets/`: `const_reborn-tweets.jsonl` (5.3 MB, one raw tweet per
line), `const_reborn-tweets.md` (0.9 MB, newest first by month),
`const_reborn-summary.md`.

- Counts: originals 408, replies 1,626, quotes 434, retweets 617. 273 tweets
  carry a `note_tweet` (long-form) body.
- Profile counts 3,184 tweets; 99 are not returned by any endpoint. Both the
  timeline and full-archive search covered every date from account creation to
  now, so those 99 are tweets X no longer serves (deleted, withheld, or replies
  to protected/suspended accounts). Treat the archive as complete.
- `GET /2/users/:id/tweets` stopped at 687 tweets (back to 2026-01-09) with no
  next page, and missed 2 tweets that search found. It is not a reliable
  3,200-tweet source on this tier.
- `GET /2/tweets/search/all` **is allowed** on pay-per-usage. Without
  `start_time` it silently returns only the last 30 days. Passing
  `start_time=<account created_at>` gave the full archive at 500 per page in
  7 pages. It also has a 1-request-per-second limit (429 with
  `x-rate-limit-remaining` still high); the script now pauses 1.2 s between
  pages.
- Credits: `GET /2/usage/tweets` reported `project_usage` = 3,083 post reads
  right after the run (about **$15.42** at $0.005 per read, plus one user read
  at $0.01). Worst case if the counter lags: 3,771 reads ≈ $18.86.
- No API errors during the successful run.

Rerun later with `python x-tweets-pull.py` (fresh) or
`--reuse-timeline --resume-search` to continue an interrupted archive search.

## Earlier attempts (App 30477439, standalone): blocked

Status at 16:15 and 16:36 UTC: **0 tweets pulled.**

## Retry at 16:36 UTC (after Jacob pointed to Project 1902396256921452545)

Same credentials, same script, same result: every v2 call returns 403
`client-not-enrolled` for `client_id` **30477439**. The env file was not
modified since 16:10, and a bearer re-minted from the API key/secret is
identical to the stored one, so the keys still belong to App 30477439.

What the API says about the Project linkage:

- `GET /2/usage/tweets` is the only public endpoint that returns a `project_id`.
  It answered 403 `client-not-enrolled` too. X only serves this endpoint for
  Apps inside a Project, so the API itself confirms **App 30477439 is not in
  any Project** as of 16:36 UTC (not in 1902396256921452545 and not elsewhere).
- `GET /1.1/application/rate_limit_status.json` works (200) but carries no
  project field.
- Both `api.x.com` and `api.twitter.com` hosts give the same answer, so this is
  not a host issue.

What Jacob must do (either one):

1. **Attach the existing App.** In the developer portal, open Project
   1902396256921452545 ("const_rebor..."), choose *Add App* → *existing
   standalone App* → App 30477439. Existing keys keep working after attaching;
   no key rotation needed. If the portal does not list 30477439 as attachable,
   it belongs to a different developer account than the Project, and option 2
   applies.
2. **Use an App that already lives inside the Project.** In Project
   1902396256921452545 → Apps → (App) → *Keys and tokens*, generate the API
   Key/Secret, Bearer Token, and Access Token/Secret, and replace the values in
   `internal/secrets/x-api.env`. A quick self-check before sending them: any v2
   call made with those keys will either succeed or fail with a `client_id`
   that is **not** 30477439.

After either step, rerun `internal/x-tweets-pull.py`; the first log line
`[ok] user lookup via bearer` confirms the fix. Reads still need credits on
the Project (pay-per-usage; roughly $0.005 per tweet).

## What happened

Every X API v2 call was refused with HTTP 403. The same error came back for
the bearer token, for OAuth 1.0a (API key/secret + access token/secret), and
for a bearer token freshly minted from the API key/secret (which is identical
to the stored one, so the stored token is genuine).

Exact error body (client_id is the public App id, not a secret):

```json
{"client_id":"30477439",
 "detail":"When authenticating requests to the X API v2 endpoints, you must use keys and tokens from a developer App that is attached to a Project. You can create a project via the developer portal.",
 "reason":"client-not-enrolled",
 "registration_url":"https://developer.x.com/en/docs/projects/overview",
 "required_enrollment":"Appropriate Level of API Access",
 "title":"Client Forbidden",
 "type":"https://api.x.com/2/problems/client-forbidden"}
```

## Probe results (all credentials from `internal/secrets/x-api.env`)

| Call | Auth | Result |
| --- | --- | --- |
| `GET /2/users/by/username/const_reborn` | bearer | 403 client-not-enrolled |
| `GET /2/users/by/username/const_reborn` | OAuth 1.0a | 403 client-not-enrolled |
| `GET /2/users/me` | OAuth 1.0a | 403 client-not-enrolled |
| `GET /2/tweets/search/all?query=from:const_reborn` | bearer | 403 client-not-enrolled |
| `GET /2/tweets/search/recent?query=from:const_reborn` | bearer | 403 client-not-enrolled |
| `GET /1.1/statuses/user_timeline.json` | OAuth 1.0a | 403 code 453: "You currently have access to a subset of X API V2 endpoints and limited v1.1 endpoints (e.g. media post, oauth) only. If you need access to this endpoint, you may need a different access level." |
| `GET /1.1/users/show.json` | bearer, OAuth 1.0a | 403 (HTML block page) |
| `GET /1.1/account/verify_credentials.json` | OAuth 1.0a | **200** - credentials are valid; the access token belongs to the App owner's account (@ThePathofTao) |
| `POST /2/oauth2/token` client_credentials | OAuth2 client id/secret | 400 invalid_request (this grant is not offered for OAuth 2.0 user-context clients; the client id/secret only work with the browser PKCE flow) |

Meaning, one idea at a time:

- The keys are real and not revoked. Auth itself is fine.
- The App (id 30477439) is a **standalone App**. It is not attached to a **Project** in the X developer portal. X requires a Project for every v2 endpoint.
- The v1.1 read endpoints (the old way to fetch a timeline) are not part of this App's access level.
- No credential in the env file can read tweets today. Retrying will not change this.

## What is needed to unblock

1. In the [X developer portal](https://developer.x.com/en/portal/dashboard), create a Project and attach App 30477439 to it (or create a new App inside a Project and regenerate keys into `x-api.env`).
2. Have a **paid access level** on that Project. As of 2026, X sells API reads as **pay-per-usage credits** (Free tier is gone for new developers; legacy Basic $200/mo is retired; Pro $5,000/mo is closed to new sign-ups). Posts read cost about $0.005 each, users read about $0.01 each. Pulling ~3,200 tweets is roughly $16 of credits plus a small amount for the user lookup.
3. Endpoint access by tier:
   - `GET /2/users/:id/tweets` (user timeline, newest ~3,200 tweets): pay-per-usage credits are enough.
   - `GET /2/tweets/search/all` (full archive, needed only if the account has more than ~3,200 tweets): **Pro or Enterprise only**. Not available on pay-per-usage or legacy Basic. If the account exceeds ~3,200 tweets, the archive will be capped at the newest ~3,200.

## Ready-to-run script

`internal/x-tweets-pull.py` (this folder) does the whole job once the App is attached to a funded Project. It:

- tries bearer, then OAuth 1.0a;
- paginates `GET /2/users/:id/tweets` with `max_results=100`, all requested `tweet.fields` (including `note_tweet`) and `expansions=referenced_tweets.id`;
- sleeps until `x-rate-limit-reset` on 429;
- falls back to `GET /2/tweets/search/all` if the profile claims more tweets than the timeline returned;
- writes `docs/sources/tweets/const_reborn-tweets.jsonl`, `-tweets.md` (newest first, by month, ORIGINAL/REPLY/QUOTE markers, retweets at the end), and `-summary.md` (counts, date range, complete/capped flag, top 30 by likes).
- scrubs secret values from every log line and error it writes.

Run:

```bash
mkdir -p /tmp/xpull && cd /tmp/xpull && uv venv && source .venv/bin/activate
uv pip install requests requests-oauthlib
cp /cursor/stores/bc-96ff1b3e-00b6-4da6-8a0d-5e72965f7e86/internal/x-tweets-pull.py pull.py
python pull.py
```

The rendering step was tested against synthetic tweets; the API path was tested only up to the 403.
