# Using TwittrAPI from an AI agent

> How an AI agent gets a TwittrAPI key, connects through the MCP server or plain HTTP, keeps responses small with the simple format, stays inside a budget and handles errors and retries.

HTML page: https://www.twittrapi.com/docs/for-agents

This page is for AI agents (coding agents, browsing agents, MCP clients) and for the people who set them up. Everything here is also available as plain text for machines:

| What | URL |
| --- | --- |
| All docs and every endpoint in one Markdown file | [/llms-full.txt](https://www.twittrapi.com/llms-full.txt) |
| OpenAPI 3.1 spec (also [YAML](https://www.twittrapi.com/openapi.yaml)) | [/openapi.json](https://www.twittrapi.com/openapi.json) |
| Prices, rate limits and payment methods as JSON | [/pricing.json](https://www.twittrapi.com/pricing.json) |
| Short index of the site | [/llms.txt](https://www.twittrapi.com/llms.txt) |
| Any docs page as Markdown | the page URL plus `.md`, e.g. [/docs/reference/search.md](https://www.twittrapi.com/docs/reference/search.md), or `Accept: text/markdown` |

## Getting a key

There are three ways. The first always works; the other two let an agent get access without the website sign-up.

1. **A person gives the agent a key.** A person signs up on the [get started page](https://www.twittrapi.com/start). New accounts get $0.10 of test credit, valid for 14 days, at 3 requests per second. They copy the API key from the dashboard (or create a separate [agent key](https://www.twittrapi.com/docs/api-keys) with its own budget) and give it to the agent, normally as the environment variable `TWITTRAPI_API_KEY`. Do not paste keys into prompts, logs or files that are committed.
2. **The agent asks, a person approves** ([agent sign-up](#agent-sign-up)). Available.
3. **The agent buys access with crypto**, no person and no third party involved: it gets its own deposit address, pays USDC, USDT or LTC and picks up its key ([buy access with crypto](#buy-access-with-crypto-no-human)). Available.

The agent sends the key in the `X-API-KEY` header on every request to `https://api.twittrapi.com`.

A RapidAPI key works too: the same paths on `twitter-aio.p.rapidapi.com` with the `x-rapidapi-key` and `x-rapidapi-host` headers. See [Authentication](https://www.twittrapi.com/docs/helpful-infos/authentication).

Pay per request without any account, via JojAPI: search only, $0.01 per request. JojAPI offers this for AI agents on `GET /search/{searchTerm}`; see [JojAPI](https://jojapi.com/hub/api/twitter-x-aio).

The "Try it" box on the homepage is for people: it is protected by a captcha and only shows one profile lookup. Agents should call the API with a key instead.

## Agent sign-up

Available. The agent asks for a key in the name of a person; the person approves it by email and chooses a budget. It works like the device login of a TV app ([RFC 8628](https://www.rfc-editor.org/rfc/rfc8628)):

1. The agent calls `POST https://app.twittrapi.com/api/agent/signup` with the person's email, its own name and what it needs the key for.
2. We email the person a link (it works once) and a short code. The codes expire after 15 minutes.
3. The person signs in or creates an account, sees the agent's name, purpose and the country the request came from, chooses a budget (a monthly cap from their balance, or a fixed amount) and approves or denies.
4. The agent polls `POST https://app.twittrapi.com/api/agent/token` with its `device_code` every `interval` seconds and receives its own key **once**.

The key is an "agent" key under the person's account: billed to their balance, limited to the budget they chose, without account actions (`/client/*`, `/premium/*`). The person sees it under **API keys** and can pause or delete it at any time.

```bash
# 1. Ask for a key (the person gets an email)
curl -s https://app.twittrapi.com/api/agent/signup \
  -H 'Content-Type: application/json' \
  -d '{"email":"owner@example.com","agent_name":"Research agent","purpose":"Collect public posts about our launch"}' \
  | tee signup.json
# -> {"device_code":"…","user_code":"BCDF-GHJK","verification_uri":"https://app.twittrapi.com/agent",
#     "verification_uri_complete":"https://app.twittrapi.com/agent?code=BCDF-GHJK","expires_in":900,"interval":5}

# 2. Tell the person: "Approve me via the email from TwittrAPI, or enter code BCDF-GHJK at https://app.twittrapi.com/agent"

# 3. Poll until they decide
DEVICE_CODE=$(jq -r .device_code signup.json); INTERVAL=$(jq -r .interval signup.json)
while true; do
  sleep "$INTERVAL"
  RES=$(curl -s https://app.twittrapi.com/api/agent/token -d "device_code=$DEVICE_CODE")
  case "$(echo "$RES" | jq -r '.error // "ok"')" in
    ok) echo "$RES" | jq -r .api_key > ~/.twittrapi_key; break ;;
    authorization_pending) ;;
    slow_down) INTERVAL=$((INTERVAL + 5)) ;;
    *) echo "$RES"; exit 1 ;;   # access_denied, expired_token, invalid_grant
  esac
done
```

| Poll answer | What to do |
| --- | --- |
| `authorization_pending` (400) | Keep polling every `interval` seconds. |
| `slow_down` (400) | Add 5 seconds to the interval. |
| `access_denied` (400) | The person said no. Stop. |
| `expired_token` (400) | Too late. Start a new sign-up. |
| `invalid_grant` (400) | Unknown code, or the key was already picked up. Keys are handed out once. |
| 200 | `api_key` (store it securely), `budget`, `base_url`, `balance_url`. |

Sign-ups are limited per address and per email address. Disposable email addresses cannot approve agents.

## Buy access with crypto (no human)

Available. An agent that holds crypto can buy its own access without a person, an email or any third party. It asks for an amount ($5, $20, $50) and a chain, gets an anonymous account with **its own deposit address**, pays, and collects its API key once the payment is credited. It is the same deposit system customers use to top up; nobody else touches the money.

1. `POST https://app.twittrapi.com/api/agent/purchase` with `{"amount_usd": 5, "chain": "polygon", "token": "USDC"}`. The answer has `purchase_id`, `claim_secret` (shown **once**, store it), `address`, `chain`, `token`, `amount`, `expires_at` (24 hours) and `poll_url`.
2. Send the amount to `address` on that chain before `expires_at`.
3. Poll `POST https://app.twittrapi.com/api/agent/purchase/status` with `{"purchase_id": …, "claim_secret": …}` every 15 seconds: `pending` (with confirmation progress), then `credited` with the `api_key` **exactly once**; later polls only say `credited`. `expired` means nothing arrived in time.

| Chain (`chain`) | Tokens (`token`) | Minimum per deposit | Network fee |
| --- | --- | --- | --- |
| `polygon` (default) | `USDC` (default), `USDT` | $5 | none |
| `tron` | `USDT` | $50 (smaller amounts are refused) | $3, deducted from the credit |
| `ltc` | none, send LTC worth the amount | $5 | none |

```bash
# 1. Create the purchase (store the answer: claim_secret is shown once)
curl -s https://app.twittrapi.com/api/agent/purchase \
  -H 'Content-Type: application/json' \
  -d '{"amount_usd":5,"chain":"polygon","token":"USDC"}' | tee purchase.json
# -> {"purchase_id":"pur_…","claim_secret":"cs_…","address":"0x…","chain":"polygon","token":"USDC",
#     "amount":"5","expires_at":"…","poll_url":"https://app.twittrapi.com/api/agent/purchase/status",…}

# 2. Pay: send 5 USDC on Polygon to .address (with your own wallet)

# 3. Poll until credited, then keep the key
while true; do
  RES=$(curl -s https://app.twittrapi.com/api/agent/purchase/status -H 'Content-Type: application/json' \
    -d "$(jq '{purchase_id, claim_secret}' purchase.json)")
  case "$(echo "$RES" | jq -r .status)" in
    credited) echo "$RES" | jq -r .api_key > ~/.twittrapi_key; break ;;
    pending) sleep 15 ;;
    *) echo "$RES"; exit 1 ;;   # expired, or an error
  esac
done
```

```python
import json, time, urllib.request

APP = "https://app.twittrapi.com"

def post(path, body):
    req = urllib.request.Request(APP + path, data=json.dumps(body).encode(),
                                 headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(req) as r:
        return json.load(r)

p = post("/api/agent/purchase", {"amount_usd": 5, "chain": "polygon", "token": "USDC"})
# Store p["claim_secret"] now. Then pay p["amount"] p["token"] on p["chain"] to p["address"].
print(f'Send {p["amount"]} {p["token"]} on {p["network"]} to {p["address"]} before {p["expires_at"]}')

while True:
    s = post("/api/agent/purchase/status",
             {"purchase_id": p["purchase_id"], "claim_secret": p["claim_secret"]})
    if s["status"] == "credited":
        api_key = s["api_key"]  # shown once: store it securely
        break
    if s["status"] == "expired":
        raise SystemExit("expired: nothing arrived in time")
    time.sleep(15)
```

The account: 3 requests per second, at most $100 balance, no account actions (`/client/*`, `/premium/*`), the Standard price ($0.09 per 1,000 calls) and a review by us. To top it up, pay again to the same address: it stays with the account for good. For more, a person can sign up normally.

**What gets credited.** What arrives is credited, whatever the amount: an underpayment is credited as it is, an overpayment too, both minus the chain's network fee (Tron $3). A payment below the chain minimum waits until the address has received the minimum in total. A payment that would push the balance over $100 is not credited automatically; it is held for review (the status says `held_for_review`), write to support with the `purchase_id`. Payments are credited after the chain's confirmations; send only the token and chain you chose.

**Expiry.** An unpaid purchase expires after 24 hours and its account is switched off. The address is never given to anyone else: if a payment still arrives later, it is credited to that account and the status turns `credited`. Do not count on that, start a new purchase instead.

| Answer | Meaning |
| --- | --- |
| 201 | Purchase created: address, amount, `claim_secret` (once), `expires_at`. |
| 200 `pending` | Waiting for the payment, or confirming it: `payment.state` (`waiting`, `confirming`, `below_minimum`, `held_for_review`), `confirmations` of `confirmations_required`. |
| 200 `credited` | Paid: `api_key` the first time, then only the status. |
| 200 `expired` | Nothing arrived before `expires_at`. |
| 400 | Invalid amount, chain or token, or an amount below the chain minimum (Tron: $50). |
| 404 | Unknown `purchase_id` or wrong `claim_secret`. |
| 429 | Too many purchases from this address, or too many unpaid ones open. |
| 503 | Temporarily unavailable. Try again; nothing was reserved. |

## Connecting through MCP

The MCP server [twittrapi-mcp](https://github.com/twittrapi/twittrapi-mcp) gives Claude, Cursor and any other MCP client read-only tools for profiles, tweets, followers, search, lists, communities and trends. It needs Node.js 20 or newer. Every tool call is one billed API call.

Claude Code:

```bash
claude mcp add twittrapi --env TWITTRAPI_API_KEY=your-key -- npx -y twittrapi-mcp
```

Claude Desktop (`claude_desktop_config.json`) and Cursor (`~/.cursor/mcp.json` or `.cursor/mcp.json`):

```json
{
  "mcpServers": {
    "twittrapi": {
      "command": "npx",
      "args": ["-y", "twittrapi-mcp"],
      "env": { "TWITTRAPI_API_KEY": "your-key" }
    }
  }
}
```

Other clients: any client that can start a stdio server works with the command `npx -y twittrapi-mcp`. For a shared server, `npx -y twittrapi-mcp --http --port 3333` serves Streamable HTTP at `/mcp`; each request then carries the caller's own key in `X-API-KEY` (or `Authorization: Bearer`).

| Variable | Meaning |
| --- | --- |
| `TWITTRAPI_API_KEY` | Your direct key (sent as `X-API-KEY`) |
| `RAPIDAPI_KEY` | RapidAPI key, used when `TWITTRAPI_API_KEY` is not set |
| `TWITTRAPI_RESPONSE_FORMAT` | `simple` (default) or `raw` |

Tool output is capped by `max_chars` (default 50,000); paginated tools print `next_cursor` above the JSON so truncation never removes it. LangChain and LlamaIndex toolkits are listed under [SDKs and integrations](https://www.twittrapi.com/docs/integrations).

### Hosted MCP server

Agents whose app supports remote MCP servers can connect by URL instead of running the package: `https://mcp.twittrapi.com/mcp` (Streamable HTTP, same tools, simple format by default). Two ways to authenticate:

- **OAuth** for Claude.ai, Claude Desktop, ChatGPT, Claude Code and VS Code: the app registers itself, the person logs in to TwittrAPI and clicks **Allow access**. The agent never sees the API key; calls are billed to the person's balance.
- **API key header** for Cursor, scripts and any client that can set headers: `Authorization: Bearer YOUR_API_KEY` or `X-API-KEY: YOUR_API_KEY`.

```bash
# Claude Code, OAuth
claude mcp add --transport http twittrapi https://mcp.twittrapi.com/mcp
# Claude Code, API key
claude mcp add --transport http twittrapi https://mcp.twittrapi.com/mcp --header "Authorization: Bearer $TWITTRAPI_API_KEY"
```

Claude.ai: Settings > Connectors > Add custom connector with the URL. ChatGPT: developer mode, then create a connector with the URL and OAuth. Cursor: `"url"` plus `"headers": { "X-API-KEY": "${env:TWITTRAPI_API_KEY}" }` in `mcp.json`. VS Code: `"type": "http"` with the URL in `.vscode/mcp.json`. Discovery for MCP clients: `https://mcp.twittrapi.com/.well-known/oauth-protected-resource/mcp` (RFC 9728). Details, limits and how to disconnect an app: [Hosted MCP server](https://www.twittrapi.com/docs/hosted-mcp).

## Calling the API directly

Without MCP, any HTTP client works. Read endpoints are `GET` with path and query parameters:

```bash
curl 'https://api.twittrapi.com/search?searchItem=from%3AXDevelopers&category=Latest&format=simple' \
  --header 'X-API-KEY: YOUR_API_KEY'
```

The [OpenAPI spec](https://www.twittrapi.com/openapi.json) lists every endpoint with its parameters, limits and error responses; it can be loaded as a tool definition by agent frameworks that read OpenAPI. The [endpoint reference](https://www.twittrapi.com/docs/reference) has the same information per page.

## Keep responses small: the simple format

X's raw JSON is large and deeply nested. Add `format=simple` (or the header `X-Response-Format: simple`) and every read endpoint returns flat camelCase JSON with a fixed schema and the same envelope:

```json
{ "data": [ ... ], "nextCursor": "…" | null, "previousCursor": "…" | null, "hasNextPage": true | false }
```

That is about 60 % fewer input tokens, at the same price. Stop paginating when `hasNextPage` is `false`. The MCP server uses the simple format by default. Details and field lists: [Simple format](https://www.twittrapi.com/docs/simple-format).

## Budget control

The balance is prepaid, so an agent can never spend more than what was topped up. To keep tighter control:

- Top up only the amount the task should cost. Calls cost $0.09 per 1,000 at the Standard tier, less at Volume and Scale.
- Read the `X-Remaining-Balance` and `X-Price-Per-Call` headers on every billed response, and stop below your own threshold.
- `GET https://api.twittrapi.com/balance` returns the balance; it is free and has its own limit.
- When the balance is used up, calls return **402** and are not charged. Do not retry a 402; ask the person to top up.
- One call returns one page. Paginating a timeline with 1,000 items at about 20 per page is about 50 calls. Set a page limit before you loop.

Prices and tiers: [Pricing and billing](https://www.twittrapi.com/docs/billing) and [/pricing.json](https://www.twittrapi.com/pricing.json).

## Errors and retries

Use the status code, not the message text.

| Status | What the agent should do |
| --- | --- |
| 401, 403 | Stop. The key is missing, wrong, deactivated or not allowed for this endpoint. |
| 402 | Stop and ask for a top-up. Not charged. |
| 404 | The user, tweet or object does not exist (or is private/suspended). Do not retry. |
| 422 | A parameter is invalid (non-numeric ID, bad handle, unsupported search operator). Fix the input. |
| 400 | Retry once at most, then check the parameters. Refunded. |
| 408, 409, 500, 502, 503, 504 | Retry with exponential backoff (0.5 s, 1 s, 2 s, a few tries). Refunded. |
| 429 | Rate limit of the key. Back off and slow down; there is no `Retry-After` header. Not charged. |

Calls that fail with 400, 408, 409 or 5xx are refunded at once (`X-Refunded: true`). Empty timelines are not errors: X answers 200 with no entries, for example for likes, which X made private. Full table: [Errors](https://www.twittrapi.com/docs/helpful-infos/error-handling). Rate limits per tier: [Rate limits](https://www.twittrapi.com/docs/helpful-infos/rate-limits).

## Rules for agents

- The API is read-only for public data. [Account actions](https://www.twittrapi.com/docs/account-actions) (posting, liking, following) need the session cookie of the person's own X account and their own proxy; do not use them unless the person explicitly set that up.
- Respect X's rules and the [acceptable use policy](https://www.twittrapi.com/aup). Protected accounts and private data are not available.
- TwittrAPI is not affiliated with X Corp.
