with one click
x-research
// Read X (Twitter) via xurl — search posts, fetch threads, read profiles, and read long-form articles
// Read X (Twitter) via xurl — search posts, fetch threads, read profiles, and read long-form articles
Multi-source research synthesis — aggregate and compare 3+ sources or any source >5KB using sub-agent dispatch and SharedState
Find your way home — register with the yoyo family, introduce yourself, and participate in family discussions
Systematically find blind spots in code, architecture, APIs, and deployment — structured critique that catches what familiarity hides
RLM-style large-codebase comprehension — build a mental map of any codebase by dispatching sub-agents to explore regions without bloating main context
Interact with the community through GitHub Discussions — reply, share, learn
Evaluate readiness and publish to crates.io
| name | x-research |
| description | Read X (Twitter) via xurl — search posts, fetch threads, read profiles, and read long-form articles |
| tools | ["bash","read_file"] |
| origin | yoyo |
| status | active |
| score | 0.24 |
| uses | 3 |
| wins | 3 |
| last_used | 2026-05-03T14:37:44Z |
| last_evolved | null |
| parent_pattern_key | null |
| keywords | ["xurl","twitter","x.com","tweet","thread","x-research"] |
Read-only access to X (Twitter) through xurl. Use this when you need to know what people are saying on X about a topic, read a specific thread, check someone's recent posts, or read long-form X Articles.
research skill instead (curl + DuckDuckGo)Two auth modes are supported. Detect which one applies first, then use the x_get helper for every request — never call xurl GET or curl directly inline, or CI mode will silently fail (xurl has no ~/.xurl on a runner).
# Auth mode detection — run this once at the top of the session:
if [ -n "$X_BEARER_TOKEN" ]; then
AUTH_MODE=ci
elif command -v xurl &>/dev/null && [ -d "$HOME/.xurl" ]; then
AUTH_MODE=local
else
have_xurl=$(command -v xurl >/dev/null 2>&1 && echo yes || echo no)
have_xurl_dir=$([ -d "$HOME/.xurl" ] && echo yes || echo no)
have_token=$([ -n "$X_BEARER_TOKEN" ] && echo yes || echo no)
echo "x-research: auth not configured; skill unavailable this session" >&2
echo " X_BEARER_TOKEN set: $have_token" >&2
echo " xurl on PATH: $have_xurl" >&2
echo " ~/.xurl exists: $have_xurl_dir" >&2
exit 1
fi
CI mode (AUTH_MODE=ci) — $X_BEARER_TOKEN is set (provisioned by the CI workflow as a repo secret). Calls go through curl with an Authorization: Bearer header. Tokens come from the X developer portal as App-only Bearer tokens, which are read-only (cannot post, like, DM, or act as a user). Don't paste a user OAuth2 access token into this secret — x_get won't validate token type, but a user token would expose write capability the skill is not designed to use.
Local mode (AUTH_MODE=local) — xurl is on PATH and ~/.xurl/ exists from a prior xurl auth oauth2 run. xurl handles auth from its credential store.
Setup instructions for the human if both checks fail:
Local: install xurl (cargo install xurl, see https://github.com/deepfates/xurl), then run xurl auth oauth2 and follow the prompts.
CI: generate an App-only Bearer token at the X developer portal → add to repo Settings → Secrets and variables → Actions as X_BEARER_TOKEN.
If neither auth mode is available, stop. Don't retry. This is not a transient failure.
x_get is the only request entry point — checks HTTP status, surfaces auth/rate-limit failures explicitly, and exits non-zero on error so primitives don't silently parse error JSON as data:
x_get() {
local path="$1"
local body http_code
if [ "$AUTH_MODE" = "ci" ]; then
# -w writes the HTTP status to stdout after the body; split with tail -n1.
local raw
raw=$(curl -sS -w $'\n%{http_code}' \
-H "Authorization: Bearer $X_BEARER_TOKEN" \
"https://api.x.com${path}") || return $?
http_code=$(printf '%s' "$raw" | tail -n1)
body=$(printf '%s' "$raw" | sed '$d')
else
body=$(xurl GET "$path") || return $?
# xurl exits non-zero on HTTP errors; assume 200 if we got here.
http_code=200
fi
case "$http_code" in
2*) printf '%s' "$body" ;;
401|403) echo "x-research: HTTP $http_code on $path — token rejected (regenerate the auth)" >&2; return 2 ;;
429) echo "x-research: HTTP 429 on $path — rate limited" >&2; return 3 ;;
*) echo "x-research: HTTP $http_code on $path — unexpected; body: $body" >&2; return 4 ;;
esac
}
Never echo or log $X_BEARER_TOKEN. Treat it like any other secret. Don't add set -x to scripts that run x_get — bash trace expands the -H arg verbatim.
# URL-encode the query (spaces → %20, # → %23, etc.)
QUERY=$(python3 -c "import urllib.parse; print(urllib.parse.quote('your search query'))")
x_get "/2/tweets/search/recent?query=${QUERY}&max_results=10&tweet.fields=created_at,author_id,public_metrics,text"
Cost: 1 request per call.
What to show: For each tweet — text, author_id, created_at, and engagement metrics (retweets, likes, replies).
Tips:
max_results at 10 unless you specifically need more (max 100).from:username, to:username, #hashtag, -is:retweet for filtering.Given a tweet URL or ID, reconstruct the full conversation thread.
Step 1: Fetch the root tweet and its conversation_id:
TWEET_ID="1234567890"
x_get "/2/tweets/${TWEET_ID}?tweet.fields=conversation_id,author_id,created_at,text,public_metrics"
Step 2: Search for all replies in that conversation:
CONV_ID="..." # from step 1 response
x_get "/2/tweets/search/recent?query=conversation_id:${CONV_ID}&max_results=50&tweet.fields=created_at,author_id,text,in_reply_to_user_id"
Cost: 2 requests per call.
What to show: Reconstruct chronological order by created_at. Show the original tweet first, then replies in time order. Include author and text for each.
Limitation: The search endpoint only covers the last 7 days. Older threads may be incomplete.
Given a username, fetch their bio and recent tweets.
Step 1: Look up the user:
USERNAME="elonmusk"
x_get "/2/users/by/username/${USERNAME}?user.fields=description,public_metrics,created_at"
Step 2: Fetch their recent tweets:
USER_ID="..." # from step 1 response
x_get "/2/users/${USER_ID}/tweets?max_results=10&tweet.fields=created_at,public_metrics,text"
Cost: 2 requests per call.
What to show: Bio, follower/following counts, then their 10 most recent tweets with dates and engagement.
X Articles are long-form posts. Given an article URL or the tweet ID that contains it:
Try the expanded tweet fields first:
TWEET_ID="1234567890"
x_get "/2/tweets/${TWEET_ID}?tweet.fields=note_tweet,created_at,author_id,text&expansions=author_id"
The note_tweet field contains expanded text for long-form content (tweets > 280 chars).
If that doesn't return full article content, fall back to fetching the page directly:
curl -sL "https://x.com/i/article/${TWEET_ID}" | sed 's/<[^>]*>//g' | head -200
Cost: 1–2 requests per call.
Note: X Articles API support is evolving. The note_tweet field may not expose full article text for all article types. If you discover a better approach at runtime, use it and note what worked for future reference.
Every API call costs money and counts toward rate limits. Cache aggressively.
Cache directory: .yoyo/x-research-cache/ (gitignored)
TTL by primitive:
| Primitive | TTL |
|---|---|
| search | 15 minutes |
| thread | 1 hour |
| profile | 1 hour |
| article | 1 hour |
Cache key: SHA256 hash of the full API URL path (including query params).
Implementation:
CACHE_DIR=".yoyo/x-research-cache"
mkdir -p "$CACHE_DIR"
API_PATH="/2/tweets/search/recent?query=..."
CACHE_KEY=$(echo -n "$API_PATH" | sha256sum | cut -d' ' -f1)
CACHE_FILE="$CACHE_DIR/$CACHE_KEY.json"
TTL_SECONDS=900 # 15 min for search
# Check cache
if [ -f "$CACHE_FILE" ]; then
AGE=$(( $(date +%s) - $(stat -c %Y "$CACHE_FILE" 2>/dev/null || stat -f %m "$CACHE_FILE") ))
if [ "$AGE" -lt "$TTL_SECONDS" ]; then
cat "$CACHE_FILE"
# Cache hit — skip API call
exit 0
fi
fi
# Cache miss — make the request via x_get (returns non-zero on HTTP error,
# so we don't cache 401/429/5xx error bodies).
if ! RESULT=$(x_get "$API_PATH"); then
exit $?
fi
echo "$RESULT" > "$CACHE_FILE"
echo "$RESULT"
Bypass: When freshness matters, skip the cache check. Use this sparingly — most reads don't need real-time data.
Every xurl call is a real API request that may cost money (X API is pay-per-use on higher tiers).
| Primitive | Requests |
|---|---|
| search | 1 |
| thread | 2 |
| profile | 2 |
| article | 1–2 |
Before every call:
| Failure | Response |
|---|---|
Neither auth mode available (no xurl+~/.xurl AND no $X_BEARER_TOKEN) | Print setup instructions for both modes, stop |
| Auth rejected (HTTP 401) | Local: tell user to re-run xurl auth oauth2. CI: tell user the X_BEARER_TOKEN secret needs regeneration at developer.x.com. Stop |
| Rate limited (HTTP 429) | Wait 60 seconds, retry once. If still 429, give up and report the limit |
| Empty results | Report "no results found for [query]". Don't retry with a broader query |
| Network timeout | Retry once after 2 seconds. If it fails again, give up |
| Malformed JSON response | Report the raw output and stop. Don't try to parse broken data |