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:retweetauto-appended; quotes carry the commentary).--with-retweetsto 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 —
--pagescaps 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:
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/tweetscuts 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'sentities.cashtagsfield 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". Nomaterial_keywordskey 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_tweetsexcludes 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, runprobe.py USERNAME N START_ISOwith 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": truein 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)