diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index ff21a00..19869e8 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -16,7 +16,28 @@
"source": "./plugins/core",
"description": "Commands, agents, skills, and context for AI-assisted development workflows",
"version": "9.22.0",
- "tags": ["commands", "agents", "skills", "workflows", "essential"]
+ "tags": [
+ "commands",
+ "agents",
+ "skills",
+ "workflows",
+ "essential"
+ ]
+ },
+ {
+ "name": "hermes-tweet",
+ "source": "./plugins/hermes-tweet",
+ "description": "Hermes Agent X/Twitter plugin for research, profile and post reads, and gated write actions through Xquik.",
+ "version": "0.1.6",
+ "tags": [
+ "hermes-agent",
+ "hermes-plugin",
+ "xquik",
+ "twitter",
+ "x",
+ "social-media",
+ "automation"
+ ]
}
]
}
diff --git a/README.md b/README.md
index fd63db3..00e3bce 100644
--- a/README.md
+++ b/README.md
@@ -44,10 +44,15 @@ If you prefer to install manually, run these in Claude Code:
```
/plugin install ai-coding-config
+/plugin install hermes-tweet
```
## Todo Persistence Across Compaction
+### Hermes Tweet Plugin
+
+Hermes Tweet adds X/Twitter research, profile reads, post reads, and gated action tools for Hermes Agent users. Exploratory catalog access works without a key; read tools require `XQUIK_API_KEY`, and action tools also require `HERMES_TWEET_ENABLE_ACTIONS=true`.
+
**The problem**: Claude Code's context compaction summarizes conversation history to
stay within token limits. When this happens, your todo list vanishes - you lose track of
what you were working on.
diff --git a/plugins/hermes-tweet/.claude-plugin/plugin.json b/plugins/hermes-tweet/.claude-plugin/plugin.json
new file mode 100644
index 0000000..366fe44
--- /dev/null
+++ b/plugins/hermes-tweet/.claude-plugin/plugin.json
@@ -0,0 +1,21 @@
+{
+ "name": "hermes-tweet",
+ "version": "0.1.6",
+ "description": "Native Hermes Agent X/Twitter plugin for Xquik automation with read-first workflows and approval-gated actions.",
+ "author": {
+ "name": "Xquik",
+ "url": "https://github.com/Xquik-dev"
+ },
+ "license": "MIT",
+ "homepage": "https://github.com/Xquik-dev/hermes-tweet#readme",
+ "repository": "https://github.com/Xquik-dev/hermes-tweet",
+ "keywords": [
+ "hermes-agent",
+ "hermes-plugin",
+ "xquik",
+ "twitter",
+ "x",
+ "social-media",
+ "automation"
+ ]
+}
diff --git a/plugins/hermes-tweet/.codex-plugin/plugin.json b/plugins/hermes-tweet/.codex-plugin/plugin.json
new file mode 100644
index 0000000..3c28322
--- /dev/null
+++ b/plugins/hermes-tweet/.codex-plugin/plugin.json
@@ -0,0 +1,45 @@
+{
+ "name": "hermes-tweet",
+ "version": "0.1.6",
+ "description": "Native Hermes Agent X/Twitter plugin for Xquik automation with read-first workflows and approval-gated actions.",
+ "author": {
+ "name": "Xquik",
+ "url": "https://github.com/Xquik-dev"
+ },
+ "homepage": "https://github.com/Xquik-dev/hermes-tweet#readme",
+ "repository": "https://github.com/Xquik-dev/hermes-tweet",
+ "license": "MIT",
+ "keywords": [
+ "hermes-agent",
+ "hermes-plugin",
+ "xquik",
+ "twitter",
+ "x",
+ "social-media",
+ "automation",
+ "codex-plugin"
+ ],
+ "skills": "./skills/",
+ "interface": {
+ "displayName": "Hermes Tweet",
+ "shortDescription": "Use Hermes Agent for X/Twitter research and gated actions.",
+ "longDescription": "Hermes Tweet gives Codex a source-native install surface for the Hermes Agent X/Twitter plugin. Use it to discover tweet and user read tools, summarize public X context, and keep account-changing actions behind explicit Hermes Tweet approval gates.",
+ "developerName": "Xquik",
+ "category": "Productivity",
+ "capabilities": [
+ "Interactive",
+ "Read",
+ "Write"
+ ],
+ "websiteURL": "https://github.com/Xquik-dev/hermes-tweet#readme",
+ "privacyPolicyURL": "https://github.com/Xquik-dev/hermes-tweet/security/policy",
+ "termsOfServiceURL": "https://github.com/Xquik-dev/hermes-tweet/blob/master/LICENSE",
+ "defaultPrompt": [
+ "Use Hermes Tweet to research this X/Twitter topic.",
+ "Use Hermes Tweet to read public replies and summarize the thread.",
+ "Use Hermes Tweet to prepare an approval-gated X/Twitter action."
+ ],
+ "brandColor": "#111827",
+ "composerIcon": "./assets/icon.svg"
+ }
+}
diff --git a/plugins/hermes-tweet/LICENSE b/plugins/hermes-tweet/LICENSE
new file mode 100644
index 0000000..ba5298f
--- /dev/null
+++ b/plugins/hermes-tweet/LICENSE
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2026 Xquik
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/plugins/hermes-tweet/README.md b/plugins/hermes-tweet/README.md
new file mode 100644
index 0000000..bdc27fd
--- /dev/null
+++ b/plugins/hermes-tweet/README.md
@@ -0,0 +1,306 @@
+# Hermes Tweet
+
+[](https://github.com/Xquik-dev/hermes-tweet/actions/workflows/ci.yml)
+[](https://github.com/Xquik-dev/hermes-tweet#readme)
+
+[](https://deepwiki.com/Xquik-dev/hermes-tweet)
+[](https://pypi.org/project/hermes-tweet/)
+[](https://piwheels.org/project/hermes-tweet/)
+[](https://pypi.org/project/hermes-tweet/)
+[](https://pypi.org/project/hermes-tweet/)
+[](https://pypi.org/project/hermes-tweet/#files)
+[](https://pypi.org/project/hermes-tweet/)
+[](https://github.com/Xquik-dev/hermes-tweet/releases)
+[](https://apify.com/xquik/x-tweet-scraper)
+[](LICENSE)
+
+Native [Hermes Agent](https://github.com/NousResearch/hermes-agent) plugin for
+X automation through [Xquik](https://xquik.com).
+
+Hermes Tweet brings X search, account reads, tweet posting, replies, likes,
+retweets, follows, DMs, monitors, webhooks, draws, extraction jobs, media, and
+trend reads into Hermes as structured tools.
+
+Use it when you need a Hermes Agent Twitter plugin, Hermes X automation, social
+media automation for agents, or a native Hermes toolset for X/Twitter.
+
+## Highlights
+
+- Published Python package with a native Hermes plugin entry point.
+- Installable from PyPI as `hermes-tweet`.
+- 99 agent-callable Xquik endpoints generated from OpenAPI.
+- 31 MPP-tagged read endpoints in the bundled catalog.
+- Read and action tools are split for least-privilege operation.
+- Action endpoints are disabled by default.
+- Bundled Hermes skill for agent-facing usage guidance.
+- Slash commands for account status and trends.
+- Current guidance for Hermes Agent v0.16.0 Desktop, remote gateway, and
+ dashboard credential workflows.
+- Strict CI with formatting, linting, type checking, tests, coverage, security
+ scan, dependency audit, and package build checks.
+
+## Install
+
+Recommended Hermes plugin install:
+
+```bash
+hermes plugins install Xquik-dev/hermes-tweet --enable
+```
+
+Hermes Agent treats third-party plugins as opt-in. Without `--enable`, the
+installer can discover Hermes Tweet, but it may leave the plugin in the
+`not enabled` state until you run `hermes plugins enable hermes-tweet` or toggle
+it in the interactive `hermes plugins` UI. Use `hermes plugins list` when a
+fresh install does not show the `hermes-tweet` toolset.
+
+Hermes will prompt for `XQUIK_API_KEY` during an interactive install and save it
+to `~/.hermes/.env`. In non-interactive installs the prompt is skipped; set the
+key through the environment or `~/.hermes/.env` before running `tweet_read`.
+If you edit `~/.hermes/.env` while Hermes is already running, use `/reload` in
+an interactive CLI session, or restart gateway and cron sessions before calling
+`tweet_read`.
+When `XQUIK_API_KEY` is not configured, Hermes should expose only the no-network
+`tweet_explore` tool from this plugin. That is expected safe gating, not an
+install failure.
+
+Install the published Python package from PyPI into the Hermes Python
+environment:
+
+```bash
+uv pip install --python ~/.hermes/hermes-agent/venv/bin/python hermes-tweet
+hermes plugins enable hermes-tweet
+```
+
+If your Hermes venv includes `pip`, this path is also valid:
+
+```bash
+~/.hermes/hermes-agent/venv/bin/python -m pip install hermes-tweet
+hermes plugins enable hermes-tweet
+```
+
+From a local checkout:
+
+```bash
+hermes plugins install file:///absolute/path/to/hermes-tweet --force --enable
+```
+
+If you are testing from a project-local `.hermes/plugins/` directory instead of
+installing the plugin into `~/.hermes/plugins/` or through the PyPI entry point,
+start Hermes with `HERMES_ENABLE_PROJECT_PLUGINS=true` only for trusted
+repositories.
+
+Hermes Agent v0.16.0 adds a native desktop app and remote gateway profiles.
+For a remote gateway profile, install and enable Hermes Tweet on the remote
+Hermes host because that is where plugin code executes and where
+`XQUIK_API_KEY` must be available. The desktop app is the chat surface; it
+should not receive or store the key unless it is also running the Hermes
+runtime locally.
+
+## Python Package
+
+| Field | Value |
+| --- | --- |
+| PyPI | [`hermes-tweet`](https://pypi.org/project/hermes-tweet/) |
+| Official guide | [`github.com/Xquik-dev/hermes-tweet#readme`](https://github.com/Xquik-dev/hermes-tweet#readme) |
+| Context7 | [`context7.com/xquik-dev/hermes-tweet`](https://context7.com/xquik-dev/hermes-tweet) |
+| piwheels | [`hermes-tweet`](https://piwheels.org/project/hermes-tweet/) |
+| Latest release | [`v0.1.6`](https://github.com/Xquik-dev/hermes-tweet/releases/tag/v0.1.6) |
+| Supported Python | `>=3.11` |
+| Package format | Wheel and source distribution |
+| Hermes entry point | `hermes-tweet = hermes_tweet` |
+| Entry point group | `hermes_agent.plugins` |
+| Included assets | `plugin.yaml`, `catalog_data.json`, bundled Hermes skill, Codex/Claude manifests |
+| Claude plugin manifest | [`.claude-plugin/plugin.json`](.claude-plugin/plugin.json) |
+| Codex plugin manifest | [`.codex-plugin/plugin.json`](.codex-plugin/plugin.json) |
+| Registry skill path | [`skills/hermes-tweet/SKILL.md`](skills/hermes-tweet/SKILL.md) |
+| Context7 guide | [`docs/CONTEXT7.md`](docs/CONTEXT7.md) |
+| Hermes surface guide | [`docs/HERMES_SURFACES.md`](docs/HERMES_SURFACES.md) |
+| Integration patterns | [`docs/INTEGRATION_PATTERNS.md`](docs/INTEGRATION_PATTERNS.md) |
+| Submission readiness | [`docs/SUBMISSION_READINESS.md`](docs/SUBMISSION_READINESS.md) |
+| Merge enablement | [`docs/MERGE_ENABLEMENT.md`](docs/MERGE_ENABLEMENT.md) |
+
+Hermes Tweet also ships source-native `.codex-plugin` metadata, a root
+security policy, a local composer icon, and a HOL Plugin Scanner workflow for
+Codex plugin catalogs that require local validation evidence before listing.
+
+## Configure
+
+Create an API key in the Xquik dashboard, then set:
+
+```bash
+export XQUIK_API_KEY="xq_..."
+```
+
+Optional settings:
+
+```bash
+export XQUIK_BASE_URL="https://xquik.com"
+export HERMES_TWEET_ENABLE_ACTIONS="false"
+```
+
+Action endpoints are disabled unless `HERMES_TWEET_ENABLE_ACTIONS=true`.
+If you configure keys through `~/.hermes/.env` during an active Hermes session,
+use `/reload` in the interactive CLI, or restart gateway and cron sessions so
+they pick up the new values.
+
+## Security Model
+
+Hermes Tweet never accepts credentials through tool arguments. Auth is read from
+environment variables and injected by the plugin at request time.
+
+The plugin blocks dashboard-only admin, billing, credit top-up, support-ticket,
+API-key, and account re-authentication endpoints from the catalog. Private reads
+and write-like endpoints go through `tweet_action`, which is hidden unless
+`HERMES_TWEET_ENABLE_ACTIONS=true`.
+
+## Tools
+
+| Tool | Purpose |
+| --- | --- |
+| `tweet_explore` | Search the bundled Xquik endpoint catalog. No API call. |
+| `tweet_read` | Call catalog-listed read-only endpoints. |
+| `tweet_action` | Call write-like or private endpoints. Disabled by default. |
+
+Use `tweet_explore` first, then call `tweet_read` or `tweet_action` with a
+concrete `/api/v1/...` path.
+Copied endpoint URLs are accepted, but Hermes Tweet matches only catalog-listed
+paths.
+
+## Hermes Agent Workflows
+
+Hermes Tweet is best used as the X context layer for Hermes Agent workflows that
+need current public signal, authenticated account context, or approval-gated
+account actions:
+
+| Workflow | Recommended Path |
+| --- | --- |
+| Social listening | Use `tweet_explore` to find search, user, trend, monitor, or radar routes, then use `tweet_read` for public reads. |
+| Launch monitoring | Keep `tweet_action` disabled, schedule Hermes cron sessions around read-only trend, mention, and account checks. |
+| Support triage | Read public mentions and user timelines, summarize issues in Hermes, then hand off account-changing responses for explicit approval. |
+| Creator or brand research | Combine X search, user profile, follower, media, and trend reads before drafting content or campaign briefs. |
+| Giveaway and community audits | Use read routes for tweet, reply, follower, list, draw, and export evidence before any action route. |
+| Controlled publishing | Enable `HERMES_TWEET_ENABLE_ACTIONS=true` only in sessions that require posting, DMs, follows, webhooks, monitors, or media changes. |
+| Desktop operator sessions | Use Hermes Desktop for interactive review, then keep action calls explicit and approval-gated. |
+| Remote gateway teams | Install Hermes Tweet and set `XQUIK_API_KEY` on the remote gateway host, then connect Desktop profiles to that host. |
+| Dashboard-administered agents | Use the dashboard for gateway and credential operations, but keep Hermes Tweet secrets in the runtime environment. |
+
+For marketing or user education, position Hermes Tweet as a native Hermes Agent
+plugin, not a generic API wrapper: it ships a PyPI entry point, a `plugin.yaml`
+manifest with interactive secret prompts, slash commands for quick diagnostics,
+and a bundled skill registered through Hermes' plugin skill system.
+
+## Hermes Runtime Fit
+
+Hermes Tweet registers a dedicated `hermes-tweet` plugin toolset. Hermes can
+show and manage those tools through its normal `hermes tools` and platform
+toolset flows, so teams can keep X automation available only where it belongs.
+Current Hermes Agent releases discover third-party plugins but do not execute
+them until they are enabled in `plugins.enabled`, through `hermes plugins enable`,
+or by installing with `--enable`. This is expected safety behavior for user and
+PyPI entry-point plugins.
+
+Hermes Agent v0.16.0 expands the surfaces where the same toolset can appear:
+the native Desktop app, remote gateway profiles, the web dashboard, the TUI,
+and the CLI can all route work to the enabled runtime. Hermes Tweet does not
+need a different plugin entry point for those surfaces. It needs the same
+enabled plugin, the same runtime environment, and the same read/action split.
+
+The v0.16.0 Desktop command palette surfaces skills and quick-command slash
+commands. Treat `/xstatus` and `/xtrends` as interactive runtime commands for
+active CLI, TUI, Desktop, or gateway sessions; keep `hermes -z` for tool-call
+smoke tests.
+
+The v0.16.0 dashboard added broader administration and credential-management
+surfaces. Those are useful for gateway operations, but Hermes Tweet still reads
+`XQUIK_API_KEY` and `HERMES_TWEET_ENABLE_ACTIONS` from the runtime environment.
+Do not paste API keys into prompts, issue bodies, PR comments, or tool inputs.
+
+For non-interactive smoke tests and CI-style diagnostics, use
+`hermes tools list`; bare `hermes tools` opens the interactive tool UI and
+requires a TTY. In current Hermes Agent releases, `hermes tools list` reports plugin toolsets,
+not every individual plugin tool name.
+
+Use the read-only path for social listening, trend research, account checks,
+giveaway audits, and draft planning. Keep `HERMES_TWEET_ENABLE_ACTIONS=false`
+for unattended cron or gateway sessions unless the workflow has an explicit
+approval step for posting, DMs, follows, monitor changes, webhook changes, or
+other account actions.
+
+Runtime smoke test:
+
+```bash
+hermes -z "Use tweet_explore, then read /api/v1/account. Do not call tweet_action." --toolsets hermes-tweet
+```
+
+Expected results:
+
+- `tweet_explore` discovers catalog endpoints without using the API key.
+- Without `XQUIK_API_KEY`, a non-mutating Hermes probe exposes `tweet_explore`
+ only.
+- After `XQUIK_API_KEY` is configured and the CLI is reloaded, or the gateway
+ or cron process is restarted, `tweet_read` can read `/api/v1/account`.
+- `tweet_action` stays hidden or disabled unless `HERMES_TWEET_ENABLE_ACTIONS=true`.
+- `/xstatus` and `/xtrends` appear in the Hermes plugin command registry.
+
+Hermes one-shot `hermes -z "/xstatus"` can run as a model prompt, not as the
+interactive slash-command dispatcher. Verify slash commands in an active CLI,
+TUI, Desktop, or gateway session, or through the plugin registry tests, and use
+`hermes -z` for tool-call probes.
+
+If `hermes plugins install` runs without a TTY, Hermes cannot safely prompt for
+secrets and will skip API-key storage. This is expected; set `XQUIK_API_KEY`
+in the process environment or `~/.hermes/.env`.
+
+## Slash Commands
+
+| Command | Purpose |
+| --- | --- |
+| `/xstatus` | Show Xquik account, subscription, and usage status. |
+| `/xtrends` | Show current X trends. |
+
+## Development
+
+Generate the bundled catalog from Xquik OpenAPI:
+
+```bash
+python scripts/build_catalog.py ../xquik/openapi.yaml
+```
+
+Run checks:
+
+```bash
+uv run --python 3.12 --extra dev ruff format --check .
+uv run --python 3.12 --extra dev ruff check .
+uv run --python 3.12 --extra dev basedpyright
+uv run --python 3.12 --extra dev pytest --cov=hermes_tweet --cov=tests --cov-report=term-missing --cov-fail-under=100
+uv run --python 3.12 --extra dev bandit -c pyproject.toml -r hermes_tweet scripts
+uv run --python 3.12 --extra dev pip-audit
+uv run --python 3.12 --extra dev python -m build
+uv run --python 3.12 --extra dev twine check dist/*
+```
+
+For a single local quality gate:
+
+```bash
+uv run --python 3.12 --extra dev ruff format --check . && \
+uv run --python 3.12 --extra dev ruff check . && \
+uv run --python 3.12 --extra dev basedpyright && \
+uv run --python 3.12 --extra dev pytest --cov=hermes_tweet --cov=tests --cov-report=term-missing --cov-fail-under=100 && \
+uv run --python 3.12 --extra dev bandit -c pyproject.toml -r hermes_tweet scripts && \
+uv run --python 3.12 --extra dev pip-audit && \
+uv run --python 3.12 --extra dev python -m build && \
+uv run --python 3.12 --extra dev twine check dist/*
+```
+
+## Public Repo Metadata
+
+Recommended GitHub description:
+
+> Native Hermes Agent plugin for X/Twitter automation through Xquik.
+
+Recommended topics:
+
+`hermes-agent`, `hermes-plugin`, `hermes`, `twitter`, `x-api`,
+`x-automation`, `xquik`, `tweet`, `automation`, `social-media`,
+`social-media-automation`, `ai-agent`, `mcp`, `agent-tools`, `twitter-api`,
+`twitter-automation`, `x-twitter`, `social-media-api`, `agent-skill`, `python`
diff --git a/plugins/hermes-tweet/__init__.py b/plugins/hermes-tweet/__init__.py
new file mode 100644
index 0000000..147cb1a
--- /dev/null
+++ b/plugins/hermes-tweet/__init__.py
@@ -0,0 +1,8 @@
+from __future__ import annotations
+
+if __package__:
+ from .hermes_tweet import register
+else:
+ from hermes_tweet import register
+
+__all__ = ["register"]
diff --git a/plugins/hermes-tweet/after-install.md b/plugins/hermes-tweet/after-install.md
new file mode 100644
index 0000000..dacb4ad
--- /dev/null
+++ b/plugins/hermes-tweet/after-install.md
@@ -0,0 +1,58 @@
+# Hermes Tweet Installed
+
+Hermes Tweet is enabled as the `hermes-tweet` toolset.
+
+If this plugin was installed without `--enable`, Hermes may show it as
+`not enabled` until you run:
+
+```bash
+hermes plugins enable hermes-tweet
+hermes plugins list
+```
+
+Set your Xquik API key before using read tools:
+
+```bash
+export XQUIK_API_KEY="xq_..."
+```
+
+For persistent Hermes sessions, add it to `~/.hermes/.env`:
+
+```bash
+XQUIK_API_KEY=xq_...
+```
+
+If Hermes is already running after you edit `~/.hermes/.env`, use `/reload` in
+an interactive CLI session, or restart gateway and cron sessions before calling
+`tweet_read`.
+When `XQUIK_API_KEY` is missing, Hermes should expose only `tweet_explore` from
+this plugin. Set the key, then reload the CLI or restart the gateway or cron
+process before expecting `tweet_read`.
+
+Keep actions disabled unless you are intentionally allowing account-changing
+operations:
+
+```bash
+export HERMES_TWEET_ENABLE_ACTIONS=false
+```
+
+Quick smoke test:
+
+```bash
+hermes -z "Use tweet_explore, then read /api/v1/account. Do not call tweet_action." --toolsets hermes-tweet
+```
+
+Use catalog-listed `/api/v1/...` paths from `tweet_explore`. Copied endpoint
+URLs are accepted only when they resolve to catalog-listed paths.
+
+Expected behavior:
+
+- `tweet_explore` loads without an API call.
+- `tweet_read` works when `XQUIK_API_KEY` is set.
+- `/xstatus` and `/xtrends` are registered slash commands.
+- `tweet_action` stays hidden or returns a disabled error unless
+ `HERMES_TWEET_ENABLE_ACTIONS=true`.
+
+For Hermes v0.12.0, do not use `hermes -z "/xstatus"` as a slash-command smoke
+test. One-shot `-z` treats that text as a model prompt. Verify slash commands in
+an active CLI or gateway session, or through the plugin registry tests.
diff --git a/plugins/hermes-tweet/assets/icon.svg b/plugins/hermes-tweet/assets/icon.svg
new file mode 100644
index 0000000..88cdf55
--- /dev/null
+++ b/plugins/hermes-tweet/assets/icon.svg
@@ -0,0 +1,7 @@
+
diff --git a/plugins/hermes-tweet/hermes_tweet/__init__.py b/plugins/hermes-tweet/hermes_tweet/__init__.py
new file mode 100644
index 0000000..1aaa2bc
--- /dev/null
+++ b/plugins/hermes-tweet/hermes_tweet/__init__.py
@@ -0,0 +1,81 @@
+from __future__ import annotations
+
+import logging
+import os
+from pathlib import Path
+from typing import Any
+
+from . import schemas
+from .tools import (
+ action_enabled,
+ call_action,
+ call_read,
+ check_api_available,
+ explore,
+ xstatus,
+ xtrends,
+)
+
+logger = logging.getLogger(__name__)
+
+TOOLSET = "hermes-tweet"
+
+
+def _register_bundled_skills(ctx: Any) -> None:
+ skills_dir = Path(__file__).parent / "skills"
+ for child in sorted(skills_dir.iterdir()):
+ skill_md = child / "SKILL.md"
+ if child.is_dir() and skill_md.exists():
+ ctx.register_skill(child.name, skill_md)
+
+
+def register(ctx: Any) -> None:
+ ctx.register_tool(
+ name="tweet_explore",
+ toolset=TOOLSET,
+ schema=schemas.TWEET_EXPLORE,
+ handler=explore,
+ is_async=False,
+ description="Search the bundled Xquik endpoint catalog.",
+ emoji="🔎",
+ )
+
+ ctx.register_tool(
+ name="tweet_read",
+ toolset=TOOLSET,
+ schema=schemas.TWEET_READ,
+ handler=call_read,
+ check_fn=check_api_available,
+ requires_env=["XQUIK_API_KEY"],
+ is_async=False,
+ description="Call catalog-listed read-only Xquik endpoints.",
+ emoji="📖",
+ )
+
+ ctx.register_tool(
+ name="tweet_action",
+ toolset=TOOLSET,
+ schema=schemas.TWEET_ACTION,
+ handler=call_action,
+ check_fn=action_enabled,
+ requires_env=["XQUIK_API_KEY", "HERMES_TWEET_ENABLE_ACTIONS"],
+ is_async=False,
+ description="Call write-like or private Xquik endpoints.",
+ emoji="✍️",
+ )
+
+ ctx.register_command(
+ "xstatus",
+ handler=xstatus,
+ description="Show Xquik account and usage status",
+ )
+ ctx.register_command("xtrends", handler=xtrends, description="Show current X trends")
+
+ _register_bundled_skills(ctx)
+ logger.info(
+ "Hermes Tweet loaded with actions=%s",
+ os.getenv("HERMES_TWEET_ENABLE_ACTIONS", "false"),
+ )
+
+
+__all__ = ["register"]
diff --git a/plugins/hermes-tweet/hermes_tweet/catalog.py b/plugins/hermes-tweet/hermes_tweet/catalog.py
new file mode 100644
index 0000000..a1efcc5
--- /dev/null
+++ b/plugins/hermes-tweet/hermes_tweet/catalog.py
@@ -0,0 +1,210 @@
+from __future__ import annotations
+
+import json
+from dataclasses import dataclass
+from importlib.resources import files
+from typing import Any, cast
+from urllib.parse import urlsplit
+
+DEFAULT_LIMIT = 25
+MAX_LIMIT = 100
+
+
+@dataclass(frozen=True)
+class Endpoint:
+ category: str
+ free: bool
+ method: str
+ path: str
+ summary: str
+ parameters: tuple[dict[str, Any], ...] = ()
+ response_shape: str | None = None
+ mpp: dict[str, str] | None = None
+ action: bool = False
+
+ def to_dict(self) -> dict[str, Any]:
+ data: dict[str, Any] = {
+ "category": self.category,
+ "free": self.free,
+ "method": self.method,
+ "path": self.path,
+ "summary": self.summary,
+ "action": self.action,
+ }
+ if self.parameters:
+ data["parameters"] = list(self.parameters)
+ if self.response_shape:
+ data["responseShape"] = self.response_shape
+ if self.mpp:
+ data["mpp"] = self.mpp
+ return data
+
+
+def _load_raw() -> list[dict[str, Any]]:
+ text = (
+ files(__package__ or "hermes_tweet")
+ .joinpath("catalog_data.json")
+ .read_text(encoding="utf-8")
+ )
+ data = cast("object", json.loads(text))
+ items = cast("list[object]", data if isinstance(data, list) else [])
+ return [cast("dict[str, Any]", item) for item in items if isinstance(item, dict)]
+
+
+def _tuple_parameters(value: Any) -> tuple[dict[str, Any], ...]:
+ items = cast("list[object]", value if isinstance(value, list) else [])
+ return tuple(cast("dict[str, Any]", item) for item in items if isinstance(item, dict))
+
+
+def _endpoint(item: dict[str, Any]) -> Endpoint:
+ return Endpoint(
+ action=bool(item.get("action")),
+ category=str(item.get("category", "unknown")),
+ free=bool(item.get("free", False)),
+ method=str(item.get("method", "GET")).upper(),
+ mpp=item.get("mpp") if isinstance(item.get("mpp"), dict) else None,
+ parameters=_tuple_parameters(item.get("parameters")),
+ path=str(item.get("path", "")),
+ response_shape=str(item["responseShape"]) if item.get("responseShape") else None,
+ summary=str(item.get("summary", "")),
+ )
+
+
+ENDPOINTS: tuple[Endpoint, ...] = tuple(_endpoint(item) for item in _load_raw())
+
+
+def normalize_method(method: Any, *, default: str = "GET") -> str:
+ if not isinstance(method, str):
+ return default.upper()
+ normalized = method.strip().upper()
+ return normalized or default.upper()
+
+
+def normalize_limit(value: Any) -> int:
+ if isinstance(value, bool):
+ return DEFAULT_LIMIT
+ if isinstance(value, str):
+ value = value.strip()
+ if not value.isdecimal():
+ return DEFAULT_LIMIT
+ return normalize_limit(int(value))
+ if not isinstance(value, int):
+ return DEFAULT_LIMIT
+ return min(max(value, 1), MAX_LIMIT)
+
+
+def _optional_bool(value: Any) -> bool | None:
+ if isinstance(value, bool):
+ return value
+ if isinstance(value, str):
+ normalized = value.strip().lower()
+ if normalized == "true":
+ return True
+ if normalized == "false":
+ return False
+ return None
+
+
+def _optional_text(value: Any) -> str:
+ if not isinstance(value, str):
+ return ""
+ return value.strip()
+
+
+def _optional_method(value: Any) -> str | None:
+ normalized = _optional_text(value)
+ if not normalized:
+ return None
+ return normalize_method(normalized)
+
+
+def _segments(path: str) -> list[str]:
+ normalized = path.removesuffix("/")
+ return normalized.split("/")
+
+
+def normalize_path(path: str) -> str:
+ return urlsplit(path.strip()).path
+
+
+def matches_path(template: str, concrete: str) -> bool:
+ normalized_template = normalize_path(template)
+ normalized_concrete = normalize_path(concrete)
+ if normalized_template == normalized_concrete:
+ return True
+ template_segments = _segments(normalized_template)
+ concrete_segments = _segments(normalized_concrete)
+ if len(template_segments) != len(concrete_segments):
+ return False
+ for template_segment, concrete_segment in zip(
+ template_segments, concrete_segments, strict=True
+ ):
+ if template_segment.startswith("{") and template_segment.endswith("}"):
+ if not concrete_segment:
+ return False
+ continue
+ if template_segment.startswith(":"):
+ if not concrete_segment:
+ return False
+ continue
+ if template_segment != concrete_segment:
+ return False
+ return True
+
+
+def find_endpoint(method: str, path: str) -> Endpoint | None:
+ normalized = normalize_method(method)
+ normalized_path = normalize_path(path)
+ for endpoint in ENDPOINTS:
+ if endpoint.method == normalized and matches_path(endpoint.path, normalized_path):
+ return endpoint
+ return None
+
+
+def _matches_query(endpoint: Endpoint, query: str) -> bool:
+ normalized = query.lower()
+ haystack = " ".join(
+ [
+ endpoint.category,
+ endpoint.method,
+ endpoint.path,
+ endpoint.summary,
+ endpoint.response_shape or "",
+ json.dumps(list(endpoint.parameters), ensure_ascii=False),
+ ]
+ ).lower()
+ return normalized in haystack
+
+
+def explore(args: dict[str, Any]) -> list[dict[str, Any]]:
+ method = _optional_method(args.get("method"))
+ category = _optional_text(args.get("category")).lower()
+ path = _optional_text(args.get("path"))
+ query = _optional_text(args.get("query"))
+ limit = normalize_limit(args.get("limit"))
+ include_actions = _optional_bool(args.get("include_actions")) is True
+
+ endpoints = ENDPOINTS
+ if method:
+ endpoints = tuple(endpoint for endpoint in endpoints if endpoint.method == method)
+ if category:
+ endpoints = tuple(
+ endpoint for endpoint in endpoints if endpoint.category.lower() == category
+ )
+ if path:
+ endpoints = tuple(
+ endpoint
+ for endpoint in endpoints
+ if path in endpoint.path or matches_path(endpoint.path, path)
+ )
+ if query:
+ endpoints = tuple(endpoint for endpoint in endpoints if _matches_query(endpoint, query))
+ free = _optional_bool(args.get("free"))
+ if free is not None:
+ endpoints = tuple(endpoint for endpoint in endpoints if endpoint.free is free)
+ mpp = _optional_bool(args.get("mpp"))
+ if mpp is not None:
+ endpoints = tuple(endpoint for endpoint in endpoints if (endpoint.mpp is not None) is mpp)
+ if not include_actions:
+ endpoints = tuple(endpoint for endpoint in endpoints if not endpoint.action)
+ return [endpoint.to_dict() for endpoint in endpoints[:limit]]
diff --git a/plugins/hermes-tweet/hermes_tweet/catalog_data.json b/plugins/hermes-tweet/hermes_tweet/catalog_data.json
new file mode 100644
index 0000000..1bdff5a
--- /dev/null
+++ b/plugins/hermes-tweet/hermes_tweet/catalog_data.json
@@ -0,0 +1,3849 @@
+[
+ {
+ "action": false,
+ "category": "account",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [],
+ "path": "/api/v1/account",
+ "responseShape": "Account info",
+ "summary": "Get account info"
+ },
+ {
+ "action": true,
+ "category": "composition",
+ "free": true,
+ "method": "POST",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Workflow step, topic, goal, and optional style parameters."
+ }
+ ],
+ "path": "/api/v1/compose",
+ "responseShape": "Composition result",
+ "summary": "Compose, refine, or score a tweet"
+ },
+ {
+ "action": false,
+ "category": "subscribe",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [],
+ "path": "/api/v1/credits",
+ "responseShape": "Credits balance and usage",
+ "summary": "Get credits balance"
+ },
+ {
+ "action": false,
+ "category": "composition",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Maximum number of items to return (1-100, default 50). For paid per-result endpoints, the returned count may be lower when remaining credits cannot cover the requested page. If zero paid results are affordable, the endpoint returns 402 insufficient_credits."
+ },
+ {
+ "name": "afterCursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Cursor for pagination"
+ }
+ ],
+ "path": "/api/v1/drafts",
+ "responseShape": "Draft list",
+ "summary": "List saved drafts"
+ },
+ {
+ "action": true,
+ "category": "composition",
+ "free": true,
+ "method": "POST",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Draft text with optional topic and optimization goal."
+ }
+ ],
+ "path": "/api/v1/drafts",
+ "responseShape": "Draft created",
+ "summary": "Save a tweet draft"
+ },
+ {
+ "action": true,
+ "category": "composition",
+ "free": true,
+ "method": "DELETE",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Resource ID returned by the matching create or list endpoint."
+ }
+ ],
+ "path": "/api/v1/drafts/{id}",
+ "responseShape": "",
+ "summary": "Delete a draft"
+ },
+ {
+ "action": false,
+ "category": "composition",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Resource ID returned by the matching create or list endpoint."
+ }
+ ],
+ "path": "/api/v1/drafts/{id}",
+ "responseShape": "Draft details",
+ "summary": "Get draft by ID"
+ },
+ {
+ "action": false,
+ "category": "draws",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Maximum number of items to return (1-100, default 50). For paid per-result endpoints, the returned count may be lower when remaining credits cannot cover the requested page. If zero paid results are affordable, the endpoint returns 402 insufficient_credits."
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Cursor for keyset pagination from prior response next_cursor"
+ }
+ ],
+ "path": "/api/v1/draws",
+ "responseShape": "Draw list",
+ "summary": "List draws"
+ },
+ {
+ "action": true,
+ "category": "draws",
+ "free": true,
+ "method": "POST",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Tweet URL, winner count, and optional eligibility filters (retweet, follow, keywords, hashtags, account age)."
+ }
+ ],
+ "path": "/api/v1/draws",
+ "responseShape": "Draw completed",
+ "summary": "Run giveaway draw"
+ },
+ {
+ "action": false,
+ "category": "draws",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Draw public ID returned by create and list draw responses."
+ }
+ ],
+ "path": "/api/v1/draws/{id}",
+ "responseShape": "Draw with winners",
+ "summary": "Get draw details"
+ },
+ {
+ "action": false,
+ "category": "draws",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Draw public ID returned by create and list draw responses."
+ },
+ {
+ "name": "format",
+ "in": "query",
+ "required": true,
+ "type": "string",
+ "description": "Export output format"
+ },
+ {
+ "name": "type",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Export winners or all entries"
+ }
+ ],
+ "path": "/api/v1/draws/{id}/export",
+ "responseShape": "Exported draw file",
+ "summary": "Export draw data"
+ },
+ {
+ "action": true,
+ "category": "events",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Maximum number of items to return (1-100, default 50). For paid per-result endpoints, the returned count may be lower when remaining credits cannot cover the requested page. If zero paid results are affordable, the endpoint returns 402 insufficient_credits."
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Cursor for keyset pagination from prior response next_cursor"
+ },
+ {
+ "name": "monitorId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter events by monitor ID"
+ },
+ {
+ "name": "eventType",
+ "in": "query",
+ "required": false,
+ "type": "unknown",
+ "description": "Filter events by type"
+ }
+ ],
+ "path": "/api/v1/events",
+ "responseShape": "Event list",
+ "summary": "List events"
+ },
+ {
+ "action": true,
+ "category": "events",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Resource ID returned by the matching create or list endpoint."
+ }
+ ],
+ "path": "/api/v1/events/{id}",
+ "responseShape": "Event details",
+ "summary": "Get event"
+ },
+ {
+ "action": false,
+ "category": "extractions",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Maximum number of items to return (1-100, default 50). For paid per-result endpoints, the returned count may be lower when remaining credits cannot cover the requested page. If zero paid results are affordable, the endpoint returns 402 insufficient_credits."
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Cursor for keyset pagination from prior response next_cursor"
+ },
+ {
+ "name": "toolType",
+ "in": "query",
+ "required": false,
+ "type": "unknown",
+ "description": "Filter by extraction tool type"
+ },
+ {
+ "name": "status",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter by job status"
+ }
+ ],
+ "path": "/api/v1/extractions",
+ "responseShape": "Extraction job list",
+ "summary": "List extraction jobs"
+ },
+ {
+ "action": true,
+ "category": "extractions",
+ "free": true,
+ "method": "POST",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Tool type and target identifier (tweet, user, community, list, or search query)."
+ }
+ ],
+ "path": "/api/v1/extractions",
+ "responseShape": "Extraction started",
+ "summary": "Run extraction"
+ },
+ {
+ "action": true,
+ "category": "extractions",
+ "free": true,
+ "method": "POST",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Same parameters as a real extraction; returns estimated credit cost without running the job."
+ }
+ ],
+ "path": "/api/v1/extractions/estimate",
+ "responseShape": "Extraction estimate",
+ "summary": "Estimate extraction cost"
+ },
+ {
+ "action": false,
+ "category": "extractions",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Extraction public ID (UUID)"
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Maximum number of results to return (1-1000, default 100)"
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Cursor for keyset pagination from prior response next_cursor"
+ }
+ ],
+ "path": "/api/v1/extractions/{id}",
+ "responseShape": "Extraction job with results",
+ "summary": "Get extraction results"
+ },
+ {
+ "action": false,
+ "category": "extractions",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Extraction public ID"
+ },
+ {
+ "name": "format",
+ "in": "query",
+ "required": true,
+ "type": "string",
+ "description": "Export file format"
+ }
+ ],
+ "path": "/api/v1/extractions/{id}/export",
+ "responseShape": "Exported file",
+ "summary": "Export extraction results"
+ },
+ {
+ "action": false,
+ "category": "monitors",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [],
+ "path": "/api/v1/monitors",
+ "responseShape": "Monitor list",
+ "summary": "List monitors"
+ },
+ {
+ "action": true,
+ "category": "monitors",
+ "free": true,
+ "method": "POST",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Target X username and event types to monitor."
+ }
+ ],
+ "path": "/api/v1/monitors",
+ "responseShape": "Monitor created",
+ "summary": "Create monitor"
+ },
+ {
+ "action": false,
+ "category": "monitors",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [],
+ "path": "/api/v1/monitors/keywords",
+ "responseShape": "Keyword monitor list",
+ "summary": "List keyword monitors"
+ },
+ {
+ "action": true,
+ "category": "monitors",
+ "free": true,
+ "method": "POST",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Search query and event types to monitor."
+ }
+ ],
+ "path": "/api/v1/monitors/keywords",
+ "responseShape": "Keyword monitor created",
+ "summary": "Create keyword monitor"
+ },
+ {
+ "action": true,
+ "category": "monitors",
+ "free": true,
+ "method": "DELETE",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Resource ID returned by the matching create or list endpoint."
+ }
+ ],
+ "path": "/api/v1/monitors/keywords/{id}",
+ "responseShape": "",
+ "summary": "Delete keyword monitor"
+ },
+ {
+ "action": false,
+ "category": "monitors",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Resource ID returned by the matching create or list endpoint."
+ }
+ ],
+ "path": "/api/v1/monitors/keywords/{id}",
+ "responseShape": "Keyword monitor details",
+ "summary": "Get keyword monitor"
+ },
+ {
+ "action": true,
+ "category": "monitors",
+ "free": true,
+ "method": "PATCH",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Resource ID returned by the matching create or list endpoint."
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Updated event types or active status."
+ }
+ ],
+ "path": "/api/v1/monitors/keywords/{id}",
+ "responseShape": "Keyword monitor updated",
+ "summary": "Update keyword monitor"
+ },
+ {
+ "action": true,
+ "category": "monitors",
+ "free": true,
+ "method": "DELETE",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Resource ID returned by the matching create or list endpoint."
+ }
+ ],
+ "path": "/api/v1/monitors/{id}",
+ "responseShape": "",
+ "summary": "Delete monitor"
+ },
+ {
+ "action": false,
+ "category": "monitors",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Resource ID returned by the matching create or list endpoint."
+ }
+ ],
+ "path": "/api/v1/monitors/{id}",
+ "responseShape": "Monitor details",
+ "summary": "Get monitor"
+ },
+ {
+ "action": true,
+ "category": "monitors",
+ "free": true,
+ "method": "PATCH",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Resource ID returned by the matching create or list endpoint."
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Event type list or active state for the account monitor."
+ }
+ ],
+ "path": "/api/v1/monitors/{id}",
+ "responseShape": "Monitor updated",
+ "summary": "Update monitor"
+ },
+ {
+ "action": false,
+ "category": "composition",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "after",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Cursor for pagination (from prior response nextCursor)."
+ },
+ {
+ "name": "category",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter by category."
+ },
+ {
+ "name": "hours",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Lookback window in hours (1-72, default 6)."
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Number of items to return (1-100, default 50)."
+ },
+ {
+ "name": "region",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Region filter. Use `global` or a region code such as `US`, `GB`, `TR`, or `ES`."
+ },
+ {
+ "name": "source",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Source filter. One of: github, google_trends, hacker_news, polymarket, reddit, trustmrr, wikipedia"
+ }
+ ],
+ "path": "/api/v1/radar",
+ "responseShape": "Radar items",
+ "summary": "Get trending topics from curated sources"
+ },
+ {
+ "action": false,
+ "category": "composition",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [],
+ "path": "/api/v1/styles",
+ "responseShape": "Style profile list",
+ "summary": "List cached style profiles"
+ },
+ {
+ "action": true,
+ "category": "composition",
+ "free": true,
+ "method": "POST",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "X username whose recent tweets define the style."
+ }
+ ],
+ "path": "/api/v1/styles",
+ "responseShape": "Fresh cached style profile returned",
+ "summary": "Analyze writing style from recent tweets"
+ },
+ {
+ "action": false,
+ "category": "composition",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "username1",
+ "in": "query",
+ "required": true,
+ "type": "string",
+ "description": "First username to compare"
+ },
+ {
+ "name": "username2",
+ "in": "query",
+ "required": true,
+ "type": "string",
+ "description": "Second username to compare"
+ }
+ ],
+ "path": "/api/v1/styles/compare",
+ "responseShape": "Style comparison",
+ "summary": "Compare two style profiles"
+ },
+ {
+ "action": true,
+ "category": "composition",
+ "free": true,
+ "method": "DELETE",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Style profile ID or X username"
+ }
+ ],
+ "path": "/api/v1/styles/{id}",
+ "responseShape": "",
+ "summary": "Delete a style profile"
+ },
+ {
+ "action": false,
+ "category": "composition",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Style profile ID or X username"
+ }
+ ],
+ "path": "/api/v1/styles/{id}",
+ "responseShape": "Style profile",
+ "summary": "Get cached style profile"
+ },
+ {
+ "action": true,
+ "category": "composition",
+ "free": true,
+ "method": "PUT",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Style profile ID or X username"
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Label and sample tweets that define the style profile."
+ }
+ ],
+ "path": "/api/v1/styles/{id}",
+ "responseShape": "Style profile saved",
+ "summary": "Save style profile with custom tweets"
+ },
+ {
+ "action": false,
+ "category": "composition",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Style profile ID or X username"
+ }
+ ],
+ "path": "/api/v1/styles/{id}/performance",
+ "responseShape": "Performance metrics",
+ "summary": "Get engagement metrics for style tweets"
+ },
+ {
+ "action": false,
+ "category": "trends",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "charge",
+ "price": "$0.00045/call"
+ },
+ "parameters": [
+ {
+ "name": "woeid",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Region Yahoo WOEID code (1=Worldwide, 23424977=US, 23424975=UK, 23424969=Turkey)"
+ },
+ {
+ "name": "count",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Number of trending topics returned (1-50, default 30)"
+ }
+ ],
+ "path": "/api/v1/trends",
+ "responseShape": "Trending topics",
+ "summary": "Get trending hashtags and topics by region (alias)"
+ },
+ {
+ "action": true,
+ "category": "webhooks",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [],
+ "path": "/api/v1/webhooks",
+ "responseShape": "Webhook list",
+ "summary": "List webhooks"
+ },
+ {
+ "action": true,
+ "category": "webhooks",
+ "free": true,
+ "method": "POST",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "HTTPS callback URL and event types to subscribe to."
+ }
+ ],
+ "path": "/api/v1/webhooks",
+ "responseShape": "Webhook created",
+ "summary": "Create webhook"
+ },
+ {
+ "action": true,
+ "category": "webhooks",
+ "free": true,
+ "method": "DELETE",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Resource ID returned by the matching create or list endpoint."
+ }
+ ],
+ "path": "/api/v1/webhooks/{id}",
+ "responseShape": "",
+ "summary": "Deactivate webhook"
+ },
+ {
+ "action": true,
+ "category": "webhooks",
+ "free": true,
+ "method": "PATCH",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Resource ID returned by the matching create or list endpoint."
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Updated URL, event types, or active status."
+ }
+ ],
+ "path": "/api/v1/webhooks/{id}",
+ "responseShape": "Webhook updated",
+ "summary": "Update webhook"
+ },
+ {
+ "action": true,
+ "category": "webhooks",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Resource ID returned by the matching create or list endpoint."
+ }
+ ],
+ "path": "/api/v1/webhooks/{id}/deliveries",
+ "responseShape": "Delivery list",
+ "summary": "List webhook deliveries"
+ },
+ {
+ "action": true,
+ "category": "webhooks",
+ "free": true,
+ "method": "POST",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Resource ID returned by the matching create or list endpoint."
+ }
+ ],
+ "path": "/api/v1/webhooks/{id}/test",
+ "responseShape": "Test result",
+ "summary": "Test webhook endpoint"
+ },
+ {
+ "action": true,
+ "category": "x-accounts",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [],
+ "path": "/api/v1/x/accounts",
+ "responseShape": "X account list",
+ "summary": "List connected X accounts"
+ },
+ {
+ "action": false,
+ "category": "articles",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "charge",
+ "price": "$0.00105/call"
+ },
+ "parameters": [
+ {
+ "name": "tweetId",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Numeric tweet ID of the article, 15-20 digits. If you have a tweet URL, use the final status ID."
+ }
+ ],
+ "path": "/api/v1/x/articles/{tweetId}",
+ "responseShape": "Article with author",
+ "summary": "Get full X Article content with cover image and metadata"
+ },
+ {
+ "action": true,
+ "category": "tweets",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "folderId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Optional bookmark folder ID"
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor for bookmarks"
+ }
+ ],
+ "path": "/api/v1/x/bookmarks",
+ "responseShape": "List of bookmarked tweets",
+ "summary": "Get bookmarked tweets"
+ },
+ {
+ "action": true,
+ "category": "tweets",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [],
+ "path": "/api/v1/x/bookmarks/folders",
+ "responseShape": "List of bookmark folders",
+ "summary": "Get bookmark folders"
+ },
+ {
+ "action": true,
+ "category": "x-write",
+ "free": true,
+ "method": "POST",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Account, community name, and optional description."
+ }
+ ],
+ "path": "/api/v1/x/communities",
+ "responseShape": "Community created",
+ "summary": "Create community"
+ },
+ {
+ "action": false,
+ "category": "communities",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "session",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "q",
+ "in": "query",
+ "required": true,
+ "type": "string",
+ "description": "Search query"
+ },
+ {
+ "name": "queryType",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Sort order (Latest or Top)"
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor for community search"
+ }
+ ],
+ "path": "/api/v1/x/communities/search",
+ "responseShape": "Community search results",
+ "summary": "Search for communities by keyword"
+ },
+ {
+ "action": false,
+ "category": "communities",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "session",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "q",
+ "in": "query",
+ "required": true,
+ "type": "string",
+ "description": "Search query for cross-community tweets"
+ },
+ {
+ "name": "queryType",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Sort order for cross-community results (Latest or Top)"
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor for cross-community results"
+ }
+ ],
+ "path": "/api/v1/x/communities/tweets",
+ "responseShape": "Paginated list of tweets from all communities",
+ "summary": "List tweets across all communities"
+ },
+ {
+ "action": true,
+ "category": "x-write",
+ "free": true,
+ "method": "DELETE",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Resource ID returned by the matching create or list endpoint."
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Account and community name for deletion confirmation."
+ }
+ ],
+ "path": "/api/v1/x/communities/{id}",
+ "responseShape": "",
+ "summary": "Delete community"
+ },
+ {
+ "action": false,
+ "category": "communities",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "charge",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Community ID"
+ }
+ ],
+ "path": "/api/v1/x/communities/{id}/info",
+ "responseShape": "Community details",
+ "summary": "Get community name, description and member count"
+ },
+ {
+ "action": true,
+ "category": "x-write",
+ "free": true,
+ "method": "DELETE",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Resource ID returned by the matching create or list endpoint."
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Account to leave the community."
+ }
+ ],
+ "path": "/api/v1/x/communities/{id}/join",
+ "responseShape": "",
+ "summary": "Leave community"
+ },
+ {
+ "action": true,
+ "category": "x-write",
+ "free": true,
+ "method": "POST",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Resource ID returned by the matching create or list endpoint."
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Account to join the community."
+ }
+ ],
+ "path": "/api/v1/x/communities/{id}/join",
+ "responseShape": "",
+ "summary": "Join community"
+ },
+ {
+ "action": false,
+ "category": "communities",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "session",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Community ID for member lookup"
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor"
+ },
+ {
+ "name": "pageSize",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Items per page (20-200, default 20). This is an upper bound for paid authenticated calls: remaining credits can reduce the returned page size, and zero affordable results returns 402 insufficient_credits."
+ }
+ ],
+ "path": "/api/v1/x/communities/{id}/members",
+ "responseShape": "List of community members",
+ "summary": "List members of a community"
+ },
+ {
+ "action": false,
+ "category": "communities",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "session",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Community ID for moderator lookup"
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor for community moderators"
+ }
+ ],
+ "path": "/api/v1/x/communities/{id}/moderators",
+ "responseShape": "List of community moderators",
+ "summary": "List moderators of a community"
+ },
+ {
+ "action": false,
+ "category": "communities",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "session",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Community ID for tweet lookup"
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor for community tweets"
+ }
+ ],
+ "path": "/api/v1/x/communities/{id}/tweets",
+ "responseShape": "List of community tweets",
+ "summary": "List tweets posted in a community"
+ },
+ {
+ "action": true,
+ "category": "x-write",
+ "free": true,
+ "method": "POST",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "userId",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Recipient user ID"
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Account, message text, and an optional media attachment."
+ }
+ ],
+ "path": "/api/v1/x/dm/{userId}",
+ "responseShape": "DM sent",
+ "summary": "Send direct message"
+ },
+ {
+ "action": true,
+ "category": "users",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "userId",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Target user ID"
+ },
+ {
+ "name": "account",
+ "in": "query",
+ "required": true,
+ "type": "string",
+ "description": "X handle (without the `@` prefix) of the connected X account used to read the conversation. The account must be a participant in the conversation."
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor for DM history"
+ },
+ {
+ "name": "maxId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Legacy pagination cursor (backward compat)"
+ }
+ ],
+ "path": "/api/v1/x/dm/{userId}/history",
+ "responseShape": "List of DM messages",
+ "summary": "Get DM conversation history"
+ },
+ {
+ "action": false,
+ "category": "users",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "charge",
+ "price": "$0.00105/call"
+ },
+ "parameters": [
+ {
+ "name": "source",
+ "in": "query",
+ "required": true,
+ "type": "string",
+ "description": "Username to check (without @)"
+ },
+ {
+ "name": "target",
+ "in": "query",
+ "required": true,
+ "type": "string",
+ "description": "Target username (without @)"
+ }
+ ],
+ "path": "/api/v1/x/followers/check",
+ "responseShape": "Follow check result",
+ "summary": "Check if one user follows another"
+ },
+ {
+ "action": false,
+ "category": "lists",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "session",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "List ID"
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor for list followers"
+ }
+ ],
+ "path": "/api/v1/x/lists/{id}/followers",
+ "responseShape": "List of followers",
+ "summary": "List followers of an X List"
+ },
+ {
+ "action": false,
+ "category": "lists",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "session",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "List ID for member lookup"
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor for list members"
+ },
+ {
+ "name": "pageSize",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Members per page (20-200, default 20)"
+ }
+ ],
+ "path": "/api/v1/x/lists/{id}/members",
+ "responseShape": "List of members",
+ "summary": "List members of an X List"
+ },
+ {
+ "action": false,
+ "category": "lists",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "session",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "List ID for tweet lookup"
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor for list tweets"
+ },
+ {
+ "name": "sinceTime",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Unix timestamp - filter after"
+ },
+ {
+ "name": "untilTime",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Unix timestamp - filter before"
+ },
+ {
+ "name": "includeReplies",
+ "in": "query",
+ "required": false,
+ "type": "boolean",
+ "description": "Include replies (default false)"
+ }
+ ],
+ "path": "/api/v1/x/lists/{id}/tweets",
+ "responseShape": "List tweets",
+ "summary": "List tweets from an X List"
+ },
+ {
+ "action": true,
+ "category": "media",
+ "free": true,
+ "method": "POST",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Upload media with multipart form data, or provide a JSON URL for server-side download."
+ }
+ ],
+ "path": "/api/v1/x/media",
+ "responseShape": "Media uploaded",
+ "summary": "Upload media"
+ },
+ {
+ "action": true,
+ "category": "media",
+ "free": false,
+ "method": "POST",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Single tweet URL/ID, accepted aliases, or array of up to 50 tweet URLs/IDs for bulk download. When `tweetIds` contains at least one string value, bulk mode is used."
+ }
+ ],
+ "path": "/api/v1/x/media/download",
+ "responseShape": "Media download result. Single: tweetId + galleryUrl + cacheHit. Bulk: galleryUrl + totalTweets + totalMedia.",
+ "summary": "Download images and videos from tweets"
+ },
+ {
+ "action": true,
+ "category": "users",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "type",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Notification type filter. Unrecognized values fall back to All."
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor for notifications"
+ }
+ ],
+ "path": "/api/v1/x/notifications",
+ "responseShape": "List of notifications",
+ "summary": "Get notifications"
+ },
+ {
+ "action": true,
+ "category": "x-write",
+ "free": true,
+ "method": "PATCH",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Account and profile fields to update (name, bio, location, website)."
+ }
+ ],
+ "path": "/api/v1/x/profile",
+ "responseShape": "",
+ "summary": "Update X profile"
+ },
+ {
+ "action": true,
+ "category": "x-write",
+ "free": true,
+ "method": "PATCH",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Account and avatar image file or HTTPS image URL (max 700 KB)."
+ }
+ ],
+ "path": "/api/v1/x/profile/avatar",
+ "responseShape": "",
+ "summary": "Update profile avatar"
+ },
+ {
+ "action": true,
+ "category": "x-write",
+ "free": true,
+ "method": "PATCH",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Account and banner image file or HTTPS image URL (max 2 MB)."
+ }
+ ],
+ "path": "/api/v1/x/profile/banner",
+ "responseShape": "",
+ "summary": "Update profile banner"
+ },
+ {
+ "action": true,
+ "category": "tweets",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "seenTweetIds",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Comma-separated tweet IDs to exclude from results. Empty entries are ignored."
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor for timeline"
+ }
+ ],
+ "path": "/api/v1/x/timeline",
+ "responseShape": "List of timeline tweets",
+ "summary": "Get home timeline"
+ },
+ {
+ "action": false,
+ "category": "trends",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "charge",
+ "price": "$0.00045/call"
+ },
+ "parameters": [
+ {
+ "name": "woeid",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Region WOEID (1=Worldwide, 23424977=US, 23424975=UK, 23424969=Turkey)"
+ },
+ {
+ "name": "count",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Number of trending topics to return (1-50, default 30)"
+ }
+ ],
+ "path": "/api/v1/x/trends",
+ "responseShape": "List of trending topics",
+ "summary": "Get trending hashtags and topics from X by region"
+ },
+ {
+ "action": false,
+ "category": "tweets",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "ids",
+ "in": "query",
+ "required": true,
+ "type": "string",
+ "description": "Comma-separated tweet IDs (max 100)"
+ }
+ ],
+ "path": "/api/v1/x/tweets",
+ "responseShape": "List of tweets",
+ "summary": "Get multiple tweets by IDs"
+ },
+ {
+ "action": true,
+ "category": "x-write",
+ "free": true,
+ "method": "POST",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Account and tweet content. Requires text, media, or both."
+ }
+ ],
+ "path": "/api/v1/x/tweets",
+ "responseShape": "Tweet created",
+ "summary": "Create tweet"
+ },
+ {
+ "action": false,
+ "category": "tweets",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "session",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "q",
+ "in": "query",
+ "required": true,
+ "type": "string",
+ "description": "Search query (keywords,"
+ },
+ {
+ "name": "queryType",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Sort order - Latest (chronological) or Top (engagement-ranked)"
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor from previous response"
+ },
+ {
+ "name": "sinceTime",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "ISO 8601 timestamp - only return tweets after this time"
+ },
+ {
+ "name": "untilTime",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "ISO 8601 timestamp - only return tweets before this time"
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Max tweets to return (server paginates internally). Omit for single page (~20). This is an upper bound for paid authenticated calls: remaining credits can reduce the returned page size, and zero affordable results returns 402 insufficient_credits."
+ },
+ {
+ "name": "fromUser",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter by author username."
+ },
+ {
+ "name": "toUser",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter replies sent to a username."
+ },
+ {
+ "name": "mentioning",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter tweets mentioning a username."
+ },
+ {
+ "name": "language",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Language code filter, e.g. en or tr."
+ },
+ {
+ "name": "sinceDate",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Start date in YYYY-MM-DD format."
+ },
+ {
+ "name": "untilDate",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "End date in YYYY-MM-DD format."
+ },
+ {
+ "name": "mediaType",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter by media type."
+ },
+ {
+ "name": "minFaves",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum likes threshold."
+ },
+ {
+ "name": "minRetweets",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum retweets threshold."
+ },
+ {
+ "name": "minReplies",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum replies threshold."
+ },
+ {
+ "name": "minQuotes",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum quote count threshold."
+ },
+ {
+ "name": "verifiedOnly",
+ "in": "query",
+ "required": false,
+ "type": "boolean",
+ "description": "Only return tweets from verified authors."
+ },
+ {
+ "name": "replies",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Reply mode."
+ },
+ {
+ "name": "retweets",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Retweet mode."
+ },
+ {
+ "name": "quotes",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Quote mode."
+ },
+ {
+ "name": "exactPhrase",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Exact phrase to match."
+ },
+ {
+ "name": "excludeWords",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Words or quoted phrases to exclude. Separate with spaces, commas, or lines."
+ },
+ {
+ "name": "anyWords",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Words or quoted phrases where any one can match. Separate with spaces, commas, or lines."
+ },
+ {
+ "name": "hashtags",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Hashtags separated by spaces, commas, or lines."
+ },
+ {
+ "name": "cashtags",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Cashtags separated by spaces, commas, or lines."
+ },
+ {
+ "name": "url",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "URL substring or domain filter."
+ },
+ {
+ "name": "conversationId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Conversation ID filter."
+ },
+ {
+ "name": "inReplyToTweetId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Only replies to this tweet ID."
+ },
+ {
+ "name": "quotesOfTweetId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Only quotes of this tweet ID."
+ },
+ {
+ "name": "retweetsOfTweetId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Only retweets of this tweet ID."
+ },
+ {
+ "name": "listId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Search within a list ID."
+ },
+ {
+ "name": "place",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Search within a place ID."
+ },
+ {
+ "name": "placeCountry",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Search within a country code."
+ },
+ {
+ "name": "pointRadius",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Geo point radius, e.g. -73.99 40.73 25mi."
+ },
+ {
+ "name": "boundingBox",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Geo bounding box, e.g. -74.1 40.6 -73.9 40.8."
+ },
+ {
+ "name": "advancedQuery",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Raw advanced search query appended as-is."
+ }
+ ],
+ "path": "/api/v1/x/tweets/search",
+ "responseShape": "Search results",
+ "summary": "Search tweets by query, Tweet ID, X status URL, or account date window"
+ },
+ {
+ "action": true,
+ "category": "x-write",
+ "free": true,
+ "method": "DELETE",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Tweet ID to delete"
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Account that owns the tweet."
+ }
+ ],
+ "path": "/api/v1/x/tweets/{id}",
+ "responseShape": "",
+ "summary": "Delete tweet"
+ },
+ {
+ "action": false,
+ "category": "tweets",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "charge",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Tweet ID"
+ }
+ ],
+ "path": "/api/v1/x/tweets/{id}",
+ "responseShape": "Tweet with author",
+ "summary": "Get tweet with full text, author, metrics and media"
+ },
+ {
+ "action": false,
+ "category": "tweets",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "session",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Tweet ID to get favoriters"
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor for favoriters"
+ }
+ ],
+ "path": "/api/v1/x/tweets/{id}/favoriters",
+ "responseShape": "List of users who liked the tweet",
+ "summary": "List users who liked a tweet"
+ },
+ {
+ "action": true,
+ "category": "x-write",
+ "free": true,
+ "method": "DELETE",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Tweet ID to unlike"
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Account that liked the tweet."
+ }
+ ],
+ "path": "/api/v1/x/tweets/{id}/like",
+ "responseShape": "",
+ "summary": "Unlike tweet"
+ },
+ {
+ "action": true,
+ "category": "x-write",
+ "free": true,
+ "method": "POST",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Tweet ID to like"
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Account to perform the like."
+ }
+ ],
+ "path": "/api/v1/x/tweets/{id}/like",
+ "responseShape": "",
+ "summary": "Like tweet"
+ },
+ {
+ "action": false,
+ "category": "tweets",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "session",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Tweet ID to get quotes"
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor for quote tweets"
+ },
+ {
+ "name": "sinceTime",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Unix timestamp - return quotes posted after this time"
+ },
+ {
+ "name": "untilTime",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Unix timestamp - return quotes posted before this time"
+ },
+ {
+ "name": "includeReplies",
+ "in": "query",
+ "required": false,
+ "type": "boolean",
+ "description": "Include reply quotes (default false)"
+ },
+ {
+ "name": "fromUser",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter by author username."
+ },
+ {
+ "name": "toUser",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter replies sent to a username."
+ },
+ {
+ "name": "mentioning",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter tweets mentioning a username."
+ },
+ {
+ "name": "language",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Language code filter, e.g. en or tr."
+ },
+ {
+ "name": "sinceDate",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Start date in YYYY-MM-DD format."
+ },
+ {
+ "name": "untilDate",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "End date in YYYY-MM-DD format."
+ },
+ {
+ "name": "mediaType",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter by media type."
+ },
+ {
+ "name": "minFaves",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum likes threshold."
+ },
+ {
+ "name": "minRetweets",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum retweets threshold."
+ },
+ {
+ "name": "minReplies",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum replies threshold."
+ },
+ {
+ "name": "minQuotes",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum quote count threshold."
+ },
+ {
+ "name": "verifiedOnly",
+ "in": "query",
+ "required": false,
+ "type": "boolean",
+ "description": "Only return tweets from verified authors."
+ },
+ {
+ "name": "replies",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Reply mode."
+ },
+ {
+ "name": "retweets",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Retweet mode."
+ },
+ {
+ "name": "quotes",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Quote mode."
+ },
+ {
+ "name": "exactPhrase",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Exact phrase to match."
+ },
+ {
+ "name": "excludeWords",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Words or quoted phrases to exclude. Separate with spaces, commas, or lines."
+ },
+ {
+ "name": "anyWords",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Words or quoted phrases where any one can match. Separate with spaces, commas, or lines."
+ },
+ {
+ "name": "hashtags",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Hashtags separated by spaces, commas, or lines."
+ },
+ {
+ "name": "cashtags",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Cashtags separated by spaces, commas, or lines."
+ },
+ {
+ "name": "url",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "URL substring or domain filter."
+ },
+ {
+ "name": "conversationId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Conversation ID filter."
+ },
+ {
+ "name": "inReplyToTweetId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Only replies to this tweet ID."
+ },
+ {
+ "name": "quotesOfTweetId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Only quotes of this tweet ID."
+ },
+ {
+ "name": "retweetsOfTweetId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Only retweets of this tweet ID."
+ }
+ ],
+ "path": "/api/v1/x/tweets/{id}/quotes",
+ "responseShape": "List of quote tweets",
+ "summary": "List quote tweets of a tweet"
+ },
+ {
+ "action": false,
+ "category": "tweets",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "session",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Tweet ID to get replies"
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor for tweet replies"
+ },
+ {
+ "name": "sinceTime",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Unix timestamp - return replies posted after this time"
+ },
+ {
+ "name": "untilTime",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Unix timestamp - return replies posted before this time"
+ },
+ {
+ "name": "fromUser",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter by author username."
+ },
+ {
+ "name": "toUser",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter replies sent to a username."
+ },
+ {
+ "name": "mentioning",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter tweets mentioning a username."
+ },
+ {
+ "name": "language",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Language code filter, e.g. en or tr."
+ },
+ {
+ "name": "sinceDate",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Start date in YYYY-MM-DD format."
+ },
+ {
+ "name": "untilDate",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "End date in YYYY-MM-DD format."
+ },
+ {
+ "name": "mediaType",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter by media type."
+ },
+ {
+ "name": "minFaves",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum likes threshold."
+ },
+ {
+ "name": "minRetweets",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum retweets threshold."
+ },
+ {
+ "name": "minReplies",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum replies threshold."
+ },
+ {
+ "name": "minQuotes",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum quote count threshold."
+ },
+ {
+ "name": "verifiedOnly",
+ "in": "query",
+ "required": false,
+ "type": "boolean",
+ "description": "Only return tweets from verified authors."
+ },
+ {
+ "name": "replies",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Reply mode."
+ },
+ {
+ "name": "retweets",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Retweet mode."
+ },
+ {
+ "name": "quotes",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Quote mode."
+ },
+ {
+ "name": "exactPhrase",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Exact phrase to match."
+ },
+ {
+ "name": "excludeWords",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Words or quoted phrases to exclude. Separate with spaces, commas, or lines."
+ },
+ {
+ "name": "anyWords",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Words or quoted phrases where any one can match. Separate with spaces, commas, or lines."
+ },
+ {
+ "name": "hashtags",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Hashtags separated by spaces, commas, or lines."
+ },
+ {
+ "name": "cashtags",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Cashtags separated by spaces, commas, or lines."
+ },
+ {
+ "name": "url",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "URL substring or domain filter."
+ },
+ {
+ "name": "conversationId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Conversation ID filter."
+ },
+ {
+ "name": "inReplyToTweetId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Only replies to this tweet ID."
+ },
+ {
+ "name": "quotesOfTweetId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Only quotes of this tweet ID."
+ },
+ {
+ "name": "retweetsOfTweetId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Only retweets of this tweet ID."
+ }
+ ],
+ "path": "/api/v1/x/tweets/{id}/replies",
+ "responseShape": "List of replies",
+ "summary": "List replies to a tweet"
+ },
+ {
+ "action": true,
+ "category": "x-write",
+ "free": true,
+ "method": "DELETE",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Tweet ID to unretweet"
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Account that retweeted the tweet."
+ }
+ ],
+ "path": "/api/v1/x/tweets/{id}/retweet",
+ "responseShape": "",
+ "summary": "Unretweet"
+ },
+ {
+ "action": true,
+ "category": "x-write",
+ "free": true,
+ "method": "POST",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Tweet ID to retweet"
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Account to perform the retweet."
+ }
+ ],
+ "path": "/api/v1/x/tweets/{id}/retweet",
+ "responseShape": "",
+ "summary": "Retweet"
+ },
+ {
+ "action": false,
+ "category": "tweets",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "session",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Tweet ID to get retweeters"
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor for retweeters"
+ }
+ ],
+ "path": "/api/v1/x/tweets/{id}/retweeters",
+ "responseShape": "List of retweeters",
+ "summary": "List users who retweeted a tweet"
+ },
+ {
+ "action": false,
+ "category": "tweets",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "session",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Tweet ID to get thread context"
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor for thread tweets"
+ }
+ ],
+ "path": "/api/v1/x/tweets/{id}/thread",
+ "responseShape": "Thread tweets",
+ "summary": "Get full conversation thread for a tweet"
+ },
+ {
+ "action": false,
+ "category": "users",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "session",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "ids",
+ "in": "query",
+ "required": true,
+ "type": "string",
+ "description": "Comma-separated user IDs (max 100)"
+ }
+ ],
+ "path": "/api/v1/x/users/batch",
+ "responseShape": "List of users",
+ "summary": "Look up multiple users by IDs in one call"
+ },
+ {
+ "action": false,
+ "category": "users",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "session",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "q",
+ "in": "query",
+ "required": true,
+ "type": "string",
+ "description": "User search query"
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor for user search"
+ }
+ ],
+ "path": "/api/v1/x/users/search",
+ "responseShape": "User search results",
+ "summary": "Search users by name or username"
+ },
+ {
+ "action": false,
+ "category": "users",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "charge",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "X username (without @) or user ID"
+ }
+ ],
+ "path": "/api/v1/x/users/{id}",
+ "responseShape": "User profile",
+ "summary": "Get user profile with follower counts and verification"
+ },
+ {
+ "action": true,
+ "category": "x-write",
+ "free": true,
+ "method": "DELETE",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "User ID to unfollow"
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Account that follows the target user."
+ }
+ ],
+ "path": "/api/v1/x/users/{id}/follow",
+ "responseShape": "",
+ "summary": "Unfollow user"
+ },
+ {
+ "action": true,
+ "category": "x-write",
+ "free": true,
+ "method": "POST",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "User ID to follow"
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Account to perform the follow."
+ }
+ ],
+ "path": "/api/v1/x/users/{id}/follow",
+ "responseShape": "",
+ "summary": "Follow user"
+ },
+ {
+ "action": false,
+ "category": "users",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "session",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "User ID or username"
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor for followers list"
+ },
+ {
+ "name": "after",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Legacy cursor alias for following lists. Prefer cursor."
+ },
+ {
+ "name": "pageSize",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Items per page (20-200, default 200). This is an upper bound for paid authenticated calls: remaining credits can reduce the returned page size, and zero affordable results returns 402 insufficient_credits."
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Legacy integer page size alias for following lists. Prefer pageSize."
+ }
+ ],
+ "path": "/api/v1/x/users/{id}/followers",
+ "responseShape": "List of user followers",
+ "summary": "List followers of a user"
+ },
+ {
+ "action": false,
+ "category": "users",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "session",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "User ID for followers-you-know lookup"
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor for followers-you-know"
+ }
+ ],
+ "path": "/api/v1/x/users/{id}/followers-you-know",
+ "responseShape": "List of mutual followers",
+ "summary": "List mutual followers between you and a user"
+ },
+ {
+ "action": false,
+ "category": "users",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "session",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "User ID or username for following lookup"
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor for following list"
+ },
+ {
+ "name": "after",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Legacy cursor alias. Prefer cursor."
+ },
+ {
+ "name": "pageSize",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Results per page (20-200, default 200). This is an upper bound for paid authenticated calls: remaining credits can reduce the returned page size, and zero affordable results returns 402 insufficient_credits."
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Legacy page size alias. Prefer pageSize."
+ }
+ ],
+ "path": "/api/v1/x/users/{id}/following",
+ "responseShape": "List of following",
+ "summary": "List accounts a user follows"
+ },
+ {
+ "action": false,
+ "category": "users",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "session",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "User ID"
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor for liked tweets"
+ },
+ {
+ "name": "fromUser",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter by author username."
+ },
+ {
+ "name": "toUser",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter replies sent to a username."
+ },
+ {
+ "name": "mentioning",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter tweets mentioning a username."
+ },
+ {
+ "name": "language",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Language code filter, e.g. en or tr."
+ },
+ {
+ "name": "sinceDate",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Start date in YYYY-MM-DD format."
+ },
+ {
+ "name": "untilDate",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "End date in YYYY-MM-DD format."
+ },
+ {
+ "name": "mediaType",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter by media type."
+ },
+ {
+ "name": "minFaves",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum likes threshold."
+ },
+ {
+ "name": "minRetweets",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum retweets threshold."
+ },
+ {
+ "name": "minReplies",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum replies threshold."
+ },
+ {
+ "name": "minQuotes",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum quote count threshold."
+ },
+ {
+ "name": "verifiedOnly",
+ "in": "query",
+ "required": false,
+ "type": "boolean",
+ "description": "Only return tweets from verified authors."
+ },
+ {
+ "name": "replies",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Reply mode."
+ },
+ {
+ "name": "retweets",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Retweet mode."
+ },
+ {
+ "name": "quotes",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Quote mode."
+ },
+ {
+ "name": "exactPhrase",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Exact phrase to match."
+ },
+ {
+ "name": "excludeWords",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Words or quoted phrases to exclude. Separate with spaces, commas, or lines."
+ },
+ {
+ "name": "anyWords",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Words or quoted phrases where any one can match. Separate with spaces, commas, or lines."
+ },
+ {
+ "name": "hashtags",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Hashtags separated by spaces, commas, or lines."
+ },
+ {
+ "name": "cashtags",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Cashtags separated by spaces, commas, or lines."
+ },
+ {
+ "name": "url",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "URL substring or domain filter."
+ },
+ {
+ "name": "conversationId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Conversation ID filter."
+ },
+ {
+ "name": "inReplyToTweetId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Only replies to this tweet ID."
+ },
+ {
+ "name": "quotesOfTweetId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Only quotes of this tweet ID."
+ },
+ {
+ "name": "retweetsOfTweetId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Only retweets of this tweet ID."
+ }
+ ],
+ "path": "/api/v1/x/users/{id}/likes",
+ "responseShape": "List of liked tweets",
+ "summary": "List tweets liked by a user"
+ },
+ {
+ "action": false,
+ "category": "users",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "session",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "User ID for media lookup"
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor for media tweets"
+ },
+ {
+ "name": "fromUser",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter by author username."
+ },
+ {
+ "name": "toUser",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter replies sent to a username."
+ },
+ {
+ "name": "mentioning",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter tweets mentioning a username."
+ },
+ {
+ "name": "language",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Language code filter, e.g. en or tr."
+ },
+ {
+ "name": "sinceDate",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Start date in YYYY-MM-DD format."
+ },
+ {
+ "name": "untilDate",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "End date in YYYY-MM-DD format."
+ },
+ {
+ "name": "mediaType",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter by media type."
+ },
+ {
+ "name": "minFaves",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum likes threshold."
+ },
+ {
+ "name": "minRetweets",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum retweets threshold."
+ },
+ {
+ "name": "minReplies",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum replies threshold."
+ },
+ {
+ "name": "minQuotes",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum quote count threshold."
+ },
+ {
+ "name": "verifiedOnly",
+ "in": "query",
+ "required": false,
+ "type": "boolean",
+ "description": "Only return tweets from verified authors."
+ },
+ {
+ "name": "replies",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Reply mode."
+ },
+ {
+ "name": "retweets",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Retweet mode."
+ },
+ {
+ "name": "quotes",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Quote mode."
+ },
+ {
+ "name": "exactPhrase",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Exact phrase to match."
+ },
+ {
+ "name": "excludeWords",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Words or quoted phrases to exclude. Separate with spaces, commas, or lines."
+ },
+ {
+ "name": "anyWords",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Words or quoted phrases where any one can match. Separate with spaces, commas, or lines."
+ },
+ {
+ "name": "hashtags",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Hashtags separated by spaces, commas, or lines."
+ },
+ {
+ "name": "cashtags",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Cashtags separated by spaces, commas, or lines."
+ },
+ {
+ "name": "url",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "URL substring or domain filter."
+ },
+ {
+ "name": "conversationId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Conversation ID filter."
+ },
+ {
+ "name": "inReplyToTweetId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Only replies to this tweet ID."
+ },
+ {
+ "name": "quotesOfTweetId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Only quotes of this tweet ID."
+ },
+ {
+ "name": "retweetsOfTweetId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Only retweets of this tweet ID."
+ }
+ ],
+ "path": "/api/v1/x/users/{id}/media",
+ "responseShape": "List of media tweets",
+ "summary": "List media tweets posted by a user"
+ },
+ {
+ "action": false,
+ "category": "users",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "session",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "User ID or username for mentions lookup"
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor for mentions"
+ },
+ {
+ "name": "sinceTime",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Unix timestamp - return mentions after this time"
+ },
+ {
+ "name": "untilTime",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Unix timestamp - return mentions before this time"
+ },
+ {
+ "name": "fromUser",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter by author username."
+ },
+ {
+ "name": "toUser",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter replies sent to a username."
+ },
+ {
+ "name": "mentioning",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter tweets mentioning a username."
+ },
+ {
+ "name": "language",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Language code filter, e.g. en or tr."
+ },
+ {
+ "name": "sinceDate",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Start date in YYYY-MM-DD format."
+ },
+ {
+ "name": "untilDate",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "End date in YYYY-MM-DD format."
+ },
+ {
+ "name": "mediaType",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter by media type."
+ },
+ {
+ "name": "minFaves",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum likes threshold."
+ },
+ {
+ "name": "minRetweets",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum retweets threshold."
+ },
+ {
+ "name": "minReplies",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum replies threshold."
+ },
+ {
+ "name": "minQuotes",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum quote count threshold."
+ },
+ {
+ "name": "verifiedOnly",
+ "in": "query",
+ "required": false,
+ "type": "boolean",
+ "description": "Only return tweets from verified authors."
+ },
+ {
+ "name": "replies",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Reply mode."
+ },
+ {
+ "name": "retweets",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Retweet mode."
+ },
+ {
+ "name": "quotes",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Quote mode."
+ },
+ {
+ "name": "exactPhrase",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Exact phrase to match."
+ },
+ {
+ "name": "excludeWords",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Words or quoted phrases to exclude. Separate with spaces, commas, or lines."
+ },
+ {
+ "name": "anyWords",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Words or quoted phrases where any one can match. Separate with spaces, commas, or lines."
+ },
+ {
+ "name": "hashtags",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Hashtags separated by spaces, commas, or lines."
+ },
+ {
+ "name": "cashtags",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Cashtags separated by spaces, commas, or lines."
+ },
+ {
+ "name": "url",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "URL substring or domain filter."
+ },
+ {
+ "name": "conversationId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Conversation ID filter."
+ },
+ {
+ "name": "inReplyToTweetId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Only replies to this tweet ID."
+ },
+ {
+ "name": "quotesOfTweetId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Only quotes of this tweet ID."
+ },
+ {
+ "name": "retweetsOfTweetId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Only retweets of this tweet ID."
+ }
+ ],
+ "path": "/api/v1/x/users/{id}/mentions",
+ "responseShape": "List of mentions",
+ "summary": "List tweets mentioning a user"
+ },
+ {
+ "action": true,
+ "category": "x-write",
+ "free": true,
+ "method": "POST",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "User ID to remove from your followers"
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "type": "object",
+ "description": "Account whose follower list should be updated."
+ }
+ ],
+ "path": "/api/v1/x/users/{id}/remove-follower",
+ "responseShape": "",
+ "summary": "Remove follower"
+ },
+ {
+ "action": false,
+ "category": "users",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "session",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "X user ID or username"
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor for user tweets"
+ },
+ {
+ "name": "includeReplies",
+ "in": "query",
+ "required": false,
+ "type": "boolean",
+ "description": "Include reply tweets"
+ },
+ {
+ "name": "includeParentTweet",
+ "in": "query",
+ "required": false,
+ "type": "boolean",
+ "description": "Include parent tweet for replies"
+ },
+ {
+ "name": "fromUser",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter by author username."
+ },
+ {
+ "name": "toUser",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter replies sent to a username."
+ },
+ {
+ "name": "mentioning",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter tweets mentioning a username."
+ },
+ {
+ "name": "language",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Language code filter, e.g. en or tr."
+ },
+ {
+ "name": "sinceDate",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Start date in YYYY-MM-DD format."
+ },
+ {
+ "name": "untilDate",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "End date in YYYY-MM-DD format."
+ },
+ {
+ "name": "mediaType",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Filter by media type."
+ },
+ {
+ "name": "minFaves",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum likes threshold."
+ },
+ {
+ "name": "minRetweets",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum retweets threshold."
+ },
+ {
+ "name": "minReplies",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum replies threshold."
+ },
+ {
+ "name": "minQuotes",
+ "in": "query",
+ "required": false,
+ "type": "integer",
+ "description": "Minimum quote count threshold."
+ },
+ {
+ "name": "verifiedOnly",
+ "in": "query",
+ "required": false,
+ "type": "boolean",
+ "description": "Only return tweets from verified authors."
+ },
+ {
+ "name": "replies",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Reply mode."
+ },
+ {
+ "name": "retweets",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Retweet mode."
+ },
+ {
+ "name": "quotes",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Quote mode."
+ },
+ {
+ "name": "exactPhrase",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Exact phrase to match."
+ },
+ {
+ "name": "excludeWords",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Words or quoted phrases to exclude. Separate with spaces, commas, or lines."
+ },
+ {
+ "name": "anyWords",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Words or quoted phrases where any one can match. Separate with spaces, commas, or lines."
+ },
+ {
+ "name": "hashtags",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Hashtags separated by spaces, commas, or lines."
+ },
+ {
+ "name": "cashtags",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Cashtags separated by spaces, commas, or lines."
+ },
+ {
+ "name": "url",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "URL substring or domain filter."
+ },
+ {
+ "name": "conversationId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Conversation ID filter."
+ },
+ {
+ "name": "inReplyToTweetId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Only replies to this tweet ID."
+ },
+ {
+ "name": "quotesOfTweetId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Only quotes of this tweet ID."
+ },
+ {
+ "name": "retweetsOfTweetId",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Only retweets of this tweet ID."
+ }
+ ],
+ "path": "/api/v1/x/users/{id}/tweets",
+ "responseShape": "User tweets list",
+ "summary": "List recent tweets posted by a user"
+ },
+ {
+ "action": false,
+ "category": "users",
+ "free": false,
+ "method": "GET",
+ "mpp": {
+ "intent": "session",
+ "price": "$0.00015/call"
+ },
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "User ID or username for verified followers"
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "type": "string",
+ "description": "Pagination cursor for verified followers"
+ }
+ ],
+ "path": "/api/v1/x/users/{id}/verified-followers",
+ "responseShape": "List of verified followers",
+ "summary": "List verified followers of a user"
+ },
+ {
+ "action": false,
+ "category": "x-write",
+ "free": true,
+ "method": "GET",
+ "mpp": null,
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "type": "string",
+ "description": "Write action ID returned by a pending write response."
+ }
+ ],
+ "path": "/api/v1/x/write-actions/{id}",
+ "responseShape": "Write action status",
+ "summary": "Get write action status"
+ }
+]
diff --git a/plugins/hermes-tweet/hermes_tweet/client.py b/plugins/hermes-tweet/hermes_tweet/client.py
new file mode 100644
index 0000000..cc89e59
--- /dev/null
+++ b/plugins/hermes-tweet/hermes_tweet/client.py
@@ -0,0 +1,122 @@
+from __future__ import annotations
+
+import json
+import os
+from math import isfinite
+from typing import Any, cast
+from urllib.parse import urljoin
+
+import httpx
+
+API_V1_PREFIX = "/api/v1/"
+DEFAULT_BASE_URL = "https://xquik.com"
+TIMEOUT_SECONDS = 30.0
+
+
+def _env_text(name: str, default: str = "") -> str:
+ value = os.getenv(name)
+ if value is None:
+ return default
+ normalized = value.strip()
+ return normalized or default
+
+
+def _request_text(value: Any) -> str:
+ if not isinstance(value, str):
+ return ""
+ return value.strip()
+
+
+def normalize_query_params(value: Any) -> dict[str, str] | None:
+ if not isinstance(value, dict):
+ return None
+ output: dict[str, str] = {}
+ for key, item in cast("dict[object, object]", value).items():
+ if not isinstance(key, str):
+ continue
+ normalized_key = key.strip()
+ if not normalized_key:
+ continue
+ if isinstance(item, bool):
+ output[normalized_key] = str(item).lower()
+ elif isinstance(item, (str, int)) or (isinstance(item, float) and isfinite(item)):
+ output[normalized_key] = str(item)
+ return output or None
+
+
+def base_url() -> str:
+ return _env_text("XQUIK_BASE_URL", DEFAULT_BASE_URL).rstrip("/") + "/"
+
+
+def api_key() -> str:
+ return _env_text("XQUIK_API_KEY")
+
+
+def check_api_available() -> bool:
+ return bool(api_key())
+
+
+def action_enabled() -> bool:
+ return check_api_available() and _env_text("HERMES_TWEET_ENABLE_ACTIONS").lower() == "true"
+
+
+def build_headers(key: str, *, has_body: bool) -> dict[str, str]:
+ headers: dict[str, str] = {}
+ if key.startswith("xq_"):
+ headers["x-api-key"] = key
+ elif key:
+ headers["authorization"] = f"Bearer {key}"
+ if has_body:
+ headers["content-type"] = "application/json"
+ return headers
+
+
+def request(
+ method: Any,
+ path: Any,
+ query: Any = None,
+ body: Any | None = None,
+) -> Any:
+ normalized_method = _request_text(method).upper() or "GET"
+ normalized_path = _request_text(path)
+ params = normalize_query_params(query)
+ if not normalized_path.startswith(API_V1_PREFIX):
+ return {"success": False, "error": f"Path must start with {API_V1_PREFIX}"}
+ if "?" in normalized_path or "#" in normalized_path:
+ return {
+ "success": False,
+ "error": "Pass query parameters through the query object, not in the path.",
+ }
+
+ key = api_key()
+ if not key:
+ return {"success": False, "error": "XQUIK_API_KEY is not configured."}
+
+ url = urljoin(base_url(), normalized_path.lstrip("/"))
+ try:
+ with httpx.Client(timeout=TIMEOUT_SECONDS) as client:
+ response = client.request(
+ method=normalized_method,
+ url=url,
+ params=params,
+ json=body,
+ headers=build_headers(key, has_body=body is not None),
+ )
+ try:
+ payload = response.json()
+ except ValueError:
+ payload = {"text": response.text}
+ if not response.is_success:
+ return {
+ "success": False,
+ "error": "API request failed.",
+ "status_code": response.status_code,
+ "response": payload,
+ }
+ return payload
+ except httpx.HTTPError as exc:
+ return {"success": False, "error": str(exc)}
+
+
+def dumps(data: Any) -> str:
+ return json.dumps(data, ensure_ascii=False, separators=(",", ":"))
diff --git a/plugins/hermes-tweet/hermes_tweet/plugin.yaml b/plugins/hermes-tweet/hermes_tweet/plugin.yaml
new file mode 100644
index 0000000..8a0af54
--- /dev/null
+++ b/plugins/hermes-tweet/hermes_tweet/plugin.yaml
@@ -0,0 +1,12 @@
+name: hermes-tweet
+version: 0.1.6
+description: "Native Hermes Agent plugin for X/Twitter automation through Xquik"
+author: Xquik
+optional_env:
+ - XQUIK_API_KEY
+ - XQUIK_BASE_URL
+ - HERMES_TWEET_ENABLE_ACTIONS
+provides_tools:
+ - tweet_explore
+ - tweet_read
+ - tweet_action
diff --git a/plugins/hermes-tweet/hermes_tweet/schemas.py b/plugins/hermes-tweet/hermes_tweet/schemas.py
new file mode 100644
index 0000000..c746cc3
--- /dev/null
+++ b/plugins/hermes-tweet/hermes_tweet/schemas.py
@@ -0,0 +1,127 @@
+from __future__ import annotations
+
+_METHOD_ENUM = ["GET", "POST", "PATCH", "PUT", "DELETE"]
+_API_PATH_PATTERN = r"^(?:/api/v1/|https?://[^/]+/api/v1/)"
+_API_PATH_DESCRIPTION = (
+ "Concrete /api/v1/... endpoint path or copied API URL whose path starts with /api/v1/."
+)
+
+TWEET_EXPLORE = {
+ "name": "tweet_explore",
+ "description": (
+ "Search the bundled Xquik endpoint catalog. Use this before calling "
+ "tweet_read or tweet_action. This tool does not make network calls."
+ ),
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "query": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "\\S",
+ "description": (
+ "Keyword search across endpoint paths, summaries, parameters, "
+ "and response shapes."
+ ),
+ },
+ "category": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "\\S",
+ "description": "Endpoint category filter.",
+ },
+ "method": {
+ "type": "string",
+ "enum": _METHOD_ENUM,
+ "description": "HTTP method filter.",
+ },
+ "path": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "\\S",
+ "description": "Exact or partial /api/v1 path filter.",
+ },
+ "free": {"type": "boolean", "description": "Filter free or paid endpoints."},
+ "mpp": {"type": "boolean", "description": "Filter MPP eligible endpoints."},
+ "include_actions": {
+ "type": "boolean",
+ "description": "Include write-like and private endpoints in catalog results.",
+ "default": False,
+ },
+ "limit": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100,
+ "default": 25,
+ "description": "Maximum endpoint descriptors to return.",
+ },
+ },
+ "additionalProperties": False,
+ },
+}
+
+TWEET_READ = {
+ "name": "tweet_read",
+ "description": (
+ "Invoke one catalog-listed read-only Xquik endpoint. Use concrete /api/v1 paths "
+ "from tweet_explore. This tool rejects write-like and private endpoints."
+ ),
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "path": {
+ "type": "string",
+ "minLength": 8,
+ "pattern": _API_PATH_PATTERN,
+ "description": _API_PATH_DESCRIPTION,
+ },
+ "query": {
+ "type": "object",
+ "description": "Query parameters as string, number, or boolean values.",
+ "propertyNames": {"minLength": 1, "pattern": "\\S"},
+ "additionalProperties": {"type": ["string", "number", "boolean"]},
+ },
+ },
+ "required": ["path"],
+ "additionalProperties": False,
+ },
+}
+
+TWEET_ACTION = {
+ "name": "tweet_action",
+ "description": (
+ "Invoke one catalog-listed Xquik action endpoint, including writes and private reads. "
+ "Disabled unless HERMES_TWEET_ENABLE_ACTIONS=true. Show the endpoint and payload "
+ "to the user first."
+ ),
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "path": {
+ "type": "string",
+ "minLength": 8,
+ "pattern": _API_PATH_PATTERN,
+ "description": _API_PATH_DESCRIPTION,
+ },
+ "method": {"type": "string", "enum": _METHOD_ENUM, "default": "POST"},
+ "query": {
+ "type": "object",
+ "description": "Query parameters as string, number, or boolean values.",
+ "propertyNames": {"minLength": 1, "pattern": "\\S"},
+ "additionalProperties": {"type": ["string", "number", "boolean"]},
+ },
+ "body": {
+ "description": "JSON request body.",
+ "type": ["object", "array", "string", "number", "boolean", "null"],
+ },
+ "reason": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "\\S",
+ "description": "Brief user-visible reason for the action.",
+ },
+ },
+ "required": ["path", "reason"],
+ "additionalProperties": False,
+ },
+}
diff --git a/plugins/hermes-tweet/hermes_tweet/skills/__init__.py b/plugins/hermes-tweet/hermes_tweet/skills/__init__.py
new file mode 100644
index 0000000..8b2c69c
--- /dev/null
+++ b/plugins/hermes-tweet/hermes_tweet/skills/__init__.py
@@ -0,0 +1 @@
+"""Bundled Hermes Tweet skills."""
diff --git a/plugins/hermes-tweet/hermes_tweet/skills/hermes-tweet/SKILL.md b/plugins/hermes-tweet/hermes_tweet/skills/hermes-tweet/SKILL.md
new file mode 100644
index 0000000..817eb61
--- /dev/null
+++ b/plugins/hermes-tweet/hermes_tweet/skills/hermes-tweet/SKILL.md
@@ -0,0 +1,249 @@
+---
+name: hermes-tweet
+version: 0.1.6
+author: Xquik
+description: Use Xquik from Hermes Agent for X search, posting, replies, likes, retweets, follows, DMs, monitors, extraction jobs, draws, media, and trends.
+tags:
+ - hermes-agent
+ - xquik
+ - twitter
+ - x
+ - social-media
+ - automation
+metadata:
+ version: 0.1.6
+ author: Xquik
+ tags:
+ - hermes-agent
+ - xquik
+ - twitter
+ - x
+ - social-media
+ - automation
+capabilities:
+ shell:
+ required: false
+ justification: Optional Hermes CLI checks are used only for installation and registry diagnostics.
+ network:
+ required: true
+ justification: Hermes Tweet tools call Xquik API routes for X/Twitter reads and approved actions.
+ files:
+ required: false
+ justification: Normal use does not require local file reads or writes.
+ environment:
+ required: true
+ variables:
+ - XQUIK_API_KEY
+ - HERMES_TWEET_ENABLE_ACTIONS
+ - HERMES_ENABLE_PROJECT_PLUGINS
+ justification: Runtime configuration controls authenticated reads, gated actions, and trusted project-local plugin loading.
+ mcp:
+ required: false
+ justification: No MCP server access is required.
+ tools:
+ - tweet_explore
+ - tweet_read
+ - tweet_action
+---
+
+# Hermes Tweet
+
+Use Hermes Tweet when the user wants to automate or inspect X through Xquik.
+
+## When to Use
+
+Use this skill for Hermes Agent sessions that need X/Twitter data or controlled
+X actions through the Hermes Tweet plugin.
+
+Use this skill especially for social listening, launch monitoring, support
+triage, creator research, brand research, giveaway audits, community audits,
+and controlled publishing workflows.
+
+Use `tweet_explore` first when the user asks for a capability, endpoint, route,
+or Xquik API surface. Use `tweet_read` only after a read-only endpoint is known.
+Use `tweet_action` only after the user requests a write, private read, monitor,
+webhook, extraction job, giveaway draw, or media operation that requires action
+permissions.
+
+## Permissions and Capabilities
+
+- Use `tweet_explore`, `tweet_read`, and `tweet_action` only through the enabled
+ Hermes Tweet toolset.
+- Network access is limited to catalog-listed Xquik API routes reached by those
+ tools. Do not create direct HTTP fallbacks.
+- Shell access is not part of normal operation. Use Hermes CLI commands only for
+ the install and registry checks listed in Testing.
+- Local file access is not part of normal operation. Do not write reports,
+ credentials, logs, screenshots, or cached API payloads unless the user asks
+ for an explicit export workflow.
+- Environment access is limited to configuration presence checks for
+ `XQUIK_API_KEY`, `HERMES_TWEET_ENABLE_ACTIONS`, and
+ `HERMES_ENABLE_PROJECT_PLUGINS`. Never request or echo their values.
+- MCP access is not required.
+
+## Workflow
+
+1. Use `tweet_explore` to find the endpoint.
+2. Use `tweet_read` for public read-only endpoints.
+3. Use `tweet_action` only for writes or private reads after stating the exact endpoint and payload.
+
+## Decision Rules
+
+- IF the task is endpoint discovery, THEN call `tweet_explore` with a short
+ query.
+- IF the endpoint method is `GET` and the catalog does not mark it as an
+ action, THEN call `tweet_read`.
+- IF the endpoint method is not `GET`, or the route touches private account
+ state, THEN call `tweet_action` only when actions are enabled and the user has
+ approved the operation.
+- IF `tweet_action` is unavailable or disabled, THEN explain that action tools
+ are intentionally gated by `HERMES_TWEET_ENABLE_ACTIONS=true`.
+- IF `XQUIK_API_KEY` is missing, THEN ask the user to set it in the Hermes
+ runtime environment without requesting the key value in chat.
+- IF Hermes lists the plugin as `not enabled`, THEN tell the user to run
+ `hermes plugins enable hermes-tweet` or reinstall with `--enable`.
+- IF the plugin is installed as a project-local `.hermes/plugins/` copy, THEN
+ remind the user that Hermes requires `HERMES_ENABLE_PROJECT_PLUGINS=true` for
+ trusted repositories.
+- IF the task is unattended, scheduled, gateway-driven, or cron-driven, THEN
+ prefer `tweet_read` and keep `tweet_action` disabled unless the workflow has a
+ clear approval step.
+- IF the user is in Hermes Desktop with a remote gateway profile, THEN remind
+ them that Hermes Tweet must be installed, enabled, and configured on the
+ remote Hermes host where plugin tools execute.
+- IF the user uses the Hermes dashboard for gateway administration or
+ credentials, THEN keep Hermes Tweet secrets in the runtime environment and do
+ not ask for key values in chat.
+
+## Safety
+
+- Never ask for or reveal API keys, signing keys, passwords, cookies, or TOTP secrets.
+- Never pass credentials in tool arguments.
+- Use only catalog-listed `/api/v1/...` endpoints.
+- Copied endpoint URLs are accepted only when they resolve to catalog-listed paths.
+- Do not use account connection, re-authentication, API key, billing, credit top-up, or support-ticket endpoints.
+- For posting, deleting, following, DMs, profile changes, monitors, webhooks, extraction jobs, and draws, summarize the action before calling `tweet_action`.
+
+## Known Risks and Mitigations
+
+- Risk: A broad X/Twitter request may map to a write-capable route.
+ Mitigation: Start with `tweet_explore`, prefer `tweet_read`, and require a
+ user-approved endpoint plus payload before `tweet_action`.
+- Risk: Secrets may be pasted into chat or examples.
+ Mitigation: Ask only for environment configuration, never for key values, and
+ never put credentials in tool arguments.
+- Risk: Endpoint guessing may bypass catalog review.
+ Mitigation: Accept only catalog-listed `/api/v1/...` paths and reject direct
+ HTTP fallbacks.
+- Risk: Automated X/Twitter actions can affect real accounts.
+ Mitigation: Keep `HERMES_TWEET_ENABLE_ACTIONS=false` by default and summarize
+ side effects before any account-changing call.
+
+## Skill Output
+
+- Output type: endpoint selection, API-result summaries, action previews, and
+ troubleshooting guidance.
+- Output format: concise Markdown for humans and JSON-like tool payloads for
+ Hermes Tweet calls.
+- Side effects: `tweet_explore` has no external side effects, `tweet_read`
+ performs authenticated reads, and `tweet_action` may change account or
+ workflow state only after explicit approval.
+
+## Pitfalls
+
+- Do not guess endpoint paths. Always use the catalog returned by `tweet_explore`.
+- Do not treat a slash command prompt as proof that Hermes registered the
+ command. Verify slash commands through an active Hermes session or plugin
+ registry test.
+- Do not use bare `hermes tools` for scripted diagnostics. Run
+ `hermes tools list` instead.
+- Do not assume installation means execution. Current Hermes Agent versions
+ discover third-party plugins before they are enabled.
+- Do not assume the Desktop app stores plugin secrets for a remote gateway.
+ Configure `XQUIK_API_KEY` where the Hermes runtime executes.
+- Do not retry writes through alternate routes after a policy, auth, or account
+ state error.
+- Do not include secrets in examples, logs, prompts, issue bodies, or tool input.
+
+## Hermes Agent v0.16.0 Surfaces
+
+Hermes Agent v0.16.0 added a native Desktop app, remote gateway profiles, a
+larger web dashboard, and a command palette that can surface skills and quick
+commands. Hermes Tweet uses the same plugin entry point on all of those
+surfaces:
+
+- Install and enable `hermes-tweet` on the Hermes runtime host.
+- Put `XQUIK_API_KEY` in the runtime environment or `~/.hermes/.env`.
+- Keep `HERMES_TWEET_ENABLE_ACTIONS=false` unless the session intentionally
+ allows account-changing actions.
+- Use Desktop, TUI, CLI, or gateway sessions for interactive slash commands such
+ as `/xstatus` and `/xtrends`.
+
+## Examples
+
+Search tweets:
+
+```json
+{"query":"tweet search","method":"GET"}
+```
+
+Then call:
+
+```json
+{"path":"/api/v1/x/tweets/search","query":{"q":"AI agents","limit":25}}
+```
+
+Post a tweet:
+
+```json
+{"query":"post tweet","include_actions":true}
+```
+
+Then call `tweet_action` with:
+
+```json
+{"path":"/api/v1/x/tweets","method":"POST","body":{"account":"@example","text":"Hello from Hermes Tweet"},"reason":"Post the user-approved tweet."}
+```
+
+## Testing
+
+After installing or upgrading the plugin in Hermes Agent:
+
+1. Run `hermes plugins enable hermes-tweet` unless the install used `--enable`.
+2. Run `hermes plugins list` and confirm the plugin is `enabled`.
+3. Run `hermes tools list` and confirm the `hermes-tweet` toolset is enabled.
+4. Confirm `tweet_explore` is available without `XQUIK_API_KEY`.
+5. Confirm `tweet_read` appears only when `XQUIK_API_KEY` is configured.
+6. Confirm `tweet_action` stays hidden or disabled unless `HERMES_TWEET_ENABLE_ACTIONS=true`.
+
+Useful CLI checks:
+
+```bash
+hermes plugins enable hermes-tweet
+hermes tools list
+```
+
+## Release Trust Gate
+
+Before presenting this skill as NVIDIA-verified or ready for broad enterprise
+deployment:
+
+1. Run SkillSpector against the complete skill directory and resolve critical or
+ high findings.
+2. Complete `skill-card.md` with owner, license, use case, deployment
+ geography, risks, references, output shape, and release version.
+3. Include Tier-3 eval data and `BENCHMARK.md` for the reviewed release.
+4. Sign the exact reviewed skill directory and publish `skill.oms.sig`.
+5. Verify the published directory with the expected certificate chain.
+
+Do not claim NVIDIA verification when those release artifacts are absent.
+
+## Version History
+
+- Unreleased: Add NVIDIA-style capability declarations, risk controls, output
+ shape, and release trust gate.
+- Unreleased: Refresh current Hermes Agent opt-in plugin lifecycle guidance and
+ workflow positioning.
+- 0.1.6: Refresh catalog wording from current Xquik OpenAPI.
+- 0.1.5: Add registry-compatible nested metadata and clearer Hermes runtime guidance.
+- 0.1.4: Add public registry frontmatter for skill directory discovery.
diff --git a/plugins/hermes-tweet/hermes_tweet/skills/hermes-tweet/skill-card.md b/plugins/hermes-tweet/hermes_tweet/skills/hermes-tweet/skill-card.md
new file mode 100644
index 0000000..9ef9774
--- /dev/null
+++ b/plugins/hermes-tweet/hermes_tweet/skills/hermes-tweet/skill-card.md
@@ -0,0 +1,102 @@
+# Hermes Tweet Skill Card
+
+Status: public self-assessment. Not NVIDIA-verified.
+
+Do not present Hermes Tweet as NVIDIA-verified unless the release also includes
+a clean SkillSpector scan report, Tier-3 eval data, `BENCHMARK.md`,
+`skill.oms.sig`, and signature verification instructions for the exact reviewed
+skill directory.
+
+## Owner
+
+- Publisher: Xquik
+- Repository: https://github.com/Xquik-dev/hermes-tweet
+- License: MIT
+- Version: 0.1.6
+- Primary skill file: `SKILL.md`
+
+## Use Case
+
+Hermes Tweet helps Hermes Agent users find X/Twitter endpoints, perform
+authenticated X/Twitter reads, and run explicitly approved X/Twitter workflow
+actions through the bundled Hermes Tweet tools.
+
+Use it for:
+
+- Searching tweets, reading tweet details, replies, and user profiles.
+- Preparing action previews for posts, replies, follows, direct messages,
+ monitors, webhooks, extraction jobs, media workflows, and giveaway draws.
+- Keeping X/Twitter automation inside catalog-listed Xquik API routes.
+
+Do not use it for account connection, re-authentication, billing, credit top-up,
+support tickets, or direct HTTP fallback routes.
+
+## Inputs and Configuration
+
+- Required configuration: `XQUIK_API_KEY` must be configured in the runtime
+ environment. Never request, echo, log, or store the value.
+- Action gate: `HERMES_TWEET_ENABLE_ACTIONS=true` is required before
+ write-capable tool calls.
+- Project plugin gate: `HERMES_ENABLE_PROJECT_PLUGINS=true` is required for
+ trusted local Hermes project plugin loading.
+- User input: natural language requests, endpoint choices, and explicit action
+ payload approval.
+
+## Capabilities
+
+- Tools: `tweet_explore`, `tweet_read`, `tweet_action`.
+- Network: required only through catalog-listed Xquik API routes reached by
+ those tools.
+- Shell: not required for normal operation. Use Hermes CLI commands only for
+ installation and registry diagnostics.
+- Files: not required for normal operation. Do not write reports, credentials,
+ logs, screenshots, or cached payloads unless the user asks for an explicit
+ export workflow.
+- MCP: not required.
+
+## Outputs
+
+- Endpoint recommendations from `tweet_explore`.
+- Concise summaries of authenticated read results from `tweet_read`.
+- Action previews, JSON-like payloads, and post-call summaries for
+ user-approved `tweet_action` calls.
+- Troubleshooting guidance for missing configuration or disabled action gates.
+
+## Side Effects
+
+- `tweet_explore` has no external side effects.
+- `tweet_read` performs authenticated reads.
+- `tweet_action` may change account or workflow state only after explicit user
+ approval and only when the action gate is enabled.
+
+## Known Risks and Mitigations
+
+- Risk: a broad X/Twitter request may map to a write-capable route.
+ Mitigation: start with `tweet_explore`, prefer `tweet_read`, and require a
+ user-approved endpoint plus payload before `tweet_action`.
+- Risk: secrets may appear in chat or examples.
+ Mitigation: ask only for environment configuration, never key values, and
+ never put credentials in tool arguments.
+- Risk: endpoint guessing may bypass catalog review.
+ Mitigation: accept only catalog-listed `/api/v1/...` paths and reject direct
+ HTTP fallbacks.
+- Risk: automated X/Twitter actions can affect real accounts.
+ Mitigation: keep `HERMES_TWEET_ENABLE_ACTIONS=false` by default and summarize
+ side effects before any account-changing call.
+
+## Release Trust Gate
+
+Before broad enterprise release or any NVIDIA-verified claim:
+
+1. Run SkillSpector against the complete skill directory.
+2. Resolve critical or high findings.
+3. Add Tier-3 eval data and `BENCHMARK.md` for the reviewed release.
+4. Sign the exact reviewed skill directory and publish `skill.oms.sig`.
+5. Verify the published directory with the expected certificate chain.
+
+## References
+
+- `SKILL.md`
+- `README.md`
+- `after-install.md`
+- `SECURITY.md`
diff --git a/plugins/hermes-tweet/hermes_tweet/tools.py b/plugins/hermes-tweet/hermes_tweet/tools.py
new file mode 100644
index 0000000..77c3cfe
--- /dev/null
+++ b/plugins/hermes-tweet/hermes_tweet/tools.py
@@ -0,0 +1,152 @@
+from __future__ import annotations
+
+from typing import Any, cast
+
+from .catalog import explore as explore_catalog
+from .catalog import find_endpoint, matches_path, normalize_method, normalize_path
+from .client import action_enabled, check_api_available, dumps, normalize_query_params, request
+
+ARGS_ERROR = "Tool arguments must be a JSON object."
+ACTION_REASON_ERROR = "Action reason is required."
+PATH_QUERY_ERROR = "Pass query parameters through the query object, not in path."
+BLOCKED_ACTION_ERROR = (
+ "Endpoint is blocked: account-connection challenges are not callable through Hermes Tweet."
+)
+BLOCKED_ACTION_ENDPOINTS: tuple[tuple[str, str], ...] = (
+ ("POST", "/api/v1/x/account-connection-challenges/{id}/submit"),
+)
+
+
+def _args(value: Any) -> dict[str, Any] | None:
+ if not isinstance(value, dict):
+ return None
+ return cast("dict[str, Any]", value)
+
+
+def _args_error() -> str:
+ return dumps({"success": False, "error": ARGS_ERROR})
+
+
+def _text(value: Any) -> str:
+ if not isinstance(value, str):
+ return ""
+ return value.strip()
+
+
+def _path_error(path: str) -> str:
+ if "?" not in path and "#" not in path:
+ return ""
+ return PATH_QUERY_ERROR
+
+
+def _is_blocked_action(method: str, path: str) -> bool:
+ return any(
+ blocked_method == method and matches_path(blocked_path, path)
+ for blocked_method, blocked_path in BLOCKED_ACTION_ENDPOINTS
+ )
+
+
+def explore(args: Any, **_: Any) -> str:
+ try:
+ tool_args = _args(args)
+ if tool_args is None:
+ return _args_error()
+ return dumps({"success": True, "endpoints": explore_catalog(tool_args)})
+ except Exception as exc:
+ return dumps({"success": False, "error": str(exc)})
+
+
+def call_read(args: Any, **_: Any) -> str:
+ try:
+ tool_args = _args(args)
+ if tool_args is None:
+ return _args_error()
+ path = _text(tool_args.get("path"))
+ path_error = _path_error(path)
+ if path_error:
+ return dumps({"success": False, "error": path_error})
+ catalog_path = normalize_path(path)
+ endpoint = find_endpoint("GET", catalog_path)
+ if endpoint is None:
+ return dumps(
+ {
+ "success": False,
+ "error": f"Endpoint is not in the Hermes Tweet catalog: GET {path}",
+ }
+ )
+ if endpoint.action:
+ return dumps(
+ {
+ "success": False,
+ "error": "Use tweet_action for private or write-like endpoints.",
+ }
+ )
+ return dumps(
+ request("GET", catalog_path, query=normalize_query_params(tool_args.get("query")))
+ )
+ except Exception as exc:
+ return dumps({"success": False, "error": str(exc)})
+
+
+def call_action(args: Any, **_: Any) -> str:
+ try:
+ tool_args = _args(args)
+ if tool_args is None:
+ return _args_error()
+ endpoint_error = ""
+ if not action_enabled():
+ endpoint_error = (
+ "tweet_action is disabled. Set HERMES_TWEET_ENABLE_ACTIONS=true to enable it."
+ )
+ elif not _text(tool_args.get("reason")):
+ endpoint_error = ACTION_REASON_ERROR
+ method = normalize_method(tool_args.get("method"), default="POST")
+ path = _text(tool_args.get("path"))
+ path_error = _path_error(path)
+ catalog_path = normalize_path(path)
+ endpoint = find_endpoint(method, catalog_path)
+ if not endpoint_error and path_error:
+ endpoint_error = path_error
+ elif _is_blocked_action(method, catalog_path):
+ endpoint_error = BLOCKED_ACTION_ERROR
+ elif endpoint is None:
+ endpoint_error = f"Endpoint is not in the Hermes Tweet catalog: {method} {path}"
+ if endpoint_error:
+ return dumps(
+ {
+ "success": False,
+ "error": endpoint_error,
+ }
+ )
+ return dumps(
+ request(
+ method,
+ catalog_path,
+ query=normalize_query_params(tool_args.get("query")),
+ body=tool_args.get("body"),
+ )
+ )
+ except Exception as exc:
+ return dumps({"success": False, "error": str(exc)})
+
+
+def xstatus(raw_args: Any = "") -> str:
+ _ = raw_args
+ return call_read({"path": "/api/v1/account"})
+
+
+def xtrends(raw_args: Any = "") -> str:
+ category = _text(raw_args)
+ query = {"category": category} if category else None
+ return call_read({"path": "/api/v1/x/trends", "query": query})
+
+
+__all__ = [
+ "action_enabled",
+ "call_action",
+ "call_read",
+ "check_api_available",
+ "explore",
+ "xstatus",
+ "xtrends",
+]
diff --git a/plugins/hermes-tweet/plugin.yaml b/plugins/hermes-tweet/plugin.yaml
new file mode 100644
index 0000000..8a0af54
--- /dev/null
+++ b/plugins/hermes-tweet/plugin.yaml
@@ -0,0 +1,12 @@
+name: hermes-tweet
+version: 0.1.6
+description: "Native Hermes Agent plugin for X/Twitter automation through Xquik"
+author: Xquik
+optional_env:
+ - XQUIK_API_KEY
+ - XQUIK_BASE_URL
+ - HERMES_TWEET_ENABLE_ACTIONS
+provides_tools:
+ - tweet_explore
+ - tweet_read
+ - tweet_action
diff --git a/plugins/hermes-tweet/pyproject.toml b/plugins/hermes-tweet/pyproject.toml
new file mode 100644
index 0000000..963fe0d
--- /dev/null
+++ b/plugins/hermes-tweet/pyproject.toml
@@ -0,0 +1,132 @@
+[build-system]
+requires = ["setuptools>=82.0.1", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "hermes-tweet"
+version = "0.1.6"
+description = "Native Hermes Agent plugin for X/Twitter automation through Xquik"
+readme = "README.md"
+requires-python = ">=3.11"
+license = "MIT"
+authors = [{ name = "Xquik" }]
+dependencies = [
+ "httpx>=0.28.1,<0.29",
+]
+keywords = [
+ "agent-skill",
+ "agent-tools",
+ "ai-agent",
+ "automation",
+ "hermes-agent",
+ "hermes-plugin",
+ "mcp",
+ "social-media",
+ "tweet",
+ "twitter",
+ "twitter-api",
+ "twitter-automation",
+ "x",
+ "x-api",
+ "x-automation",
+ "x-twitter",
+ "xquik",
+]
+classifiers = [
+ "Development Status :: 3 - Alpha",
+ "Environment :: Plugins",
+ "Intended Audience :: Developers",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Topic :: Communications",
+ "Topic :: Internet",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+]
+
+[project.optional-dependencies]
+dev = [
+ "bandit[toml]>=1.9.4",
+ "basedpyright>=1.39.9",
+ "build>=1.5.0",
+ "pip>=26.1.2",
+ "pip-audit>=2.10.1",
+ "PyYAML>=6.0.3",
+ "pytest>=9.1.1",
+ "pytest-cov>=7.1.0",
+ "ruff>=0.15.20",
+ "twine>=6.2.0",
+ "types-PyYAML>=6.0.12.20260518",
+]
+
+[project.urls]
+Homepage = "https://github.com/Xquik-dev/hermes-tweet#readme"
+Documentation = "https://github.com/Xquik-dev/hermes-tweet#readme"
+Repository = "https://github.com/Xquik-dev/hermes-tweet"
+Issues = "https://github.com/Xquik-dev/hermes-tweet/issues"
+ClawHub = "https://clawhub.ai/skills/hermes-tweet"
+DeepWiki = "https://deepwiki.com/Xquik-dev/hermes-tweet"
+piwheels = "https://piwheels.org/project/hermes-tweet/"
+"Xquik Platform" = "https://xquik.com"
+
+[project.entry-points."hermes_agent.plugins"]
+hermes-tweet = "hermes_tweet"
+
+[tool.setuptools]
+packages = ["hermes_tweet", "hermes_tweet.skills"]
+include-package-data = true
+
+[tool.setuptools.package-data]
+hermes_tweet = [
+ "plugin.yaml",
+ "catalog_data.json",
+ "skills/hermes-tweet/SKILL.md",
+]
+
+[tool.ruff]
+line-length = 100
+src = ["hermes_tweet", "scripts", "tests"]
+target-version = "py311"
+
+[tool.ruff.lint]
+select = ["ALL"]
+ignore = [
+ "ANN401",
+ "BLE001",
+ "COM812",
+ "D100",
+ "D101",
+ "D102",
+ "D103",
+ "D104",
+ "D203",
+ "D213",
+ "ISC001",
+ "N999",
+ "TRY300",
+]
+
+[tool.ruff.lint.per-file-ignores]
+"tests/**/*.py" = ["D105", "D107", "EM101", "PLR2004", "S101", "TRY003"]
+"scripts/**/*.py" = ["T201"]
+
+[tool.ruff.lint.isort]
+known-first-party = ["hermes_tweet"]
+
+[tool.basedpyright]
+include = ["hermes_tweet", "scripts", "tests"]
+pythonVersion = "3.11"
+typeCheckingMode = "strict"
+
+[tool.coverage.run]
+source = ["hermes_tweet", "tests"]
+
+[tool.coverage.report]
+fail_under = 100
+show_missing = true
+skip_covered = true
+
+[tool.bandit]
+exclude_dirs = ["tests"]
+skips = []
diff --git a/plugins/hermes-tweet/skill.json b/plugins/hermes-tweet/skill.json
new file mode 100644
index 0000000..2c98f5a
--- /dev/null
+++ b/plugins/hermes-tweet/skill.json
@@ -0,0 +1,39 @@
+{
+ "name": "hermes-tweet",
+ "version": "0.1.6",
+ "description": "Hermes Agent X/Twitter plugin for Xquik automation",
+ "author": "Xquik",
+ "tags": [
+ "hermes-agent",
+ "hermes-plugin",
+ "xquik",
+ "twitter",
+ "x",
+ "social-media",
+ "automation"
+ ],
+ "dependencies": [],
+ "conflicts": [],
+ "install": {
+ "openclaw": "hermes plugins install Xquik-dev/hermes-tweet --enable"
+ },
+ "keywords": [
+ "agent-skill",
+ "hermes-agent",
+ "hermes-plugin",
+ "xquik",
+ "twitter",
+ "twitter-api",
+ "twitter-automation",
+ "x",
+ "x-api",
+ "x-automation",
+ "x-twitter",
+ "social-media",
+ "automation",
+ "agent-tools",
+ "mcp"
+ ],
+ "homepage": "https://github.com/Xquik-dev/hermes-tweet#readme",
+ "repository": "https://github.com/Xquik-dev/hermes-tweet"
+}
diff --git a/plugins/hermes-tweet/skills/hermes-tweet/SKILL.md b/plugins/hermes-tweet/skills/hermes-tweet/SKILL.md
new file mode 100644
index 0000000..817eb61
--- /dev/null
+++ b/plugins/hermes-tweet/skills/hermes-tweet/SKILL.md
@@ -0,0 +1,249 @@
+---
+name: hermes-tweet
+version: 0.1.6
+author: Xquik
+description: Use Xquik from Hermes Agent for X search, posting, replies, likes, retweets, follows, DMs, monitors, extraction jobs, draws, media, and trends.
+tags:
+ - hermes-agent
+ - xquik
+ - twitter
+ - x
+ - social-media
+ - automation
+metadata:
+ version: 0.1.6
+ author: Xquik
+ tags:
+ - hermes-agent
+ - xquik
+ - twitter
+ - x
+ - social-media
+ - automation
+capabilities:
+ shell:
+ required: false
+ justification: Optional Hermes CLI checks are used only for installation and registry diagnostics.
+ network:
+ required: true
+ justification: Hermes Tweet tools call Xquik API routes for X/Twitter reads and approved actions.
+ files:
+ required: false
+ justification: Normal use does not require local file reads or writes.
+ environment:
+ required: true
+ variables:
+ - XQUIK_API_KEY
+ - HERMES_TWEET_ENABLE_ACTIONS
+ - HERMES_ENABLE_PROJECT_PLUGINS
+ justification: Runtime configuration controls authenticated reads, gated actions, and trusted project-local plugin loading.
+ mcp:
+ required: false
+ justification: No MCP server access is required.
+ tools:
+ - tweet_explore
+ - tweet_read
+ - tweet_action
+---
+
+# Hermes Tweet
+
+Use Hermes Tweet when the user wants to automate or inspect X through Xquik.
+
+## When to Use
+
+Use this skill for Hermes Agent sessions that need X/Twitter data or controlled
+X actions through the Hermes Tweet plugin.
+
+Use this skill especially for social listening, launch monitoring, support
+triage, creator research, brand research, giveaway audits, community audits,
+and controlled publishing workflows.
+
+Use `tweet_explore` first when the user asks for a capability, endpoint, route,
+or Xquik API surface. Use `tweet_read` only after a read-only endpoint is known.
+Use `tweet_action` only after the user requests a write, private read, monitor,
+webhook, extraction job, giveaway draw, or media operation that requires action
+permissions.
+
+## Permissions and Capabilities
+
+- Use `tweet_explore`, `tweet_read`, and `tweet_action` only through the enabled
+ Hermes Tweet toolset.
+- Network access is limited to catalog-listed Xquik API routes reached by those
+ tools. Do not create direct HTTP fallbacks.
+- Shell access is not part of normal operation. Use Hermes CLI commands only for
+ the install and registry checks listed in Testing.
+- Local file access is not part of normal operation. Do not write reports,
+ credentials, logs, screenshots, or cached API payloads unless the user asks
+ for an explicit export workflow.
+- Environment access is limited to configuration presence checks for
+ `XQUIK_API_KEY`, `HERMES_TWEET_ENABLE_ACTIONS`, and
+ `HERMES_ENABLE_PROJECT_PLUGINS`. Never request or echo their values.
+- MCP access is not required.
+
+## Workflow
+
+1. Use `tweet_explore` to find the endpoint.
+2. Use `tweet_read` for public read-only endpoints.
+3. Use `tweet_action` only for writes or private reads after stating the exact endpoint and payload.
+
+## Decision Rules
+
+- IF the task is endpoint discovery, THEN call `tweet_explore` with a short
+ query.
+- IF the endpoint method is `GET` and the catalog does not mark it as an
+ action, THEN call `tweet_read`.
+- IF the endpoint method is not `GET`, or the route touches private account
+ state, THEN call `tweet_action` only when actions are enabled and the user has
+ approved the operation.
+- IF `tweet_action` is unavailable or disabled, THEN explain that action tools
+ are intentionally gated by `HERMES_TWEET_ENABLE_ACTIONS=true`.
+- IF `XQUIK_API_KEY` is missing, THEN ask the user to set it in the Hermes
+ runtime environment without requesting the key value in chat.
+- IF Hermes lists the plugin as `not enabled`, THEN tell the user to run
+ `hermes plugins enable hermes-tweet` or reinstall with `--enable`.
+- IF the plugin is installed as a project-local `.hermes/plugins/` copy, THEN
+ remind the user that Hermes requires `HERMES_ENABLE_PROJECT_PLUGINS=true` for
+ trusted repositories.
+- IF the task is unattended, scheduled, gateway-driven, or cron-driven, THEN
+ prefer `tweet_read` and keep `tweet_action` disabled unless the workflow has a
+ clear approval step.
+- IF the user is in Hermes Desktop with a remote gateway profile, THEN remind
+ them that Hermes Tweet must be installed, enabled, and configured on the
+ remote Hermes host where plugin tools execute.
+- IF the user uses the Hermes dashboard for gateway administration or
+ credentials, THEN keep Hermes Tweet secrets in the runtime environment and do
+ not ask for key values in chat.
+
+## Safety
+
+- Never ask for or reveal API keys, signing keys, passwords, cookies, or TOTP secrets.
+- Never pass credentials in tool arguments.
+- Use only catalog-listed `/api/v1/...` endpoints.
+- Copied endpoint URLs are accepted only when they resolve to catalog-listed paths.
+- Do not use account connection, re-authentication, API key, billing, credit top-up, or support-ticket endpoints.
+- For posting, deleting, following, DMs, profile changes, monitors, webhooks, extraction jobs, and draws, summarize the action before calling `tweet_action`.
+
+## Known Risks and Mitigations
+
+- Risk: A broad X/Twitter request may map to a write-capable route.
+ Mitigation: Start with `tweet_explore`, prefer `tweet_read`, and require a
+ user-approved endpoint plus payload before `tweet_action`.
+- Risk: Secrets may be pasted into chat or examples.
+ Mitigation: Ask only for environment configuration, never for key values, and
+ never put credentials in tool arguments.
+- Risk: Endpoint guessing may bypass catalog review.
+ Mitigation: Accept only catalog-listed `/api/v1/...` paths and reject direct
+ HTTP fallbacks.
+- Risk: Automated X/Twitter actions can affect real accounts.
+ Mitigation: Keep `HERMES_TWEET_ENABLE_ACTIONS=false` by default and summarize
+ side effects before any account-changing call.
+
+## Skill Output
+
+- Output type: endpoint selection, API-result summaries, action previews, and
+ troubleshooting guidance.
+- Output format: concise Markdown for humans and JSON-like tool payloads for
+ Hermes Tweet calls.
+- Side effects: `tweet_explore` has no external side effects, `tweet_read`
+ performs authenticated reads, and `tweet_action` may change account or
+ workflow state only after explicit approval.
+
+## Pitfalls
+
+- Do not guess endpoint paths. Always use the catalog returned by `tweet_explore`.
+- Do not treat a slash command prompt as proof that Hermes registered the
+ command. Verify slash commands through an active Hermes session or plugin
+ registry test.
+- Do not use bare `hermes tools` for scripted diagnostics. Run
+ `hermes tools list` instead.
+- Do not assume installation means execution. Current Hermes Agent versions
+ discover third-party plugins before they are enabled.
+- Do not assume the Desktop app stores plugin secrets for a remote gateway.
+ Configure `XQUIK_API_KEY` where the Hermes runtime executes.
+- Do not retry writes through alternate routes after a policy, auth, or account
+ state error.
+- Do not include secrets in examples, logs, prompts, issue bodies, or tool input.
+
+## Hermes Agent v0.16.0 Surfaces
+
+Hermes Agent v0.16.0 added a native Desktop app, remote gateway profiles, a
+larger web dashboard, and a command palette that can surface skills and quick
+commands. Hermes Tweet uses the same plugin entry point on all of those
+surfaces:
+
+- Install and enable `hermes-tweet` on the Hermes runtime host.
+- Put `XQUIK_API_KEY` in the runtime environment or `~/.hermes/.env`.
+- Keep `HERMES_TWEET_ENABLE_ACTIONS=false` unless the session intentionally
+ allows account-changing actions.
+- Use Desktop, TUI, CLI, or gateway sessions for interactive slash commands such
+ as `/xstatus` and `/xtrends`.
+
+## Examples
+
+Search tweets:
+
+```json
+{"query":"tweet search","method":"GET"}
+```
+
+Then call:
+
+```json
+{"path":"/api/v1/x/tweets/search","query":{"q":"AI agents","limit":25}}
+```
+
+Post a tweet:
+
+```json
+{"query":"post tweet","include_actions":true}
+```
+
+Then call `tweet_action` with:
+
+```json
+{"path":"/api/v1/x/tweets","method":"POST","body":{"account":"@example","text":"Hello from Hermes Tweet"},"reason":"Post the user-approved tweet."}
+```
+
+## Testing
+
+After installing or upgrading the plugin in Hermes Agent:
+
+1. Run `hermes plugins enable hermes-tweet` unless the install used `--enable`.
+2. Run `hermes plugins list` and confirm the plugin is `enabled`.
+3. Run `hermes tools list` and confirm the `hermes-tweet` toolset is enabled.
+4. Confirm `tweet_explore` is available without `XQUIK_API_KEY`.
+5. Confirm `tweet_read` appears only when `XQUIK_API_KEY` is configured.
+6. Confirm `tweet_action` stays hidden or disabled unless `HERMES_TWEET_ENABLE_ACTIONS=true`.
+
+Useful CLI checks:
+
+```bash
+hermes plugins enable hermes-tweet
+hermes tools list
+```
+
+## Release Trust Gate
+
+Before presenting this skill as NVIDIA-verified or ready for broad enterprise
+deployment:
+
+1. Run SkillSpector against the complete skill directory and resolve critical or
+ high findings.
+2. Complete `skill-card.md` with owner, license, use case, deployment
+ geography, risks, references, output shape, and release version.
+3. Include Tier-3 eval data and `BENCHMARK.md` for the reviewed release.
+4. Sign the exact reviewed skill directory and publish `skill.oms.sig`.
+5. Verify the published directory with the expected certificate chain.
+
+Do not claim NVIDIA verification when those release artifacts are absent.
+
+## Version History
+
+- Unreleased: Add NVIDIA-style capability declarations, risk controls, output
+ shape, and release trust gate.
+- Unreleased: Refresh current Hermes Agent opt-in plugin lifecycle guidance and
+ workflow positioning.
+- 0.1.6: Refresh catalog wording from current Xquik OpenAPI.
+- 0.1.5: Add registry-compatible nested metadata and clearer Hermes runtime guidance.
+- 0.1.4: Add public registry frontmatter for skill directory discovery.
diff --git a/plugins/hermes-tweet/skills/hermes-tweet/skill-card.md b/plugins/hermes-tweet/skills/hermes-tweet/skill-card.md
new file mode 100644
index 0000000..9ef9774
--- /dev/null
+++ b/plugins/hermes-tweet/skills/hermes-tweet/skill-card.md
@@ -0,0 +1,102 @@
+# Hermes Tweet Skill Card
+
+Status: public self-assessment. Not NVIDIA-verified.
+
+Do not present Hermes Tweet as NVIDIA-verified unless the release also includes
+a clean SkillSpector scan report, Tier-3 eval data, `BENCHMARK.md`,
+`skill.oms.sig`, and signature verification instructions for the exact reviewed
+skill directory.
+
+## Owner
+
+- Publisher: Xquik
+- Repository: https://github.com/Xquik-dev/hermes-tweet
+- License: MIT
+- Version: 0.1.6
+- Primary skill file: `SKILL.md`
+
+## Use Case
+
+Hermes Tweet helps Hermes Agent users find X/Twitter endpoints, perform
+authenticated X/Twitter reads, and run explicitly approved X/Twitter workflow
+actions through the bundled Hermes Tweet tools.
+
+Use it for:
+
+- Searching tweets, reading tweet details, replies, and user profiles.
+- Preparing action previews for posts, replies, follows, direct messages,
+ monitors, webhooks, extraction jobs, media workflows, and giveaway draws.
+- Keeping X/Twitter automation inside catalog-listed Xquik API routes.
+
+Do not use it for account connection, re-authentication, billing, credit top-up,
+support tickets, or direct HTTP fallback routes.
+
+## Inputs and Configuration
+
+- Required configuration: `XQUIK_API_KEY` must be configured in the runtime
+ environment. Never request, echo, log, or store the value.
+- Action gate: `HERMES_TWEET_ENABLE_ACTIONS=true` is required before
+ write-capable tool calls.
+- Project plugin gate: `HERMES_ENABLE_PROJECT_PLUGINS=true` is required for
+ trusted local Hermes project plugin loading.
+- User input: natural language requests, endpoint choices, and explicit action
+ payload approval.
+
+## Capabilities
+
+- Tools: `tweet_explore`, `tweet_read`, `tweet_action`.
+- Network: required only through catalog-listed Xquik API routes reached by
+ those tools.
+- Shell: not required for normal operation. Use Hermes CLI commands only for
+ installation and registry diagnostics.
+- Files: not required for normal operation. Do not write reports, credentials,
+ logs, screenshots, or cached payloads unless the user asks for an explicit
+ export workflow.
+- MCP: not required.
+
+## Outputs
+
+- Endpoint recommendations from `tweet_explore`.
+- Concise summaries of authenticated read results from `tweet_read`.
+- Action previews, JSON-like payloads, and post-call summaries for
+ user-approved `tweet_action` calls.
+- Troubleshooting guidance for missing configuration or disabled action gates.
+
+## Side Effects
+
+- `tweet_explore` has no external side effects.
+- `tweet_read` performs authenticated reads.
+- `tweet_action` may change account or workflow state only after explicit user
+ approval and only when the action gate is enabled.
+
+## Known Risks and Mitigations
+
+- Risk: a broad X/Twitter request may map to a write-capable route.
+ Mitigation: start with `tweet_explore`, prefer `tweet_read`, and require a
+ user-approved endpoint plus payload before `tweet_action`.
+- Risk: secrets may appear in chat or examples.
+ Mitigation: ask only for environment configuration, never key values, and
+ never put credentials in tool arguments.
+- Risk: endpoint guessing may bypass catalog review.
+ Mitigation: accept only catalog-listed `/api/v1/...` paths and reject direct
+ HTTP fallbacks.
+- Risk: automated X/Twitter actions can affect real accounts.
+ Mitigation: keep `HERMES_TWEET_ENABLE_ACTIONS=false` by default and summarize
+ side effects before any account-changing call.
+
+## Release Trust Gate
+
+Before broad enterprise release or any NVIDIA-verified claim:
+
+1. Run SkillSpector against the complete skill directory.
+2. Resolve critical or high findings.
+3. Add Tier-3 eval data and `BENCHMARK.md` for the reviewed release.
+4. Sign the exact reviewed skill directory and publish `skill.oms.sig`.
+5. Verify the published directory with the expected certificate chain.
+
+## References
+
+- `SKILL.md`
+- `README.md`
+- `after-install.md`
+- `SECURITY.md`