From pepe-multi-channel-content-pipelines
Publish posts, threads, replies, and reposted reels to X (formerly Twitter) from an autonomous content pipeline. Covers operator setup (X developer account + project + app + OAuth 2.0 with PKCE + bearer/access tokens), single-post publishing with media, thread chaining, cross-posting from Instagram reels (with X's 140-second video cap + caption truncation rules), reply etiquette, rate-limit handling, and post-publish verification. Use whenever the pipeline needs X as a publishing surface.
How this skill is triggered — by the user, by Claude, or both
Slash command
/pepe-multi-channel-content-pipelines:publishing-xThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
X's developer platform is **the X API v2** with **OAuth 2.0 Authorization Code with PKCE**. Posting requires a paid developer tier (Basic at minimum as of 2025) — the free tier is read-only. The operator must accept this cost up front.
X's developer platform is the X API v2 with OAuth 2.0 Authorization Code with PKCE. Posting requires a paid developer tier (Basic at minimum as of 2025) — the free tier is read-only. The operator must accept this cost up front.
Compared to Instagram, X is simpler: no business-account-linked-to-page plumbing, no two-step container/publish dance — one POST per tweet. The complexity is in media-attached posting (videos must be chunk-uploaded) and in thread chaining (each subsequent tweet references the parent via in_reply_to_tweet_id).
Audience: the human operator. The agent walks each step in sequence and verifies before proceeding.
Sign up for the X Developer Platform. Visit https://developer.x.com/ → sign in with the brand X account → apply for developer access (free tier first; the upgrade to Basic happens in step 4).
Create a project + app. Developer Portal → Projects → Add project → name it <brand>-content-pipeline. Add an app under that project, name it the same. Note the App ID.
Confirm the use case. When prompted: "Making a bot" / "Publishing and analytics" — be honest. Lying about the use case is a TOS violation and X has terminated dev accounts retroactively for misrepresentation.
Upgrade to the paid Basic tier (≈ USD 100 / month as of 2025). The free tier blocks POST /2/tweets. The operator confirms they understand the recurring cost before continuing — this is the only paid skill in the playbook.
Configure OAuth 2.0 settings. App settings → User authentication settings → set up. Pick OAuth 2.0 (not 1.0a). App permissions: Read and write. Type of App: Web App, Automated App or Bot. Callback URI: http://127.0.0.1:8765/oauth/callback (local) or the operator's chosen HTTPS callback. Website URL: the brand's homepage.
Generate OAuth 2.0 credentials. App → Keys and tokens → OAuth 2.0 Client ID and Client Secret. Copy both.
Perform the one-time OAuth Authorization Code flow with PKCE. This grants the agent a refresh token. Procedure:
Generate a PKCE verifier + challenge (43-128 random chars verifier; SHA-256 → base64url challenge).
Open in browser:
https://twitter.com/i/oauth2/authorize?response_type=code&client_id=<CLIENT_ID>&redirect_uri=http://127.0.0.1:8765/oauth/callback&scope=tweet.read%20tweet.write%20users.read%20offline.access&state=<RANDOM>&code_challenge=<CHALLENGE>&code_challenge_method=S256
Sign in as the brand X account → Authorize.
The browser redirects to the callback with ?code=<AUTH_CODE>&state=<RANDOM>. Capture the code (a one-shot listener on 127.0.0.1:8765 or a manual URL paste).
Exchange for a refresh + access token pair:
curl -sf -X POST "https://api.x.com/2/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "<CLIENT_ID>:<CLIENT_SECRET>" \
-d "grant_type=authorization_code" \
-d "code=<AUTH_CODE>" \
-d "redirect_uri=http://127.0.0.1:8765/oauth/callback" \
-d "code_verifier=<VERIFIER>"
Response: {access_token, refresh_token, expires_in: 7200, scope, token_type: "bearer"}. The access token lasts 2 hours; the refresh token rotates on every refresh and lasts ~6 months.
Store credentials.
mkdir -p ~/.openclaw/credentials/x
cat > ~/.openclaw/credentials/x/env <<EOF
X_CLIENT_ID=<CLIENT_ID>
X_CLIENT_SECRET=<CLIENT_SECRET>
X_REFRESH_TOKEN=<REFRESH_TOKEN>
X_USER_ID=<USER_ID>
EOF
chmod 600 ~/.openclaw/credentials/x/env
The access token is not persisted — it's refreshed at the start of every batch run (see Command 2.1). The refresh token is rotated on every refresh, so the agent writes the new refresh token back to disk after each refresh.
Define per-account voice rules. Same shape as publishing-instagram Command 1.12 but tuned to X's 280-char limit + thread norms: tone, language, hashtag policy (typically lighter on X — 1-3 hashtags vs IG's 10), emoji signature, banned phrases. Persist as ~/.openclaw/credentials/x/voice-rules.json.
Smoke-test. Run Command 2 with a draft text-only post. Verify the first publish lands as drafted. X_FIRST_PUBLISH_GATE=1 holds the post as a draft (saved-but-not-tweeted) until the operator confirms — set to 0 after the first round-trip.
Operator confirms: "Setup complete."
X's access token lives for 2 hours; the agent refreshes it once per batch run.
curl -sf -X POST "https://api.x.com/2/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "<X_CLIENT_ID>:<X_CLIENT_SECRET>" \
-d "grant_type=refresh_token" \
-d "refresh_token=<X_REFRESH_TOKEN>"
Response carries a new access token + a new refresh token. Write the new refresh token back to ~/.openclaw/credentials/x/env — if the agent loses it, the operator must redo Command 1.7.
curl -sf -X POST "https://api.x.com/2/tweets" \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"text": "<TWEET_TEXT>"}'
{data: {id, text, edit_history_tweet_ids}}. Log the id — analytics + thread chaining consume it.state/publish-log.jsonl with {ts, channel:"x", media_type:"tweet", tweet_id, text_hash}.X has a separate media upload endpoint. For videos, X caps uploads at 140 seconds for the public API (Basic tier) — longer videos must be trimmed.
Trim if needed. If the source MP4 exceeds 140 s, the agent cuts it (ffmpeg -i source.mp4 -t 140 -c copy x-trimmed.mp4) and logs the truncation event. Aspect 9:16 reels under 140 s pass through as-is.
Initialize the chunked upload.
curl -sf -X POST "https://upload.x.com/1.1/media/upload.json" \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-d "command=INIT" \
-d "media_type=video/mp4" \
-d "media_category=tweet_video" \
-d "total_bytes=$(stat -f %z x-trimmed.mp4)"
Response: {media_id_string}.
Upload chunks (5 MB each, indexed from 0):
curl -sf -X POST "https://upload.x.com/1.1/media/upload.json" \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-F "command=APPEND" \
-F "media_id=<MEDIA_ID>" \
-F "segment_index=<N>" \
-F "media=@chunk-<N>.bin"
Finalize.
curl -sf -X POST "https://upload.x.com/1.1/media/upload.json" \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-d "command=FINALIZE" \
-d "media_id=<MEDIA_ID>"
Response includes a processing_info block — poll for state=succeeded (or failed) before tweeting:
curl -sf "https://upload.x.com/1.1/media/upload.json?command=STATUS&media_id=<MEDIA_ID>" \
-H "Authorization: Bearer <ACCESS_TOKEN>"
Tweet with the media attached.
curl -sf -X POST "https://api.x.com/2/tweets" \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"text": "<TWEET_TEXT>", "media": {"media_ids": ["<MEDIA_ID>"]}}'
A thread is a chain where each subsequent tweet references the prior tweet's ID via in_reply_to_tweet_id.
Post the head (Command 3 or 4). Capture tweet_id_0.
Post each follow-up:
curl -sf -X POST "https://api.x.com/2/tweets" \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"text": "<NEXT_TWEET>", "reply": {"in_reply_to_tweet_id": "<PREV_ID>"}}'
Capture each new tweet's ID; chain forward.
Spacing. 1-2 s between tweets is fine — well under X's rate limit. Beyond ~25 tweets in a thread, X's conversation_id window starts caching slow; cap practical thread length at 20.
Atomic-thread discipline. If a mid-thread tweet fails, do not retry that single tweet — the thread is now split. Delete from the failure point onward (DELETE /2/tweets/<id>) and re-post from there. Persistence: every tweet's ID is in state/publish-log.jsonl, so the agent can locate the split.
After publishing-instagram publishes a reel, mirror the same video to X. Don't re-encode — use the same MP4 the IG poster used, run it through the X-specific trimmer if needed, post via Command 4. Don't re-author the caption — derive from the canonical content source's text the same way Instagram's caption did, but with X's per-account voice rules applied.
source_video_path).via @<brand_ig_handle> if cross-posting is explicit in the brand voice (Pepe omits this — the same content stands on its own on each surface).Replies are tweets with in_reply_to_tweet_id set (same shape as thread continuations) but to a tweet not authored by the brand account. Don't auto-reply at the pipeline level — replies are part of the operator's engagement loop, not the publishing pipeline. The agent is allowed to:
The default is off — replies are a human surface.
X's Basic tier as of 2025:
/2/tweets / 15 min (rate-limit window).content-strategy-planning-optimization must respect the 100/24 h cap; the agent flags an over-budget calendar at planning time, not at publish time.x-rate-limit-remaining, x-rate-limit-reset — back off when remaining ≤ 5.Fetch by ID.
curl -sf "https://api.x.com/2/tweets/<TWEET_ID>?tweet.fields=created_at,text,attachments" \
-H "Authorization: Bearer <ACCESS_TOKEN>"
Confirm the text matches the submitted text verbatim. X strips zero-width and odd unicode at submit time — if the response text differs, the agent logs the diff and re-submits the next post with sanitised text.
Open the public URL https://x.com/<HANDLE>/status/<TWEET_ID> — agent fetches it as a sanity check; the page must render the tweet.
Stash the URL in the publish-log line.
@pepe_arturo_ai on X (linked to the same brand identity as the IG account).#PepeArturo + 1 topical. Emoji signature: 🍝.X_FIRST_PUBLISH_GATE=1 until the first round-trip).next_due_at, not a cron.Provides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
npx claudepluginhub helmut-hoffer-von-ankershoffen/helmguild-plugins --plugin pepe-multi-channel-content-pipelines