# ReadX - X/Twitter JSON API

Access Twitter/X data as JSON via micropayments - pay per call with
[x402](https://x402.org). Each call costs a small amount of USDC (default
**$0.01**); read-only, no X login required.

Base URL: **`https://readx.sh`**

## When to use

- Look up an X user's profile, bio, follower counts, or pinned tweet
- Export a user's followers or following list (paginated; full list or web-view)
- Fetch a user's timeline (tweets, replies, or media)
- Read a single tweet - with its conversation (replies), or on its own
- Read a tweet's edit history (latest plus prior versions)
- Read an X article (long-form post) by its id
- Search tweets or users; resolve a hashtag
- Read the members of an X List, or a broadcast
- Resolve a numeric user id -> username, or an @handle -> user id
- Check what's trending, worldwide or for a specific country

## Payment

The `/api/*` endpoints are paid per call with **x402**. A request with no
payment returns `402` with a `PAYMENT-REQUIRED` header. The challenge offers two
rails, **USDC on Base** and **USDC on Solana**, and the client picks whichever it
can pay. The service publishes its `/openapi.json` describing every endpoint. An
[agentcash](https://agentcash.dev) client pays automatically:

```bash
# x402 (USDC on Base or Solana)
npx agentcash fetch https://readx.sh/api/jack/profile
```

- The client needs native **USDC** in its wallet (Base or Solana). On Base,
  bridged/wrapped USDC (e.g. Stargate) is not accepted - x402 requires EIP-3009
  native USDC.

## Liveness check

To check connectivity or uptime, call `GET /api/health` - free, unauthenticated,
no parameters:

```bash
curl -s https://readx.sh/api/health
# {"message":"OK"}
```

Don't probe liveness by calling an endpoint below with its literal `:name`/`:id`
placeholder left in - those are real routes matched by any value, so an
unsubstituted placeholder just returns a normal `404 not_found` for a
nonexistent user/tweet, not a liveness signal.

## Endpoints

| Method & path | Returns |
|---------------|---------|
| `GET /api/:name` | user timeline (first page includes `data.pinned`; also `/with_replies`, `/with_media` — `/with_media` may omit `pinned`) |
| `GET /api/:name/profile` | user object + photo rail + pinned tweet |
| `GET /api/:name/followers` | a user's followers (~200/page; paginate with `?cursor=` + `pagination.next_cursor`) |
| `GET /api/:name/followers_web` | a user's followers as shown on the web (~70/page, capped at ~800; paginate with `?cursor=` + `pagination.next_cursor`) |
| `GET /api/:name/following` | accounts a user follows (~200/page; paginate with `?cursor=` + `pagination.next_cursor`) |
| `GET /api/:name/following_web` | accounts a user follows as shown on the web (~70/page, capped at ~800; paginate with `?cursor=` + `pagination.next_cursor`) |
| `GET /api/:name/verified_followers` | a user's verified followers (~200/page; paginate with `?cursor=` + `pagination.next_cursor`) |
| `GET /api/:name/mentions` | tweets that mention a user (paginate with `?cursor=` + `pagination.next_cursor`) |
| `GET /api/:name/media` | a user's tweets that contain media (~20/page; paginate with `?cursor=` + `pagination.next_cursor`) |
| `GET /api/:name/tweets_complete` | a user's recent tweets and replies with self-threads expanded (~40/page; dedupe by tweet `id` across pages; paginate with `?cursor=` + `pagination.next_cursor`) |
| `GET /api/:name/affiliates` | accounts affiliated with a user (e.g. an org's members) |
| `GET /api/:name/relations?name=...` | whether `:name` follows `name` (plus block/mute/DM flags) |
| `GET /api/:name/about` | account info (join date, location, verification) |
| `GET /api/:name/info` | profile info (id, bio, location, follower/following counts, verification) |
| `GET /api/:name/status` | account status: `alive`, `suspended`, `not_found`, or `unavailable` (always 200; `id` set when alive) |
| `GET /api/:name/pinned` | the user's pinned tweet |
| `GET /api/tweets/:id` | a single tweet, without its conversation |
| `GET /api/:name/tweets/:id` | a tweet + conversation (before/after/replies) |
| `GET /api/:name/tweets/:id/history` | a tweet's edit history (`latest` + prior versions) |
| `GET /api/tweets/:id/replies` | replies to a tweet (paginate with `?cursor=` + `pagination.next_cursor`) |
| `GET /api/tweets/:id/retweeters` | accounts that retweeted a tweet (paginate with `?cursor=` + `pagination.next_cursor`) |
| `GET /api/tweets/:id/thread` | the author's full self-thread from any tweet in it, root first (one response; `complete` is false if only partially retrieved) |
| `GET /api/search?q=...` | tweet search (or user search for `@`/`f:user` queries) |
| `GET /api/search/users?q=...` | user search (paginate with `?cursor=` + `pagination.next_cursor`) |
| `GET /api/search/tweets?q=...` | tweet search (optional `product`; paginate with `?cursor=` + `pagination.next_cursor`) |
| `GET /api/hashtag/:tag` | tweet search for a hashtag (same as `/api/search?q=%23tag`) |
| `GET /api/lists/:id/members` | members of a List by id (paginate with `?cursor=` + `pagination.next_cursor`) |
| `GET /api/trends?country=...` | trending topics for a location (worldwide by default; or `?woeid=...`) |
| `GET /api/trends/locations` | every location trends are available for (worldwide, countries, cities), with its `woeid` |
| `GET /api/tools/get_user_id?username=...` | `{ "id": ... }` for an @handle |
| `GET /api/tools/get_user_ids?usernames=...` | map of `@handle` → id (up to 20; failed lookups are `null`) |
| `GET /api/tools/get_username?user_id=...` | `{ "username": ... }` for a numeric user id |
| `GET /api/tools/get_usernames?user_ids=...` | map of user id → `@username` (up to 20; failed lookups are `null`) |
| `GET /api/spaces/:id` | a Space's metadata (title, host, state, listener counts, downloadable) |
| `GET /api/communities/:id` | a Community's metadata (name, description, member/moderator counts, rules, topic, admin, creator) |
| `GET /api/broadcasts/:id` | a broadcast |
| `GET /api/articles/:id` | an X article by wrapper tweet id (public) |
| `GET /api/article-entities/:article_id?auth_token=...` | an X article by entity rest_id (owner-only) |

`:name` is an X handle without `@`. Paginate any endpoint by passing the
response's `pagination.next_cursor` back as `?cursor=...` until
`pagination.has_more` is false. (The conversation endpoint,
`GET /api/:name/tweets/:id`, is the one exception — see its row above; it
carries its own nested `before`/`after`/`replies` cursors instead of a single
root-level `pagination`.)

## Response format

Endpoints wrap data as `{ "code": 0, "data": ..., "pagination": ... }` on
success (`pagination` present whenever the endpoint paginates) and
`{ "code": -1, "error": ..., "error_type": ... }` on error. Examples:

```bash
# profile
curl -s https://readx.sh/api/jack/profile | jq '.data.user | {username, followers, verifiedType}'

# followers (first page) + next cursor
curl -s https://readx.sh/api/jack/followers | jq '{count: .data.total_count, has_more: .pagination.has_more, next: .pagination.next_cursor, users: [.data.users[] | {username, followers}]}'

# following (first page)
curl -s https://readx.sh/api/jack/following | jq '{count: .data.total_count, has_more: .pagination.has_more, users: [.data.users[] | .username]}'

# timeline (first page) + next cursor
curl -s https://readx.sh/api/jack | jq '{next: .pagination.next_cursor, tweets: [.data.timeline[] | {id, text}]}'

# a tweet + its conversation
curl -s https://readx.sh/api/jack/tweets/20 | jq '.data.tweet | {text, time, stats}'

# just the tweet (no conversation)
curl -s https://readx.sh/api/tweets/20 | jq '.data | {text, time, stats}'

# a tweet's edit history
curl -s https://readx.sh/api/jack/tweets/20/history | jq '{latest: .data.latest.text, versions: (.data.history | length)}'

# search
curl -s "https://readx.sh/api/search?q=typescript" | jq '.data.timeline | length'

# profile info
curl -s https://readx.sh/api/jack/info | jq '.data | {username, followers, verifiedType}'

# trending topics (worldwide)
curl -s https://readx.sh/api/trends | jq '.data.trends[] | {rank, name, tweet_volume}'
```

A `Tweet` includes `id`, `text`, `time` (unix), `user`, `stats`
(replies/retweets/likes/quotes/views), and `media` (photos/videos/gifs);
retweets/quotes/polls/cards are nested when present. A `User` includes
`username`, `fullname`, `bio`, `followers`, `following`, `verifiedType`
(`"None"`, `"Blue"`, `"Business"`, or `"Government"`), `joinDate`, etc.
Endpoints that return tweets or users use these same two shapes, except
`GET /api/articles/:id`, whose `author` object uses a different field set.

## A2A agent card

ReadX also publishes an [A2A](https://a2a-protocol.org) `AgentCard` at
[`/.well-known/agent-card.json`](https://readx.sh/.well-known/agent-card.json),
listing every capability above as a named skill. An A2A-aware orchestrator can
invoke it directly instead of calling `/api/*` paths itself:

```bash
curl -s -X POST https://readx.sh/api/a2a/v1/message:send \
  -H 'Content-Type: application/json' \
  -d '{"message":{"role":"user","parts":[{"kind":"data","data":{"skill":"profile-lookup","input":{"username":"jack"}}}]}}'
```

Returns a `Task` with `status.state` `"completed"` (result in
`artifacts[0].parts[0].data`) or `"failed"` (reason in `status.message`).
Re-fetch a past task with `GET /api/a2a/v1/tasks/{id}` (retained 10 minutes).
Each `message:send` call is priced the same as any other endpoint.

## Notes

- Read-only mirror of X's public data; no posting/auth.
- Stable entities (profiles, broadcasts) are cached; timelines, search,
  list members, and conversations are always fresh.
- Errors surface as the normal envelope, e.g. `404 not_found` for a missing
  user/tweet, `429 rate_limited`, `402` when payment is required.
