SKILL.md explained: the Agent Skills format, X's own skill, and a Twitter (X) API skill you can install

By TwitterAPI.io Team•22 min read
Diagram of how a SKILL.md reaches an API. Left: the twitterapi-io/SKILL.md file with its frontmatter (name twitterapi-io, description, license MIT, metadata version 1.0) and body sections when to use, setup, cost, six reads, fields, pagination, errors, keep the context small. Middle: the agent (Claude Code .claude/skills/, Codex and Cursor .agents/skills/, OpenClaw skills/) loading about 100 tokens at startup and about 2,237 tokens when activated. Right: api.twitterapi.io with the X-API-Key header, the six paths /twitter/user/info, /twitter/user/last_tweets, /twitter/tweet/advanced_search, /twitter/user/mentions, /twitter/user/followers, /twitter/tweets, has_next_page and next_cursor, 15 credits per tweet. Footer: install with npx skills add https://twitterapi.io/skills/twitterapi-io/SKILL.md; measured on 70 calls, a raw 20-post page is about 32,044 tokens, trimmed about 2,181.
A skill is a folder the agent loads in stages: name and description first, the body when the task matches, and the API only when the body says how. Diagram; the footer numbers come from the study below.

If you searched for "skill md" you probably met the file in one of three places: a SKILL.md inside a Claude Code or Codex repository, the skill.md page X added to its developer docs under Agent Resources next to llms.txt and the MCP server, or a security write-up about skills that run shell commands. All three are the same thing: a small Markdown file, with a YAML header, that tells an AI agent what it can do and when to do it. The format has a name, Agent Skills, and a public specification at agentskills.io that Anthropic released as an open standard and that Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenClaw, Goose, Kiro and a few dozen other clients now read.

This page does three things. First it explains the format from the specification itself: the fields, the limits, the folder layout and the progressive-disclosure idea that makes skills cheap to keep installed. Second it opens X's own skill.md and lists what is actually in it, because the install command is one line but the file decides what your agent will do with your X developer account. Third it publishes a twitterapi.io SKILL.md, written to the spec, that gives an agent the six most common reads of public X data with an API key instead of OAuth, and shows how to install it in Claude Code, Codex, Cursor or OpenClaw.

Then we did what a skill page should do and measured the skill: we ran its six reads against 10 organization accounts (70 calls, collected in 2026) and recorded what an agent that follows the file actually pays, in context tokens before and after the trimming rule the file prescribes, in wall-clock per call, and in dollars at both the twitterapi.io and the official X API rate cards. Every number below comes from that run.

01 — Section

What a SKILL.md is: the Agent Skills format in one page

The specification is short, and worth reading in the original at agentskills.io/specification. A skill is a directory that contains, at minimum, a SKILL.md file. The file is YAML frontmatter followed by Markdown. The directory may also hold scripts/ (code the agent can run), references/ (documents the agent reads on demand) and assets/ (templates, images, data files), plus anything else you like.

FieldRequiredRule from the spec
nameyes1 to 64 characters; lowercase letters, digits and hyphens only; no leading, trailing or double hyphen; must match the parent directory name
descriptionyes1 to 1,024 characters; says what the skill does and when to use it; include the keywords an agent would match on
licensenoa license name or the name of a bundled license file
compatibilityno1 to 500 characters; only if the skill needs a particular product, system package or network access
metadatanoa map of string keys to string values (author, version, homepage); use distinctive key names
allowed-toolsnospace-separated tools pre-approved to run; marked experimental, support varies by agent

The body has no required structure. The spec recommends step-by-step instructions, examples of inputs and outputs, and common edge cases, and warns that the agent loads the entire file once it decides to activate the skill, so long material belongs in references/ files linked by relative path, one level deep.

Progressive disclosure is why skills scale. The spec describes three stages. At startup the agent loads only name and description of every installed skill, about 100 tokens each. When a task matches a description, it loads the full body, which the spec recommends keeping under 5,000 tokens and under 500 lines. Files in scripts/, references/ and assets/ are read only when the body sends the agent there. A machine can therefore keep fifty skills installed for the price of a short paragraph of context, and pay for a skill only when it uses it.

Who reads the format. The agentskills.io home page lists the clients that support it; the list includes Claude Code, Claude, ChatGPT and Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, OpenHands, Goose, Amp, Letta, Roo Code, Kiro, Factory, JetBrains Junie, Hermes Agent and OpenClaw. The standard was developed by Anthropic, released openly, and takes contributions on GitHub under agentskills/agentskills, which also hosts skills-ref, a reference validator: skills-ref validate ./my-skill checks the frontmatter and the naming rules.

Not to be confused with. AGENTS.md and CLAUDE.md are always-on project instructions, loaded every session; a SKILL.md is loaded on demand. llms.txt is an index of documentation for crawlers and agents, not instructions. An MCP server exposes tools over a protocol; a skill is plain text that may tell the agent to call such tools, or to run curl. The skills.md and skillsmd.dev sites you will see in the search results are registries and explainers built around the same file format.

02 — Section

Where skills live and how to install one

Each agent reads skills from its own directory; the skills CLI published by vercel-labs on GitHub knows the paths and copies a skill into them. Its README documents these locations for the four agents most readers of this page use (it supports many more).

AgentProject-level pathGlobal path
Claude Code./.claude/skills/<name>/SKILL.md~/.claude/skills/<name>/SKILL.md
Codex./.agents/skills/~/.agents/skills/
Cursor./.agents/skills/~/.cursor/skills/
OpenClawskills/ in the workspace~/.openclaw/skills/

What npx skills add accepts. Per the README: GitHub shorthand (owner/repo), a full GitHub, GitLab or Azure Repos URL, a URL to a sub-folder of a repository (.../tree/main/skills/<name>), a git SSH URL, a local path, and a direct download URL that points at a single valid SKILL.md file or at a .zip, .tar, .tar.gz or .tgz archive. Inside a repository it looks for SKILL.md files in the root, in skills/ and in agent-specific folders. -g installs globally instead of into the current project, -y skips the prompts, -a <agent> targets one agent, -s <skill> picks one skill from a repository with several, and npx skills find <query> searches a public index.

Claude Code specifics. The Claude Code documentation (code.claude.com/docs/en/skills) adds that the directory name becomes a slash command (~/.claude/skills/deploy/ gives /deploy), that Claude also invokes a skill automatically when a request matches its description, and that disable-model-invocation: true turns the automatic part off. Claude Code extends the standard with fields such as context: fork, agent, paths, argument-hint and dynamic shell injection; the docs state that only name, description, license, compatibility, metadata and allowed-tools travel to other products, so a skill meant to be portable should stay inside those six.

Manual install is a copy. A skill is files, so mkdir plus curl -o into the right directory is a complete installation; nothing registers it. Deleting the directory uninstalls it. The commands below show both routes for the twitterapi.io skill published on this page.

bash
# Route 1: the skills CLI (vercel-labs/skills). A direct URL to a single SKILL.md is a documented source.
npx skills add https://twitterapi.io/skills/twitterapi-io/SKILL.md            # into the current project
npx skills add https://twitterapi.io/skills/twitterapi-io/SKILL.md -g -y      # globally, no prompts
npx skills add https://twitterapi.io/skills/twitterapi-io/SKILL.md -a claude-code -a codex

# Route 2: copy the file yourself (name must equal the folder name: twitterapi-io)
mkdir -p ~/.claude/skills/twitterapi-io && curl -fsSL https://twitterapi.io/skills/twitterapi-io/SKILL.md -o ~/.claude/skills/twitterapi-io/SKILL.md
mkdir -p ~/.agents/skills/twitterapi-io && curl -fsSL https://twitterapi.io/skills/twitterapi-io/SKILL.md -o ~/.agents/skills/twitterapi-io/SKILL.md   # Codex, Cursor
mkdir -p ~/.openclaw/skills/twitterapi-io && curl -fsSL https://twitterapi.io/skills/twitterapi-io/SKILL.md -o ~/.openclaw/skills/twitterapi-io/SKILL.md

# The key the skill expects (dashboard: https://twitterapi.io/dashboard)
export TWITTERAPI_IO_API_KEY=...

# Check the file validates against the spec (optional)
npx skills-ref validate ~/.claude/skills/twitterapi-io   # or: skills-ref validate <dir> if installed from agentskills/agentskills

# X's own skill, for comparison
npx skills add https://docs.x.com
03 — Section

X's own skill.md: what docs.x.com actually ships

X added a skill.md page to its developer documentation under Agent Resources, next to llms.txt and its MCP server. The page describes the file as "a structured capability summary that describes what AI agents can do with the X API", following the agentskills.io specification, and gives one install command: npx skills add https://docs.x.com. The CLI finds the file through X's discovery endpoint, docs.x.com/.well-known/agent-skills/index.json, which lists a single skill named x with the file at /.well-known/agent-skills/x/skill.md and a SHA-256 digest. The same text is served at docs.x.com/skill.md. We fetched it this session: 235 lines, 9,926 bytes, roughly 2,482 tokens once an agent loads it, comfortably inside the spec's 5,000-token recommendation.

Frontmatter. name: x; a description that starts "Use when building integrations with X (formerly Twitter), accessing posts, users, trends, and real-time data; managing authentication and API requests; handling pagination and rate limits; or troubleshooting API errors and response codes"; and metadata with mintlify-proj: x and version: "1.0". No license, compatibility or allowed-tools.

Body. Nine sections: Product Summary, When to Use, Quick Reference, Decision Guidance, Workflow, Common Gotchas, Verification Checklist and Resources. The Quick Reference is where the substance is: an authentication table (bearer token for app-only public reads, OAuth 1.0a user context, OAuth 2.0 authorization code with scopes, Basic auth for Enterprise), eleven endpoints (GET /2/users/by/username/{username}, GET /2/users/{id}, GET /2/tweets/search/recent, GET /2/tweets/{id}, GET /2/users/{id}/tweets, POST /2/tweets, DELETE /2/tweets/{id}, POST /2/users/{id}/likes, POST /2/users/{id}/following, GET /2/tweets/search/stream, POST /2/tweets/search/stream/rules), the tweet.fields and expansions parameters, max_results plus pagination_token with meta.next_token, and the three x-rate-limit-* headers.

What it tells the agent to do. Create an app at console.x.com, accept the developer agreement, store the bearer token or OAuth credentials in environment variables, always add fields and expansions (the file's first gotcha is that posts come back with only id and text by default), stop paginating when next_token is absent, parse the errors array rather than a single error, back off on 429 using x-rate-limit-reset, reconnect streams, and URL-encode spaces as %20.

What it does not contain. Prices. The Product Summary says only that "the API uses pay-per-usage pricing with no commitments"; the numbers live on docs.x.com/x-api/getting-started/pricing: $0.005 per post read, $0.010 per user read, $0.010 per follower or following read, $0.015 per post created ($0.200 with a URL), capped at 3 million post reads per monthly billing cycle with Enterprise above that. An agent following X's skill will not know that a 100-post search page costs $0.50 unless you tell it, which is one of the reasons the twitterapi.io skill below puts its prices in the body.

bash
# How the CLI finds X's skill: the discovery document
curl -s https://docs.x.com/.well-known/agent-skills/index.json
# {"$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
#  "skills": [{"name": "x", "type": "skill-md",
#              "description": "Use when building integrations with X (formerly Twitter), accessing",
#              "url": "/.well-known/agent-skills/x/skill.md", "digest": "sha256:6ea7a081..."}]}

# The file itself (235 lines when we fetched it)
curl -s https://docs.x.com/skill.md | head -20

# Install into the current project's agents
npx skills add https://docs.x.com
04 — Section

The twitterapi.io SKILL.md, in full

Below is the complete file we publish at twitterapi.io/skills/twitterapi-io/SKILL.md (177 lines, 8,948 bytes, about 2,237 tokens). It follows the specification literally: name equals the folder name twitterapi-io, the description says both what and when, and the only optional fields used are the four that travel to every client (license, compatibility, metadata). Everything an agent needs to make a correct, cheap call is in the body; there is no references/ folder yet, because the body stays under half the spec's line budget.

Design choices, and why. The description names the concrete tasks (profile, recent posts, search, mentions, followers, posts by ID) so that an agent's matcher fires on the words a user would use. The cost section comes before the endpoint table because an agent that loops over pages should know the price of a page before it starts; the spec's own description advice ("include specific keywords") applies to cost too. The six reads are a table of path, required parameters and where the items sit in the response, because that last column is where generic LLM knowledge fails: last_tweets nests under data.tweets, search and mentions return a top-level tweets, followers return followers with snake_case fields. The error section leads with the shape that bites: HTTP 200 with status: "error" and a msg. And the trimming section exists because of the numbers in the next part of this page.

On writes. The file mentions the write actions only in a final section that starts "only on explicit request", and carries the caveats verbatim: writes run through a logged-in session (/twitter/user_login_v2, $0.10, with a proxy you supply), each action is $0.002, every action acts as that X account, which X can rate-limit, lock or suspend, and calls are billed per HTTP 200 response. An agent that reads only the first screen of the skill will never see a write path, which is the right default.

markdown
---
name: twitterapi-io
description: Read public X (Twitter) data through the twitterapi.io REST API with a plain API key, no X developer account or OAuth. Use when a task needs a user's profile, an account's recent posts, a keyword or from:user search, mentions of an account, a followers list, or specific posts by ID, and when the agent must keep the response small and know what each call costs.
license: MIT
compatibility: Needs network access to https://api.twitterapi.io and the TWITTERAPI_IO_API_KEY environment variable. Examples use curl and Python 3 with requests.
metadata:
  author: twitterapi.io
  version: "1.0"
  homepage: https://twitterapi.io/skills/twitterapi-io/SKILL.md
  docs: https://docs.twitterapi.io
---

# twitterapi.io: X (Twitter) data for agents

## When to use

- The user asks what an X account posted, who someone is on X, who follows an account,
  what people are saying about a topic, or wants specific posts by ID.
- You need the data as JSON in one HTTP call, billed per item returned, with no OAuth flow.

Do not use this skill for the official X API v2 (`api.x.com/2/...`): the paths, auth and
response fields below are twitterapi.io's and are different. Do not use it to post, like,
follow or DM unless the user explicitly asks for a write and accepts the caveats at the end.

## Setup

1. Get an API key at https://twitterapi.io/dashboard (sign-up gives up to $1 of free credits to new
   accounts that pass the automatic bot check, one per person or network, expiring after 31 days).
2. Put it in the environment: `export TWITTERAPI_IO_API_KEY=...`. Never print or commit it.
3. Every request is a GET to the base URL `https://api.twitterapi.io` plus one of the paths below,
   with the header `X-API-Key: $TWITTERAPI_IO_API_KEY`.

Rate limits: 0.2 requests per second before the account's first payment, 20 per second once
paid (50 on the Scale plan, 500 on Business). Space calls accordingly; retry on HTTP 429 after
a short pause.

## Cost (check before looping)

- $1 = 100,000 credits. Pay only for the data returned.
- Tweet endpoints (search, timelines, mentions, tweets by ID): 15 credits per tweet returned,
  $0.15 per 1,000 tweets. Minimum 15 credits ($0.00015) per request, so an empty search costs
  $0.00015 and a full 20-tweet page costs $0.003.
- Profile (`/twitter/user/info`): 18 credits per user, $0.18 per 1,000 profiles.
- Followers / followings: 1 to 3 credits per user returned, in per-page tiers; a 200-row page
  is between 200 and 600 credits ($0.002 to $0.006).
- Tell the user the estimated cost before fetching more than 10 pages of anything.

## The six reads

All return JSON. Tweet objects look the same everywhere. Error text is always in `msg`.

| Need | Path and required params | Where the items are |
| --- | --- | --- |
| Profile | `/twitter/user/info?userName=<handle>` | `data` (one object) |
| Recent posts | `/twitter/user/last_tweets?userName=<handle>` (`includeReplies=true` to add replies) | `data.tweets[]`, 20 per page, newest first |
| Search | `/twitter/tweet/advanced_search?query=<q>&queryType=Latest` (`Top` for ranked) | `tweets[]`, up to 20 per page |
| Mentions | `/twitter/user/mentions?userName=<handle>` | `tweets[]` |
| Followers | `/twitter/user/followers?userName=<handle>` (`/followings` for who they follow) | `followers[]`, about 200 per page |
| Posts by ID | `/twitter/tweets?tweet_ids=<id1,id2,...>` | `tweets[]` |

Search `query` uses X search operators: `from:handle`, `to:handle`, `"exact phrase"`,
`lang:en`, `-filter:replies`, `min_faves:10`, `since_time:<unix>`, `until_time:<unix>`.
Combine with `OR` and parentheses.

### Tweet fields (camelCase)

`id`, `url`, `text`, `createdAt` (format `Thu Oct 16 12:34:56 +0000 2026`, not ISO 8601),
`lang`, `likeCount`, `retweetCount`, `replyCount`, `quoteCount`, `viewCount`, `bookmarkCount`,
`conversationId`, `isReply`, `inReplyToId`, `inReplyToUsername`, `entities` (hashtags, urls,
user_mentions), `extendedEntities.media[]` (type photo, video or animated_gif), `quoted_tweet`,
`retweeted_tweet`, and `author` with `id`, `userName`, `name`, `followers`, `following`,
`isBlueVerified`, `isVerified`, `createdAt`. There is no `public_metrics` object.

### Profile fields (`/twitter/user/info` -> `data`)

`id`, `name`, `userName`, `description`, `location`, `url`, `followers`, `following`,
`statusesCount`, `mediaCount`, `favouritesCount`, `createdAt`, `isBlueVerified`, `isVerified`,
`canDm`, `isAutomated`, `protected`.

### Follower rows (`/twitter/user/followers` -> `followers[]`, snake_case)

`id`, `screen_name`, `name`, `description`, `location`, `created_at`, `followers_count`,
`following_count`, `statuses_count`, `favourites_count`, `media_tweets_count`, `protected`,
`verified`, `can_dm`, `profile_image_url_https`, `url`.

## Pagination

Every list response carries `has_next_page` (boolean) and `next_cursor` (string). To get the
next page, repeat the same request with `cursor=<next_cursor>`. Stop when `has_next_page` is
false or the page is empty. Each page is billed separately.

## Errors

- HTTP 200 with `"status": "error"` and a `msg` string is the normal error shape. An unknown
  handle on `/twitter/user/info` returns 200, `status: "error"`, `msg: "user not found"`,
  `data: null` (never 404). Check `status` before reading `data`.
- HTTP 401: missing or wrong `X-API-Key`. HTTP 402 or an `msg` about credits: the account is
  out of credits. HTTP 429: over the rate limit; wait and retry.
- A protected account returns an empty `tweets` list rather than an error.

## Keep the context small

Raw responses are large because each tweet repeats its full `author` object, `entities` and
media. Before reasoning over results, keep only what the task needs. The default subset is
`id`, `createdAt`, `text`, `url`, `author.userName`, `likeCount`, `retweetCount`,
`replyCount`, `viewCount`; for profiles `id`, `userName`, `name`, `description`, `followers`,
`following`, `statusesCount`, `createdAt`, `isBlueVerified`; for follower rows `id`,
`screen_name`, `name`, `followers_count`, `created_at`. Fetch one page, trim, then decide
whether another page is needed. Never paste a raw page into the conversation.

## Examples

Shell (one search page, trimmed with jq):

```bash
curl -s -G "https://api.twitterapi.io/twitter/tweet/advanced_search" \
  -H "X-API-Key: $TWITTERAPI_IO_API_KEY" \
  --data-urlencode 'query=from:NASA -filter:replies' --data-urlencode 'queryType=Latest' \
  | jq -c '.tweets[] | {id, createdAt, text, url, user: .author.userName, likeCount, retweetCount, replyCount, viewCount}'
```

Python (profile, two timeline pages, pagination, error check, trimming):

```python
import os, requests

BASE = "https://api.twitterapi.io"
H = {"X-API-Key": os.environ["TWITTERAPI_IO_API_KEY"]}
KEEP = ("id", "createdAt", "text", "url", "likeCount", "retweetCount", "replyCount", "viewCount")

def trim(t):
    d = {k: t.get(k) for k in KEEP}
    d["user"] = (t.get("author") or {}).get("userName")
    return d

r = requests.get(f"{BASE}/twitter/user/info", headers=H, params={"userName": "NASA"}, timeout=30)
r.raise_for_status()
body = r.json()
if body.get("status") == "error":
    raise SystemExit(body.get("msg"))            # e.g. "user not found"
profile = body["data"]
print(profile["userName"], profile["followers"], profile["statusesCount"])

posts, cursor = [], None
for _ in range(2):                                # 2 pages = up to 40 posts = $0.006
    params = {"userName": "NASA"}
    if cursor:
        params["cursor"] = cursor
    r = requests.get(f"{BASE}/twitter/user/last_tweets", headers=H, params=params, timeout=30)
    r.raise_for_status()
    body = r.json()
    if body.get("status") == "error":
        raise SystemExit(body.get("msg"))
    page = (body.get("data") or {}).get("tweets") or []
    posts.extend(trim(t) for t in page)
    if not body.get("has_next_page") or not body.get("next_cursor") or not page:
        break
    cursor = body["next_cursor"]
print(len(posts), "posts;", "credits:", 15 * len(posts))
```

## Writes (only on explicit request)

twitterapi.io also offers write actions, which run through an authenticated session: one
`/twitter/user_login_v2` login ($0.10, with a proxy the customer supplies) returns
`login_cookies`; each post, delete, like, retweet, follow, unfollow or DM is then $0.002
(`/twitter/create_tweet_v2`, `/twitter/follow_user_v2`, and so on). Every write acts as that
X account, which X can rate-limit, lock or suspend; keep a human pace, expect the session to
need a re-login, and note that calls are billed per HTTP 200 response. Reads need no X account.

## Reference

- API reference and OpenAPI: https://docs.twitterapi.io (machine-readable index:
  https://docs.twitterapi.io/llms.txt)
- Prices and plans: https://twitterapi.io/pricing
- Dashboard, key and usage: https://twitterapi.io/dashboard
- MCP server with the same reads as tools: https://twitterapi.io/twitter-mcp-server
05 — Section

Side by side: X's skill versus the twitterapi.io skill

Both files are valid Agent Skills and both are small enough to load whole. They differ in what the agent must have before the first call, in how much the agent is told, and in what each read costs. Prices are from the X API pay-per-usage page on docs.x.com and from twitterapi.io/pricing.

DimensionX skill (docs.x.com/skill.md)twitterapi.io skill
Account neededX developer app at console.x.com, developer agreement, payment card for pay-per-usagetwitterapi.io account; up to $1 of free credits for new accounts that pass the bot check (one per person or network, 31-day expiry), no card, no X account
Auth in the filebearer token (app-only), OAuth 1.0a, OAuth 2.0 with scopes, Basic for Enterpriseone header: X-API-Key
Size235 lines, about 2,482 tokens177 lines, about 2,237 tokens
Reads documented5 read endpoints plus streaming, with tweet.fields and expansions to request data6 reads with the full default field list; no field flags needed
Paginationmax_results, pagination_token, stop when meta.next_token is absentcursor, stop when has_next_page is false or next_cursor is empty
Errorserrors[] array with code and message; 429 with x-rate-limit-resetHTTP 200 with status: "error" and msg; 401 bad key; 429 over the limit
Rate limitsper-endpoint windows read from x-rate-limit-* headers0.2 requests/s before the first payment, 20 once paid, 50 on Scale, 500 on Business
Price of 1,000 posts read$5.00 (pay-per-usage, $0.005 per post)$0.15 (15 credits per tweet returned), about 33x less
Price of one profile$0.010$0.00018 (18 credits)
Price of 200 follower rows$2.00 ($0.010 per follower read)$0.002 to $0.006 (1 to 3 credits per row)
Prices inside the fileno, only "pay-per-usage pricing with no commitments"yes, per item, with the per-request minimum
WritesPOST /2/tweets $0.015 ($0.200 with a URL), likes and follows under OAuth user contextsession login $0.10 then $0.002 per action, acts as that account, human pace, re-login possible
Trimming guidancerequest fewer fields with tweet.fieldskeep 9 tweet fields, 9 profile fields, 5 follower fields; fetch, trim, then decide on the next page

Neither file replaces the other. If your agent must act as a specific X account with X's own guarantees, or needs the filtered stream, X's skill is the one to install and the OAuth table in it is accurate. If your agent needs to read public X data at scale, from any account, without an app review and at a price it can be told in advance, the twitterapi.io skill is the shorter path. Many setups will have both installed; the descriptions do not overlap enough to confuse a matcher, because X's starts with "building integrations with X" and ours with "read public X (Twitter) data through the twitterapi.io REST API with a plain API key".

06 — Section

What an agent following the skill pays in context

A skill is cheap to hold (2,237 tokens when active) but what it causes the agent to fetch is not. We ran each of the six reads against 10 organization accounts (@vercel, @github, @Docker, @CloudflareDev, @OpenAIDevs, @nvidia, @NASA, @stripe, @supabase, @AnthropicAI) through a rate-limited client: one profile, two timeline pages, one mentions page, one from:<account> search page, one followers page and one /twitter/tweets batch of the 20 IDs from the first timeline page, 70 calls in all, zero errors. For every response we recorded the compact-JSON size in bytes, divided by four as the usual token approximation, then applied the trimming rule from the skill (9 tweet fields, 9 profile fields, 5 follower fields) and measured again.

Read (items per call)Raw bytes, medianTokens raw, then trimmedTrimmed byWall-clock, median (p90)
/twitter/user/info (1 profile)1,329332, then 8374.5%1,108 ms (1,347)
/twitter/user/last_tweets (20 posts)128,17732,044, then 2,18193.4%2,400 ms (2,594)
/twitter/tweet/advanced_search (20 posts)109,00227,250, then 2,13892.6%2,218 ms (2,431)
/twitter/user/mentions (20 posts)60,11815,030, then 1,99187.6%1,927 ms (2,230)
/twitter/user/followers (200 rows)115,77028,943, then 7,16675.5%2,470 ms (2,886)
/twitter/tweets (20 posts by ID)142,98635,746, then 2,29194.2%3,417 ms (3,537)

A page of posts is a chapter of a book. The median 20-post timeline page was 128,177 bytes, about 32,044 tokens, and the largest single response in the run (245,639 bytes, a /twitter/tweets batch) was about 61,410 tokens. Across the 1,000 tweets the run returned, the average tweet object was 6,261 bytes raw, about 1,565 tokens, because every tweet carries its full author profile, entities, media objects and any quoted or reposted tweet in full. Trimmed to the skill's nine fields the same tweet is 448 bytes, about 112 tokens.

Trimming removed 90.1% of the bytes. The whole run was 7,441,992 bytes raw (1,860,498 tokens) and 737,285 bytes trimmed (184,321 tokens). The reduction is largest where the author object repeats most, posts by ID (94.2%) and timelines (93.4%), and smallest for profiles (74.5%) and follower rows (75.5%), which are already flat. Mentions trimmed less than timelines (87.6%) because mentions from smaller accounts carry smaller author objects and fewer media attachments to begin with.

One account, the whole skill, once. An agent that runs all six reads for a single account (seven calls, with two timeline pages) ingests about 685,559 bytes raw, roughly 171,390 tokens, which is more than many models' entire context window, for what is a modest question. With the trimming rule it is about 18,030 tokens. That difference, not the API price, is the main reason the skill tells the agent to fetch one page, trim it, and only then decide whether to fetch another.

Latency. Median wall-clock across all 70 calls was 2,238 ms, measured from the moment the client's rate-limit lock released to the parsed body, so it excludes our own pacing. Profiles were fastest (1,108 ms), the 20-ID batch slowest (3,417 ms). For an agent this means a six-read session (seven calls) takes about 16 seconds of API time at the medians; an agent that paginates deep into followers will spend most of its time waiting, so the skill tells it to estimate before looping.

Horizontal grouped bar chart of approximate tokens per call for the six reads in the twitterapi.io SKILL.md, raw versus trimmed, medians of 10 organization accounts. tweets by ID batch of 20: 35,746 raw, 2,291 trimmed, minus 94 percent. last_tweets 20 posts: 32,044 and 2,181. followers 200 rows: 28,943 and 7,166. advanced_search 20 posts: 27,250 and 2,138. mentions 20 posts: 15,030 and 1,991. user/info 1 profile: 332 and 83. Subtitle: across the run trimming removed 90.1 percent of the bytes, 1,860,498 tokens became 184,321.
Approximate tokens (bytes / 4) per call, as returned versus after the skill's field trimming, medians over 10 organization accounts, collected in 2026. A raw page of posts is a chapter; trimmed, it is a paragraph.
07 — Section

What the same calls cost in dollars, on both rate cards

Credits are known exactly from the published prices: tweet endpoints bill 15 credits per tweet returned with a 15-credit minimum per request, profiles 18 credits, follower pages 1 to 3 credits per row depending on the page tier, and $1 buys 100,000 credits. For the official X API we priced the same items at the pay-per-usage rate card on docs.x.com: $0.005 per post read, $0.010 per user read, $0.010 per follower read. Both columns price the items each call actually returned; neither includes a subscription.

Read (items per call)twitterapi.io, per callX API rate card, same itemsRatio
/twitter/user/info (1 profile)$0.00018 (18 credits)$0.0156x
/twitter/user/last_tweets (20 posts)$0.003 (300 credits)$0.133x
/twitter/tweet/advanced_search (20 posts)$0.003 (300 credits)$0.133x
/twitter/user/mentions (20 posts)$0.003 (300 credits)$0.133x
/twitter/user/followers (200 rows)$0.002 to $0.006 (200 to 600 credits)$2333x to 1000x
/twitter/tweets (20 posts by ID)$0.003 (300 credits)$0.133x
All six reads for one account (7 calls)$0.0172 to $0.0212$2.51118x to 146x
The whole study (70 calls)$0.17 to $0.21 (17,180 to 21,180 credits)$25.10119x to 146x

Followers decide the bill on the X side. Of the $25.10 the study would have cost at the X rate card, $20.00 is the 2,000 follower rows at $0.010 each; the 1,000 posts are $5.00 and the ten profiles $0.10. On twitterapi.io the order flips: a follower row is 1 to 3 credits and a tweet is 15, so an agent's cost profile inverts: here, a deep follower crawl is the smaller item and a deep timeline crawl is what to estimate first. For 1,000 posts the two rate cards are $5.00 and $0.15, about 33x apart.

The per-request minimum matters for agents. An agent that searches for something rare and gets zero results still pays 15 credits ($0.00015) on twitterapi.io, and a 1-post result costs the same 15 credits as the minimum. That floor is why the skill tells the agent to make searches specific (from:, lang:, -filter:replies) rather than issuing many near-empty probes; at 20 results a page the floor is irrelevant, at 0 or 1 it is the whole bill. Subscription plans with bonus credits are listed on the pricing page; the per-item prices above are what the skill quotes to the agent.

Dot plot on a log dollar axis of the cost per call for the six reads, twitterapi.io versus the official X API pay-per-usage rate card. user/info: $0.00018 versus $0.01. last_tweets, advanced_search, mentions and tweets by ID, 20 posts each: $0.003 versus $0.1. followers, 200 rows: $0.002 to $0.006 versus $2. Subtitle: the whole study of 70 calls cost $0.17 to $0.21 on twitterapi.io and $25.10 at the X API rate card.
Dollars per call for the items each call returned, both rate cards, log scale. The 70-call study: $0.17 to $0.21 on twitterapi.io, $25.10 at the X API pay-per-usage prices.
08 — Section

Writing a SKILL.md for your own API: what the spec and the measurements taught us

Make name equal the folder, and keep it boring. The validator rejects uppercase, leading or double hyphens and a name that differs from the directory. twitterapi-io installs the same way on every client; a clever name does not.

Write the description for the matcher, not for a human. Agents decide whether to load your skill from this one field, with about 100 tokens of budget. List the concrete tasks a user would phrase (profile, recent posts, search, followers) and the distinguishing condition (API key, no OAuth). The spec's own good example does exactly this for PDFs.

Put the response shape in the body, not a link to it. The field names and the nesting (data.tweets versus tweets versus followers) are what an agent cannot guess, and the body is loaded whole, so this is the surest place to prevent a wrong call. Save references/ for long material such as a full search-operator list.

Put prices and rate limits in the body too. X's skill omits them and defers to a docs page the agent may never fetch. Ours quotes 15 credits per tweet, the 15-credit minimum, 18 per profile and the request-rate tiers, and asks the agent to estimate before fetching more than 10 pages. Measured, the six reads for one account are $0.0172 to $0.0212; a skill that says so lets the agent tell the user before spending.

Prescribe a trimming rule. This is the lesson of the numbers: a 20-post page is about 32,044 tokens raw and about 2,181 trimmed. Without an instruction, an agent will paste the raw JSON into its context and run out of room after a few pages. Name the fields to keep and say "fetch one page, trim, then decide".

Lead with the error shape that is not an HTTP error. A 200 with status: "error" and msg: "user not found" is the case agents mishandle most; put it first, with the data: null consequence.

Keep writes behind an explicit request. If the API can act as a user account, say so late in the file, state the price and the account risk plainly, and make the default path read-only in the agent's behavior without claiming the API is. The spec has no field for this; the ordering of your sections is the mechanism.

Version in metadata, validate, then publish at a stable URL. metadata.version lets an agent or a human see which revision is installed; skills-ref validate catches frontmatter mistakes; a stable HTTPS URL to the single file is enough for npx skills add and for curl. If you want the file discoverable the way X's is, add a .well-known/agent-skills/index.json that lists it with a digest, per the discovery schema at schemas.agentskills.io.

09 — Section

How we collected this

Question: what does an agent that follows the twitterapi.io SKILL.md pay per call, in context (bytes and approximate tokens as returned and after the skill's trimming rule), in wall-clock, and in credits at the twitterapi.io rate card, and what would the same items cost at the official X API pay-per-usage rate card.

Accounts and calls: 10 public organization accounts, @vercel, @github, @Docker, @CloudflareDev, @OpenAIDevs, @nvidia, @NASA, @stripe, @supabase, @AnthropicAI. For each: /twitter/user/info, two pages of /twitter/user/last_tweets (default, replies excluded), one page of /twitter/user/mentions, one page of /twitter/tweet/advanced_search with query=from:<account> and queryType=Latest, one page of /twitter/user/followers, and one /twitter/tweets call with the 20 IDs from the first timeline page. 70 requests, 1,000 tweets, 2,000 follower rows, 10 profiles, collected in 2026 through a rate-limited client; 0 errors.

Measurement: size is the UTF-8 length of the response serialized as compact JSON; tokens are bytes divided by four, the common rule of thumb, which understates real tokenizer counts for JSON by a margin that varies by model, so treat the token figures as relative, not exact. Trimmed size applies the exact field lists in the skill (tweets: id, createdAt, text, url, author.userName, likeCount, retweetCount, replyCount, viewCount; profiles: nine fields; follower rows: five). Wall-clock runs from the release of the client's rate-limit lock to the parsed body. Credits follow the published prices; follower pages are shown as the 1-to-3-credit range because the tier is per page. X API prices are the pay-per-usage rate card on docs.x.com applied to the same item counts.

What the sample cannot show: medians over 10 large organization accounts; small accounts with short descriptions and no media would produce smaller raw pages and lower trim ratios. Latency was measured from one location on one day and will vary. We did not measure token counts with a real tokenizer, and we did not run the skill inside an agent harness to count the tokens the agent spent reasoning over the data; the figures are what the agent would ingest, not what it would do with it. The npx skills add direct-URL install was taken from the CLI's README and is not verified end to end until the file is live at the published URL.

Privacy: aggregates only. Profiles, timelines, from: searches and ID batches belong to the named organization accounts and are kept compressed with the study. Mentions and follower pages contain private individuals; those pages were kept only long enough to measure size and timing and were not stored with the study, and no text, handle or ID from them appears on this page.

python
"""skill_dry_run.py - run the six reads from the twitterapi.io SKILL.md for one account and report
what an agent would ingest (bytes, approximate tokens) raw and trimmed, plus the credits spent.

    pip install requests
    export TWITTERAPI_IO_API_KEY=...     # https://twitterapi.io/dashboard
    python skill_dry_run.py NASA
"""
import json
import os
import sys
import time

import requests

BASE = "https://api.twitterapi.io"
H = {"X-API-Key": os.environ["TWITTERAPI_IO_API_KEY"]}
TWEET_KEEP = ("id", "createdAt", "text", "url", "likeCount", "retweetCount", "replyCount", "viewCount")
PROFILE_KEEP = ("id", "userName", "name", "description", "followers", "following", "statusesCount", "createdAt", "isBlueVerified")
FOLLOWER_KEEP = ("id", "screen_name", "name", "followers_count", "created_at")


def get(url, **params):
    """One GET of a full URL. Errors come back as HTTP 200 with status=error and msg; raise on those too."""
    t0 = time.perf_counter()
    r = requests.get(url, headers=H, params=params, timeout=30)
    ms = (time.perf_counter() - t0) * 1000
    r.raise_for_status()
    body = r.json()
    if isinstance(body, dict) and body.get("status") == "error":
        raise RuntimeError(f"{url}: {body.get('msg')}")
    return body, ms


def size(obj):
    return len(json.dumps(obj, ensure_ascii=False, separators=(",", ":")).encode("utf-8"))


def trim_tweet(t):
    d = {k: t.get(k) for k in TWEET_KEEP}
    d["user"] = (t.get("author") or {}).get("userName")
    return d


def report(label, body, items, trimmed, credits, ms):
    raw = size(body)
    print(f"{label:<34} {items:>4} items  {raw:>8,} B ~{raw // 4:>6,} tok  ->  {size(trimmed):>7,} B ~{size(trimmed) // 4:>5,} tok  {credits:>4} credits  {ms:>6.0f} ms")
    return credits


def main(handle):
    spent = 0
    # 1. profile: 18 credits
    body, ms = get("/twitter/user/info", userName=handle)
    u = body["data"]
    spent += report("/twitter/user/info", body, 1, {k: u.get(k) for k in PROFILE_KEEP}, 18, ms)

    # 2. timeline, two pages: 15 credits per tweet returned (min 15 per request)
    cursor, ids = None, []
    for page in (1, 2):
        params = {"userName": handle}
        if cursor:
            params["cursor"] = cursor
        body, ms = get("/twitter/user/last_tweets", **params)
        tweets = (body.get("data") or {}).get("tweets") or []     # nested under data for this endpoint
        ids += [t["id"] for t in tweets]
        spent += report(f"/twitter/user/last_tweets p{page}", body, len(tweets), [trim_tweet(t) for t in tweets], max(15, 15 * len(tweets)), ms)
        if not body.get("has_next_page") or not body.get("next_cursor"):
            break
        cursor = body["next_cursor"]

    # 3. mentions and 4. search: top-level tweets[]
    body, ms = get("/twitter/user/mentions", userName=handle)
    tweets = body.get("tweets") or []
    spent += report("/twitter/user/mentions", body, len(tweets), [trim_tweet(t) for t in tweets], max(15, 15 * len(tweets)), ms)
    body, ms = get("/twitter/tweet/advanced_search", query=f"from:{handle}", queryType="Latest")
    tweets = body.get("tweets") or []
    spent += report("/twitter/tweet/advanced_search", body, len(tweets), [trim_tweet(t) for t in tweets], max(15, 15 * len(tweets)), ms)

    # 5. followers: top-level followers[], snake_case, 1 to 3 credits per row (we count the low tier)
    body, ms = get("/twitter/user/followers", userName=handle)
    rows = body.get("followers") or []
    spent += report("/twitter/user/followers", body, len(rows), [{k: r.get(k) for k in FOLLOWER_KEEP} for r in rows], len(rows), ms)

    # 6. posts by ID: the first 20 ids from the timeline
    if ids:
        body, ms = get("/twitter/tweets", tweet_ids=",".join(ids[:20]))
        tweets = body.get("tweets") or []
        spent += report("/twitter/tweets (20 ids)", body, len(tweets), [trim_tweet(t) for t in tweets], max(15, 15 * len(tweets)), ms)

    print(f"\ncredits spent (followers at the 1-credit tier): {spent:,} = ${spent / 100000:.4f}")


if __name__ == "__main__":
    main(sys.argv[1] if len(sys.argv) > 1 else "NASA")
10 — Questions

Questions readers ask

What is a SKILL.md file?

The required file of an Agent Skill, the open format at agentskills.io for giving AI agents instructions they load on demand. It is YAML frontmatter (a required name of up to 64 lowercase characters that matches its folder, a required description of up to 1,024 characters, and optional license, compatibility, metadata and allowed-tools) followed by Markdown instructions. The folder can also hold scripts/, references/ and assets/. Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenClaw and many other agents read it.

Is SKILL.md the same as CLAUDE.md, AGENTS.md or llms.txt?

No. CLAUDE.md and AGENTS.md are always-on project instructions loaded every session. llms.txt is an index of documentation pages for crawlers and agents. A SKILL.md is loaded only when a task matches its description, which is why it can carry detailed, task-specific instructions without costing context the rest of the time. X publishes all three for its API: llms.txt, skill.md and an MCP server, grouped as Agent Resources in its docs.

How do I install a SKILL.md in Claude Code?

Put the folder under .claude/skills/ in a project or ~/.claude/skills/ for all projects, so the file sits at .claude/skills/<name>/SKILL.md with the folder named exactly as the name field. The skills CLI does it for you: npx skills add <source>, where source can be a GitHub repository, a sub-folder URL, a local path or a direct URL to one SKILL.md. The directory name becomes a slash command and Claude also activates the skill automatically when a request matches the description.

What sources does npx skills add accept?

Per the vercel-labs/skills README: GitHub shorthand such as owner/repo, full GitHub, GitLab and Azure Repos URLs, a URL to a specific skill folder inside a repository, git SSH URLs, local paths, and direct download URLs that point at a single valid SKILL.md file or a .zip, .tar, .tar.gz or .tgz archive. Flags: -g for a global install, -y to skip prompts, -a to target specific agents, -s to pick skills from a multi-skill repository.

What is in X's skill.md and does it include pricing?

The file at docs.x.com/skill.md (235 lines when fetched) has a frontmatter name of x, sections for product summary, when to use, quick reference, decision guidance, workflow, gotchas, a checklist and resources. It covers bearer-token and OAuth authentication, eleven /2/ endpoints, fields and expansions, pagination with meta.next_token and the x-rate-limit headers. It says the API is pay-per-usage but gives no prices; those are on the pricing page: $0.005 per post read, $0.010 per user or follower read, $0.015 per post created.

Do I need an X developer account to use the twitterapi.io skill?

No. Reads need only a twitterapi.io API key sent in the X-API-Key header, obtained from the dashboard; new accounts that pass the automatic bot check get up to $1 of free credits (one per person or network, expiring after 31 days) without a card. There is no X app, OAuth flow or developer agreement for reads. Write actions are different: they run through a logged-in X account session and act as that account.

How big should a SKILL.md be?

The specification says to keep the main file under 500 lines and the body under about 5,000 tokens, moving long reference material into references/ files the agent loads only when pointed to them. Both skills on this page are well inside that: X's is 235 lines (about 2,482 tokens) and the twitterapi.io file is 177 lines (about 2,237 tokens). The data the skill causes the agent to fetch is far larger: a single 20-post page is about 32,044 tokens raw, which is why the file prescribes a trimming rule.

Can a skill post to X, like or follow?

X's skill documents POST /2/tweets, likes and follows under OAuth user context, priced at $0.015 per post created ($0.200 with a URL) on the pay-per-usage card. The twitterapi.io skill mentions writes only in a final section marked for explicit requests: a session login via /twitter/user_login_v2 ($0.10, with a proxy you supply) and $0.002 per action, with the caveats that the call acts as that X account, which X can rate-limit, lock or suspend, that a human pace is needed, that the session may need re-login, and that calls are billed per HTTP 200 response.

11 — Further reading

Continue

Sources & further reading
More from this series
Build it

Stop reading. Start building.

Starter credits cover real testing on real data. Google sign-in, no card, no application queue.

Get an API key