# X API

> Use when: monitoring curated X accounts, cashtags, finance tweet pulls. Paid API ($/post). NOT general search (/research).

- Skill: `markusstrasser/x-api` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add markusstrasser/x-api`
- Raw SKILL.md: https://api.skillmd.com/api/skills/markusstrasser/x-api/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Research & Search
- Author: markusstrasser (https://skillmd.com/u/markusstrasser)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/markusstrasser/x-api

---


# X API v2 Client

Generic, project-agnostic interface to the X (Twitter) API v2 pay-per-use
developer tier. Built for daily-batch monitoring of a curated account list.

**Authentication:** Bearer token from `X_API_BEARER_TOKEN` env var. Caller is
responsible for loading the token (e.g., from `.env.local`, `~/.env`, or
launchd plist EnvironmentVariables).

## Subcommands

### `probe USERNAME [MAX_RESULTS] [START_TIME_ISO]`

Smoke-test auth, pull recent tweets from a single account, dump JSON to
`.scratch/x_probe_<username>.json`. Reports cost.

```
uv run --project ~/Projects/skills python3 ~/Projects/skills/x-api/scripts/probe.py aleabitoreddit 10
uv run --project ~/Projects/skills python3 ~/Projects/skills/x-api/scripts/probe.py aleabitoreddit 100 2026-04-01T00:00:00Z
```

### `search.py query|verify|thread` — full-archive search (added 2026-08-20)

Promoted after being hand-rolled 4× across two research sessions. Wraps
`/2/tweets/search/all` with ledger discipline and the operational lessons baked in.

```
uv run --project ~/Projects/skills python3 ~/Projects/skills/x-api/scripts/search.py query '"greater male variability" lang:en' --out hits.jsonl --label mytopic
uv run --project ~/Projects/skills python3 ~/Projects/skills/x-api/scripts/search.py verify StuartJRitchie lakens cremieuxrecueil
uv run --project ~/Projects/skills python3 ~/Projects/skills/x-api/scripts/search.py thread 1234567890 --author whyvert
```

Rules it encodes (learned the expensive way):
- **verify handles BEFORE `from:` queries** — a wrong training-data handle reads as a
  silent zero, and squatters shadow real names (<100-follower flag printed).
- **Quotes in, pure RTs out by default** (`-is:retweet` auto-appended; quotes carry the
  commentary). `--with-retweets` to override.
- **A zero result means "not keyword-reachable in this phrasing," never "doesn't exist"**
  — search the discourse's own vocabulary, not your methods jargon.
- 402 = vendor credit balance (developer.x.com), NOT the local $100/mo ledger cap.
- Every returned post bills $0.005 — `--pages` caps spend deliberately.

### `pull --config FILE [--since-hours 24] [--max-pages 2] [--digest-out PATH] [--tracked-tickers-file PATH] [--themes-dir PATH]`

Read a JSON account-list, pull tweets since N hours ago, filter for cashtags
and material-claim keywords, emit a markdown digest with coverage delta.

Hard cap $100/month spend (refuses to run if MTD exceeds). Cost ledger is
wallet-scoped at `~/.local/state/x-api/cost_ledger.jsonl` (NOT CWD-scoped —
spend from every repo counts against the one monthly cap).

```
uv run --project ~/Projects/skills python3 ~/Projects/skills/x-api/scripts/pull.py \
    --config .claude/config/x_curated_accounts.json \
    --tracked-tickers-file <(ls analysis/entities/*.md | xargs -I {} basename {} .md) \
    --themes-dir analysis/themes \
    --digest-out .scratch/social_digest_$(date -u +%Y-%m-%d).md
```

## Library

`scripts/x_api.py` — `get_user`, `get_user_tweets`, `CostTally`, `log_cost`,
`month_to_date_usd`. Import from external scripts via:

```python
import sys
from pathlib import Path
sys.path.insert(0, str(Path.home() / "Projects/skills/x-api/scripts"))
from x_api import CostTally, get_user, get_user_tweets, log_cost, month_to_date_usd
```

## Pricing reference (2026-05-01)

| Resource | Price | Note |
|---|---|---|
| Post read | $0.005 | Each tweet returned, not per request |
| User lookup | $0.010 | Per `/users/by/username` or `/users/:id` |
| Owned reads | $0.001 | Reads of the auth'd app's own data only |
| Monthly post-read cap | 2,000,000 | Hard limit at PPU tier |

See `~/Projects/intel/.scratch/x_api_features_research.md` for full feature map
(Lists endpoint, mentions endpoint, context_annotations).

## Architecture notes

- **Lists endpoint refactor deferred** until curated list grows past ~5
  accounts. `/2/lists/:id/tweets` cuts request count ~10x for the same
  per-post cost; not worth the refactor for 1-3 accounts.
- **Server-side cashtag extraction** is enabled via `tweet.fields=entities`.
  Each tweet's `entities.cashtags` field returns X's own ticker tagging,
  more accurate than text regex (skips `$` in money figures and quoted text).
- **Material-claim regex** is a starting heuristic. Replace with LLM
  classification (Haiku/Flash) once enough volume justifies the cost.
- **Materiality is domain-configurable.** The finance/8-K keyword set is the
  default; pass an optional `"material_keywords": [...]` list in the config
  JSON to override it for a non-finance domain (e.g. AI-research account
  monitoring). Config-driven keywords match as a leading-`\b`-anchored
  PREFIX (not whole-word) — a stem like `"distill"` also catches
  "distillation"/"distilled". No `material_keywords` key in the config ⇒
  the finance default is unchanged (`pull.py::build_material_pattern`).
- **Zero tweets pulled for an active account is usually not a bug** — the
  default `get_user_tweets` excludes replies and retweets, and a short
  (24-48h) window can simply miss an account's last original post. Before
  assuming a resolution/pagination/API bug, run `probe.py USERNAME N
  START_ISO` with a wide window (e.g. 7d) and read the raw response: it
  prints the resolved user id/name/follower count (catches wrong-account
  resolution) and each tweet's exact timestamp (catches window-vs-cadence
  mismatches). For accounts whose signal is mostly in-thread commentary,
  set per-account `"include_replies": true` in the config — confirmed case
  2026-07-20: an account's only post in a 7-day window was a reply, silently
  excluded until this flag was added (`get_user_tweets(include_replies=)`).

## What this skill does NOT do

- Posting tweets, DMs, or any write actions
- Real-time streaming (Filtered Stream — defer until >50 accounts)
- Account discovery (curated list management is caller's responsibility)
- LLM claim extraction (text classification belongs in caller, model choice
  varies per project)
- Auto-updates to track-record memory files (caller decides when to write)

