{
  "schemaVersion": "1.0",
  "item": {
    "slug": "x-page",
    "name": "X Twitter",
    "source": "tencent",
    "type": "skill",
    "category": "开发工具",
    "sourceUrl": "https://clawhub.ai/seph1709/x-page",
    "canonicalUrl": "https://clawhub.ai/seph1709/x-page",
    "targetPlatform": "OpenClaw"
  },
  "install": {
    "downloadMode": "redirect",
    "downloadUrl": "/downloads/x-page",
    "sourceDownloadUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=x-page",
    "sourcePlatform": "tencent",
    "targetPlatform": "OpenClaw",
    "installMethod": "Manual import",
    "extraction": "Extract archive",
    "prerequisites": [
      "OpenClaw"
    ],
    "packageFormat": "ZIP package",
    "includedAssets": [
      "SKILL.md",
      "_meta.json"
    ],
    "primaryDoc": "SKILL.md",
    "quickSetup": [
      "Download the package from Yavira.",
      "Extract the archive and review SKILL.md first.",
      "Import or place the package into your OpenClaw setup."
    ],
    "agentAssist": {
      "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
      "steps": [
        "Download the package from Yavira.",
        "Extract it into a folder your agent can access.",
        "Paste one of the prompts below and point your agent at the extracted folder."
      ],
      "prompts": [
        {
          "label": "New install",
          "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
        },
        {
          "label": "Upgrade existing",
          "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
        }
      ]
    },
    "sourceHealth": {
      "source": "tencent",
      "status": "healthy",
      "reason": "direct_download_ok",
      "recommendedAction": "download",
      "checkedAt": "2026-05-07T17:22:31.273Z",
      "expiresAt": "2026-05-14T17:22:31.273Z",
      "httpStatus": 200,
      "finalUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=afrexai-annual-report",
      "contentType": "application/zip",
      "probeMethod": "head",
      "details": {
        "probeUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=afrexai-annual-report",
        "contentDisposition": "attachment; filename=\"afrexai-annual-report-1.0.0.zip\"",
        "redirectLocation": null,
        "bodySnippet": null
      },
      "scope": "source",
      "summary": "Source download looks usable.",
      "detail": "Yavira can redirect you to the upstream package for this source.",
      "primaryActionLabel": "Download for OpenClaw",
      "primaryActionHref": "/downloads/x-page"
    },
    "validation": {
      "installChecklist": [
        "Use the Yavira download entry.",
        "Review SKILL.md after the package is downloaded.",
        "Confirm the extracted package contains the expected setup assets."
      ],
      "postInstallChecks": [
        "Confirm the extracted package includes the expected docs or setup files.",
        "Validate the skill or prompts are available in your target agent workspace.",
        "Capture any manual follow-up steps the agent could not complete."
      ]
    },
    "downloadPageUrl": "https://openagent3.xyz/downloads/x-page",
    "agentPageUrl": "https://openagent3.xyz/skills/x-page/agent",
    "manifestUrl": "https://openagent3.xyz/skills/x-page/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/x-page/agent.md"
  },
  "agentAssist": {
    "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
    "steps": [
      "Download the package from Yavira.",
      "Extract it into a folder your agent can access.",
      "Paste one of the prompts below and point your agent at the extracted folder."
    ],
    "prompts": [
      {
        "label": "New install",
        "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
      },
      {
        "label": "Upgrade existing",
        "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
      }
    ]
  },
  "documentation": {
    "source": "clawhub",
    "primaryDoc": "SKILL.md",
    "sections": [
      {
        "title": "x-twitter - Universal X/Twitter API Skill",
        "body": "Constructs and executes X API v2 calls inline based on what the user wants. No scripts needed.\n\nAPI version: v2\nBase URL: https://api.twitter.com/2\n\nRequires an X Developer App with OAuth 1.0a User Context credentials.\nFree tier supports posting, reading own timeline, and basic lookups.\nElevated/Pro tier required for search and higher rate limits."
      },
      {
        "title": "STEP 1 - Load Credentials",
        "body": "Credentials are stored in ~/.config/x-twitter/credentials.json.\n\n$cfg           = Get-Content \"$HOME/.config/x-twitter/credentials.json\" -Raw | ConvertFrom-Json\n$apiKey        = $cfg.X_API_KEY\n$apiSecret     = $cfg.X_API_SECRET\n$accessToken   = $cfg.X_ACCESS_TOKEN\n$accessSecret  = $cfg.X_ACCESS_SECRET\n\nIf the file does not exist, guide setup. Required fields:\n\nFieldPurposeX_API_KEYApp API Key (Consumer Key) - from X Developer PortalX_API_SECRETApp API Secret (Consumer Secret) - from X Developer PortalX_ACCESS_TOKENAccount Access Token - from X Developer PortalX_ACCESS_SECRETAccount Access Token Secret - from X Developer Portal\n\nOne-time setup:\n\nGo to X Developer Portal\nCreate a Project and App (or use existing)\nUnder App Settings -> User authentication settings: enable OAuth 1.0a with Read and Write permissions\nGo to App Keys and Tokens -> Generate Access Token and Secret (for your own account)\nSave all four values:\n\n@{\n    X_API_KEY       = \"your_api_key\"\n    X_API_SECRET    = \"your_api_secret\"\n    X_ACCESS_TOKEN  = \"your_access_token\"\n    X_ACCESS_SECRET = \"your_access_token_secret\"\n} | ConvertTo-Json | Set-Content \"$HOME/.config/x-twitter/credentials.json\" -Encoding UTF8\n\nRestrict file permissions immediately after saving:\n\n# Windows\nicacls \"$HOME/.config/x-twitter/credentials.json\" /inheritance:r /grant:r \"$($env:USERNAME):(R,W)\"\n# macOS / Linux\n# chmod 600 ~/.config/x-twitter/credentials.json\n\nNever commit this file to version control. It contains long-lived secrets.\nRotate X_ACCESS_TOKEN and X_ACCESS_SECRET periodically and immediately if the host is ever compromised.\nX_API_KEY and X_API_SECRET are app-level credentials - keep them permanently but treat as sensitive.\nThis skill makes no external calls other than to api.twitter.com. No data is forwarded to third parties."
      },
      {
        "title": "STEP 2 - Figure Out the API Call",
        "body": "X API v2 uses OAuth 1.0a for user-context actions (post, delete, like, retweet) and\nBearer Token for read-only public data. This skill uses OAuth 1.0a for all calls\n(covers both read and write)."
      },
      {
        "title": "OAuth 1.0a Signing Helper",
        "body": "All requests require an OAuth 1.0a Authorization header. Use this helper:\n\nfunction Get-OAuthHeader {\n    param($method, $url, $apiKey, $apiSecret, $accessToken, $accessSecret, [hashtable]$params = @{})\n    $nonce     = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes([System.Guid]::NewGuid().ToString(\"N\")))\n    $timestamp = [int][double]::Parse(([datetime]::UtcNow - [datetime]\"1970-01-01\").TotalSeconds)\n    $oauthParams = @{\n        oauth_consumer_key     = $apiKey\n        oauth_nonce            = $nonce\n        oauth_signature_method = \"HMAC-SHA1\"\n        oauth_timestamp        = $timestamp\n        oauth_token            = $accessToken\n        oauth_version          = \"1.0\"\n    }\n    # Merge all params for signature base\n    $allParams = @{}\n    $oauthParams.GetEnumerator() | ForEach-Object { $allParams[$_.Key] = $_.Value }\n    $params.GetEnumerator() | ForEach-Object { $allParams[$_.Key] = $_.Value }\n    # Build signature base string\n    $sortedParams = ($allParams.GetEnumerator() | Sort-Object Key | ForEach-Object {\n        \"$([Uri]::EscapeDataString($_.Key))=$([Uri]::EscapeDataString($_.Value))\"\n    }) -join \"&\"\n    $baseString = \"$method&$([Uri]::EscapeDataString($url))&$([Uri]::EscapeDataString($sortedParams))\"\n    # Sign\n    $signingKey = \"$([Uri]::EscapeDataString($apiSecret))&$([Uri]::EscapeDataString($accessSecret))\"\n    $hmac = New-Object System.Security.Cryptography.HMACSHA1\n    $hmac.Key = [System.Text.Encoding]::ASCII.GetBytes($signingKey)\n    $signature = [System.Convert]::ToBase64String($hmac.ComputeHash([System.Text.Encoding]::ASCII.GetBytes($baseString)))\n    $oauthParams[\"oauth_signature\"] = $signature\n    # Build header\n    $headerParts = $oauthParams.GetEnumerator() | Sort-Object Key | ForEach-Object {\n        \"$([Uri]::EscapeDataString($_.Key))=`\"$([Uri]::EscapeDataString($_.Value))`\"\"\n    }\n    return \"OAuth $($headerParts -join ', ')\"\n}"
      },
      {
        "title": "Common Endpoints",
        "body": "What user wantsMethodEndpointPost a tweetPOST/tweets body: textReply to a tweetPOST/tweets body: text + reply.in_reply_to_tweet_idQuote a tweetPOST/tweets body: text + quote_tweet_idDelete a tweetDELETE/tweets/{id}Like a tweetPOST/users/{id}/likes body: tweet_idUnlike a tweetDELETE/users/{id}/likes/{tweet_id}RetweetPOST/users/{id}/retweets body: tweet_idUndo retweetDELETE/users/{id}/retweets/{tweet_id}Get own timelineGET/users/{id}/tweets?max_results=10&tweet.fields=created_at,public_metricsGet home timelineGET/users/{id}/timelines/reverse_chronological?max_results=10Search recent tweetsGET/tweets/search/recent?query=...&max_results=10Get tweet by IDGET/tweets/{id}?tweet.fields=created_at,public_metrics,author_idGet own user infoGET/users/me?user.fields=username,name,public_metrics,descriptionGet user by usernameGET/users/by/username/{username}?user.fields=public_metricsGet followersGET/users/{id}/followers?max_results=100Get followingGET/users/{id}/following?max_results=100Follow a userPOST/users/{id}/following body: target_user_idUnfollow a userDELETE/users/{id}/following/{target_id}Get mentionsGET/users/{id}/mentions?max_results=10&tweet.fields=created_at,author_idGet bookmarksGET/users/{id}/bookmarks?max_results=10Bookmark a tweetPOST/users/{id}/bookmarks body: tweet_idCreate a listPOST/lists body: name, privateGet own listsGET/users/{id}/owned_listsAdd member to listPOST/lists/{id}/members body: user_id"
      },
      {
        "title": "API Call Patterns",
        "body": "GET:\n\n$url    = \"https://api.twitter.com/2/ENDPOINT\"\n$authHeader = Get-OAuthHeader -method \"GET\" -url $url -apiKey $apiKey -apiSecret $apiSecret -accessToken $accessToken -accessSecret $accessSecret\n$result = Invoke-RestMethod -Uri $url -Headers @{ Authorization = $authHeader } -ErrorAction Stop\n\nGET with query params (include in signature):\n\n$url    = \"https://api.twitter.com/2/tweets/search/recent\"\n$qp     = @{ query = \"from:username\"; max_results = \"10\" }\n$authHeader = Get-OAuthHeader -method \"GET\" -url $url -apiKey $apiKey -apiSecret $apiSecret -accessToken $accessToken -accessSecret $accessSecret -params $qp\n$qs     = ($qp.GetEnumerator() | ForEach-Object { \"$($_.Key)=$([Uri]::EscapeDataString($_.Value))\" }) -join \"&\"\n$result = Invoke-RestMethod -Uri \"$url`?$qs\" -Headers @{ Authorization = $authHeader } -ErrorAction Stop\n\nPOST (JSON body):\n\n$url    = \"https://api.twitter.com/2/tweets\"\n$body   = @{ text = \"Hello from OpenClaw!\" } | ConvertTo-Json\n$authHeader = Get-OAuthHeader -method \"POST\" -url $url -apiKey $apiKey -apiSecret $apiSecret -accessToken $accessToken -accessSecret $accessSecret\n$result = Invoke-RestMethod -Uri $url -Method POST -Headers @{ Authorization = $authHeader; \"Content-Type\" = \"application/json\" } -Body $body -ErrorAction Stop\nWrite-Host \"Posted tweet ID: $($result.data.id)\"\n\nDELETE:\n\n$url    = \"https://api.twitter.com/2/tweets/{id}\"\n$authHeader = Get-OAuthHeader -method \"DELETE\" -url $url -apiKey $apiKey -apiSecret $apiSecret -accessToken $accessToken -accessSecret $accessSecret\n$result = Invoke-RestMethod -Uri $url -Method DELETE -Headers @{ Authorization = $authHeader } -ErrorAction Stop"
      },
      {
        "title": "Get Own User ID (needed for user-context endpoints)",
        "body": "$url    = \"https://api.twitter.com/2/users/me\"\n$authHeader = Get-OAuthHeader -method \"GET\" -url $url -apiKey $apiKey -apiSecret $apiSecret -accessToken $accessToken -accessSecret $accessSecret\n$me     = Invoke-RestMethod -Uri $url -Headers @{ Authorization = $authHeader } -ErrorAction Stop\n$userId = $me.data.id"
      },
      {
        "title": "Post with Media (image attachment)",
        "body": "# Step 1: Upload media via v1.1 endpoint (media upload is not on v2 yet)\n$mediaUrl   = \"https://upload.twitter.com/1.1/media/upload.json\"\n$fileBytes  = [System.IO.File]::ReadAllBytes($imagePath)\n$b64        = [System.Convert]::ToBase64String($fileBytes)\n$uploadAuth = Get-OAuthHeader -method \"POST\" -url $mediaUrl -apiKey $apiKey -apiSecret $apiSecret -accessToken $accessToken -accessSecret $accessSecret\n$upload     = Invoke-RestMethod -Uri $mediaUrl -Method POST -Headers @{ Authorization = $uploadAuth; \"Content-Type\" = \"application/json\" } `\n    -Body (@{ media_data = $b64 } | ConvertTo-Json) -ErrorAction Stop\n$mediaId    = $upload.media_id_string\n# Step 2: Post tweet with media\n$tweetUrl   = \"https://api.twitter.com/2/tweets\"\n$tweetAuth  = Get-OAuthHeader -method \"POST\" -url $tweetUrl -apiKey $apiKey -apiSecret $apiSecret -accessToken $accessToken -accessSecret $accessSecret\n$result     = Invoke-RestMethod -Uri $tweetUrl -Method POST `\n    -Headers @{ Authorization = $tweetAuth; \"Content-Type\" = \"application/json\" } `\n    -Body (@{ text = $caption; media = @{ media_ids = @($mediaId) } } | ConvertTo-Json -Depth 3) -ErrorAction Stop\nWrite-Host \"Posted tweet with media ID: $($result.data.id)\""
      },
      {
        "title": "STEP 3 - Handle Errors",
        "body": "try {\n    # ... API call ...\n} catch {\n    $err    = $_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue\n    $status = $_.Exception.Response.StatusCode.value__\n    $title  = $err.title\n    $detail = $err.detail\n    Write-Host \"HTTP $status - $title : $detail\"\n}\n\nHTTP StatusTitle / CodeMeaningFix400Invalid RequestBad parameters or malformed JSONCheck required fields; ensure JSON body is valid401UnauthorizedInvalid or expired credentialsRegenerate Access Token and Secret in Developer Portal403ForbiddenApp lacks permission or write access disabledEnable Read and Write in App -> User authentication settings403duplicate-contentTweet text is a duplicateChange the tweet text429Too Many RequestsRate limit exceededCheck x-rate-limit-reset header; wait until reset time404Not FoundTweet or user does not existVerify the ID; tweet may have been deleted453Access to endpoint deniedEndpoint requires elevated access tierUpgrade to Basic/Pro at developer.twitter.com"
      },
      {
        "title": "Rate Limits (Free Tier)",
        "body": "ActionLimitPOST /tweets17 tweets per 24h per user; 50 per appDELETE /tweets50 per 15 minGET /users/me25 per 24hGET /tweets/search/recentRequires Basic tier or aboveGET timelines5 per 15 min (Free); 180 per 15 min (Basic)\n\nIf rate limited: read the x-rate-limit-reset response header (Unix timestamp) and tell the user when they can retry."
      },
      {
        "title": "Access Tiers",
        "body": "TierCostKey limitsFree$017 tweets/day write; very limited readBasic$100/month100 tweets/day; search; higher read limitsPro$5000/monthFull access; high rate limits"
      },
      {
        "title": "AGENT RULES",
        "body": "Always load credentials first. If missing or incomplete, guide setup.\nAlways use OAuth 1.0a via the Get-OAuthHeader helper - never send raw tokens in query strings.\nGet own user ID first when calling user-context endpoints (/users/{id}/...) - use /users/me.\nNever embed tokens as literals - read all four credential fields fresh from disk at runtime.\nRotate credentials if the host is ever compromised: regenerate Access Token and Secret in Developer Portal.\nRate limits: on HTTP 429, read x-rate-limit-reset header and tell the user the exact retry time.\nFree tier restrictions: search requires Basic tier; if user gets 453 \"Access to endpoint denied\", tell them the required tier and link to developer.twitter.com/en/portal/products.\nMedia upload: uses v1.1 upload endpoint (upload.twitter.com) - this is intentional and expected; it is still Twitter/X infrastructure. State this if the user asks.\nLeast-privilege: instruct user to enable only Read and Write in app settings; do not request DM permissions unless explicitly needed.\nAll API calls go to api.twitter.com and upload.twitter.com only - both are X/Twitter infrastructure. No external forwarding, no third-party services.\nConstruct API calls inline from user intent - do not look for script files.\nOn any error: parse HTTP status, map to the table above, tell the user exactly what to do.\nDuplicate tweet: if user tries to post the same text twice, tell them X blocks duplicate content and ask them to change the wording."
      }
    ],
    "body": "x-twitter - Universal X/Twitter API Skill\n\nConstructs and executes X API v2 calls inline based on what the user wants. No scripts needed.\n\nAPI version: v2 Base URL: https://api.twitter.com/2\n\nRequires an X Developer App with OAuth 1.0a User Context credentials. Free tier supports posting, reading own timeline, and basic lookups. Elevated/Pro tier required for search and higher rate limits.\n\nSTEP 1 - Load Credentials\n\nCredentials are stored in ~/.config/x-twitter/credentials.json.\n\n$cfg           = Get-Content \"$HOME/.config/x-twitter/credentials.json\" -Raw | ConvertFrom-Json\n$apiKey        = $cfg.X_API_KEY\n$apiSecret     = $cfg.X_API_SECRET\n$accessToken   = $cfg.X_ACCESS_TOKEN\n$accessSecret  = $cfg.X_ACCESS_SECRET\n\n\nIf the file does not exist, guide setup. Required fields:\n\nField\tPurpose\nX_API_KEY\tApp API Key (Consumer Key) - from X Developer Portal\nX_API_SECRET\tApp API Secret (Consumer Secret) - from X Developer Portal\nX_ACCESS_TOKEN\tAccount Access Token - from X Developer Portal\nX_ACCESS_SECRET\tAccount Access Token Secret - from X Developer Portal\n\nOne-time setup:\n\nGo to X Developer Portal\nCreate a Project and App (or use existing)\nUnder App Settings -> User authentication settings: enable OAuth 1.0a with Read and Write permissions\nGo to App Keys and Tokens -> Generate Access Token and Secret (for your own account)\nSave all four values:\n@{\n    X_API_KEY       = \"your_api_key\"\n    X_API_SECRET    = \"your_api_secret\"\n    X_ACCESS_TOKEN  = \"your_access_token\"\n    X_ACCESS_SECRET = \"your_access_token_secret\"\n} | ConvertTo-Json | Set-Content \"$HOME/.config/x-twitter/credentials.json\" -Encoding UTF8\n\n\nRestrict file permissions immediately after saving:\n\n# Windows\nicacls \"$HOME/.config/x-twitter/credentials.json\" /inheritance:r /grant:r \"$($env:USERNAME):(R,W)\"\n# macOS / Linux\n# chmod 600 ~/.config/x-twitter/credentials.json\n\n\nNever commit this file to version control. It contains long-lived secrets. Rotate X_ACCESS_TOKEN and X_ACCESS_SECRET periodically and immediately if the host is ever compromised. X_API_KEY and X_API_SECRET are app-level credentials - keep them permanently but treat as sensitive. This skill makes no external calls other than to api.twitter.com. No data is forwarded to third parties.\n\nSTEP 2 - Figure Out the API Call\n\nX API v2 uses OAuth 1.0a for user-context actions (post, delete, like, retweet) and Bearer Token for read-only public data. This skill uses OAuth 1.0a for all calls (covers both read and write).\n\nOAuth 1.0a Signing Helper\n\nAll requests require an OAuth 1.0a Authorization header. Use this helper:\n\nfunction Get-OAuthHeader {\n    param($method, $url, $apiKey, $apiSecret, $accessToken, $accessSecret, [hashtable]$params = @{})\n    $nonce     = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes([System.Guid]::NewGuid().ToString(\"N\")))\n    $timestamp = [int][double]::Parse(([datetime]::UtcNow - [datetime]\"1970-01-01\").TotalSeconds)\n    $oauthParams = @{\n        oauth_consumer_key     = $apiKey\n        oauth_nonce            = $nonce\n        oauth_signature_method = \"HMAC-SHA1\"\n        oauth_timestamp        = $timestamp\n        oauth_token            = $accessToken\n        oauth_version          = \"1.0\"\n    }\n    # Merge all params for signature base\n    $allParams = @{}\n    $oauthParams.GetEnumerator() | ForEach-Object { $allParams[$_.Key] = $_.Value }\n    $params.GetEnumerator() | ForEach-Object { $allParams[$_.Key] = $_.Value }\n    # Build signature base string\n    $sortedParams = ($allParams.GetEnumerator() | Sort-Object Key | ForEach-Object {\n        \"$([Uri]::EscapeDataString($_.Key))=$([Uri]::EscapeDataString($_.Value))\"\n    }) -join \"&\"\n    $baseString = \"$method&$([Uri]::EscapeDataString($url))&$([Uri]::EscapeDataString($sortedParams))\"\n    # Sign\n    $signingKey = \"$([Uri]::EscapeDataString($apiSecret))&$([Uri]::EscapeDataString($accessSecret))\"\n    $hmac = New-Object System.Security.Cryptography.HMACSHA1\n    $hmac.Key = [System.Text.Encoding]::ASCII.GetBytes($signingKey)\n    $signature = [System.Convert]::ToBase64String($hmac.ComputeHash([System.Text.Encoding]::ASCII.GetBytes($baseString)))\n    $oauthParams[\"oauth_signature\"] = $signature\n    # Build header\n    $headerParts = $oauthParams.GetEnumerator() | Sort-Object Key | ForEach-Object {\n        \"$([Uri]::EscapeDataString($_.Key))=`\"$([Uri]::EscapeDataString($_.Value))`\"\"\n    }\n    return \"OAuth $($headerParts -join ', ')\"\n}\n\nCommon Endpoints\nWhat user wants\tMethod\tEndpoint\nPost a tweet\tPOST\t/tweets body: text\nReply to a tweet\tPOST\t/tweets body: text + reply.in_reply_to_tweet_id\nQuote a tweet\tPOST\t/tweets body: text + quote_tweet_id\nDelete a tweet\tDELETE\t/tweets/{id}\nLike a tweet\tPOST\t/users/{id}/likes body: tweet_id\nUnlike a tweet\tDELETE\t/users/{id}/likes/{tweet_id}\nRetweet\tPOST\t/users/{id}/retweets body: tweet_id\nUndo retweet\tDELETE\t/users/{id}/retweets/{tweet_id}\nGet own timeline\tGET\t/users/{id}/tweets?max_results=10&tweet.fields=created_at,public_metrics\nGet home timeline\tGET\t/users/{id}/timelines/reverse_chronological?max_results=10\nSearch recent tweets\tGET\t/tweets/search/recent?query=...&max_results=10\nGet tweet by ID\tGET\t/tweets/{id}?tweet.fields=created_at,public_metrics,author_id\nGet own user info\tGET\t/users/me?user.fields=username,name,public_metrics,description\nGet user by username\tGET\t/users/by/username/{username}?user.fields=public_metrics\nGet followers\tGET\t/users/{id}/followers?max_results=100\nGet following\tGET\t/users/{id}/following?max_results=100\nFollow a user\tPOST\t/users/{id}/following body: target_user_id\nUnfollow a user\tDELETE\t/users/{id}/following/{target_id}\nGet mentions\tGET\t/users/{id}/mentions?max_results=10&tweet.fields=created_at,author_id\nGet bookmarks\tGET\t/users/{id}/bookmarks?max_results=10\nBookmark a tweet\tPOST\t/users/{id}/bookmarks body: tweet_id\nCreate a list\tPOST\t/lists body: name, private\nGet own lists\tGET\t/users/{id}/owned_lists\nAdd member to list\tPOST\t/lists/{id}/members body: user_id\nAPI Call Patterns\n\nGET:\n\n$url    = \"https://api.twitter.com/2/ENDPOINT\"\n$authHeader = Get-OAuthHeader -method \"GET\" -url $url -apiKey $apiKey -apiSecret $apiSecret -accessToken $accessToken -accessSecret $accessSecret\n$result = Invoke-RestMethod -Uri $url -Headers @{ Authorization = $authHeader } -ErrorAction Stop\n\n\nGET with query params (include in signature):\n\n$url    = \"https://api.twitter.com/2/tweets/search/recent\"\n$qp     = @{ query = \"from:username\"; max_results = \"10\" }\n$authHeader = Get-OAuthHeader -method \"GET\" -url $url -apiKey $apiKey -apiSecret $apiSecret -accessToken $accessToken -accessSecret $accessSecret -params $qp\n$qs     = ($qp.GetEnumerator() | ForEach-Object { \"$($_.Key)=$([Uri]::EscapeDataString($_.Value))\" }) -join \"&\"\n$result = Invoke-RestMethod -Uri \"$url`?$qs\" -Headers @{ Authorization = $authHeader } -ErrorAction Stop\n\n\nPOST (JSON body):\n\n$url    = \"https://api.twitter.com/2/tweets\"\n$body   = @{ text = \"Hello from OpenClaw!\" } | ConvertTo-Json\n$authHeader = Get-OAuthHeader -method \"POST\" -url $url -apiKey $apiKey -apiSecret $apiSecret -accessToken $accessToken -accessSecret $accessSecret\n$result = Invoke-RestMethod -Uri $url -Method POST -Headers @{ Authorization = $authHeader; \"Content-Type\" = \"application/json\" } -Body $body -ErrorAction Stop\nWrite-Host \"Posted tweet ID: $($result.data.id)\"\n\n\nDELETE:\n\n$url    = \"https://api.twitter.com/2/tweets/{id}\"\n$authHeader = Get-OAuthHeader -method \"DELETE\" -url $url -apiKey $apiKey -apiSecret $apiSecret -accessToken $accessToken -accessSecret $accessSecret\n$result = Invoke-RestMethod -Uri $url -Method DELETE -Headers @{ Authorization = $authHeader } -ErrorAction Stop\n\nGet Own User ID (needed for user-context endpoints)\n$url    = \"https://api.twitter.com/2/users/me\"\n$authHeader = Get-OAuthHeader -method \"GET\" -url $url -apiKey $apiKey -apiSecret $apiSecret -accessToken $accessToken -accessSecret $accessSecret\n$me     = Invoke-RestMethod -Uri $url -Headers @{ Authorization = $authHeader } -ErrorAction Stop\n$userId = $me.data.id\n\nPost with Media (image attachment)\n# Step 1: Upload media via v1.1 endpoint (media upload is not on v2 yet)\n$mediaUrl   = \"https://upload.twitter.com/1.1/media/upload.json\"\n$fileBytes  = [System.IO.File]::ReadAllBytes($imagePath)\n$b64        = [System.Convert]::ToBase64String($fileBytes)\n$uploadAuth = Get-OAuthHeader -method \"POST\" -url $mediaUrl -apiKey $apiKey -apiSecret $apiSecret -accessToken $accessToken -accessSecret $accessSecret\n$upload     = Invoke-RestMethod -Uri $mediaUrl -Method POST -Headers @{ Authorization = $uploadAuth; \"Content-Type\" = \"application/json\" } `\n    -Body (@{ media_data = $b64 } | ConvertTo-Json) -ErrorAction Stop\n$mediaId    = $upload.media_id_string\n# Step 2: Post tweet with media\n$tweetUrl   = \"https://api.twitter.com/2/tweets\"\n$tweetAuth  = Get-OAuthHeader -method \"POST\" -url $tweetUrl -apiKey $apiKey -apiSecret $apiSecret -accessToken $accessToken -accessSecret $accessSecret\n$result     = Invoke-RestMethod -Uri $tweetUrl -Method POST `\n    -Headers @{ Authorization = $tweetAuth; \"Content-Type\" = \"application/json\" } `\n    -Body (@{ text = $caption; media = @{ media_ids = @($mediaId) } } | ConvertTo-Json -Depth 3) -ErrorAction Stop\nWrite-Host \"Posted tweet with media ID: $($result.data.id)\"\n\nSTEP 3 - Handle Errors\ntry {\n    # ... API call ...\n} catch {\n    $err    = $_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue\n    $status = $_.Exception.Response.StatusCode.value__\n    $title  = $err.title\n    $detail = $err.detail\n    Write-Host \"HTTP $status - $title : $detail\"\n}\n\nHTTP Status\tTitle / Code\tMeaning\tFix\n400\tInvalid Request\tBad parameters or malformed JSON\tCheck required fields; ensure JSON body is valid\n401\tUnauthorized\tInvalid or expired credentials\tRegenerate Access Token and Secret in Developer Portal\n403\tForbidden\tApp lacks permission or write access disabled\tEnable Read and Write in App -> User authentication settings\n403\tduplicate-content\tTweet text is a duplicate\tChange the tweet text\n429\tToo Many Requests\tRate limit exceeded\tCheck x-rate-limit-reset header; wait until reset time\n404\tNot Found\tTweet or user does not exist\tVerify the ID; tweet may have been deleted\n453\tAccess to endpoint denied\tEndpoint requires elevated access tier\tUpgrade to Basic/Pro at developer.twitter.com\nRate Limits (Free Tier)\nAction\tLimit\nPOST /tweets\t17 tweets per 24h per user; 50 per app\nDELETE /tweets\t50 per 15 min\nGET /users/me\t25 per 24h\nGET /tweets/search/recent\tRequires Basic tier or above\nGET timelines\t5 per 15 min (Free); 180 per 15 min (Basic)\n\nIf rate limited: read the x-rate-limit-reset response header (Unix timestamp) and tell the user when they can retry.\n\nAccess Tiers\nTier\tCost\tKey limits\nFree\t$0\t17 tweets/day write; very limited read\nBasic\t$100/month\t100 tweets/day; search; higher read limits\nPro\t$5000/month\tFull access; high rate limits\nAGENT RULES\nAlways load credentials first. If missing or incomplete, guide setup.\nAlways use OAuth 1.0a via the Get-OAuthHeader helper - never send raw tokens in query strings.\nGet own user ID first when calling user-context endpoints (/users/{id}/...) - use /users/me.\nNever embed tokens as literals - read all four credential fields fresh from disk at runtime.\nRotate credentials if the host is ever compromised: regenerate Access Token and Secret in Developer Portal.\nRate limits: on HTTP 429, read x-rate-limit-reset header and tell the user the exact retry time.\nFree tier restrictions: search requires Basic tier; if user gets 453 \"Access to endpoint denied\", tell them the required tier and link to developer.twitter.com/en/portal/products.\nMedia upload: uses v1.1 upload endpoint (upload.twitter.com) - this is intentional and expected; it is still Twitter/X infrastructure. State this if the user asks.\nLeast-privilege: instruct user to enable only Read and Write in app settings; do not request DM permissions unless explicitly needed.\nAll API calls go to api.twitter.com and upload.twitter.com only - both are X/Twitter infrastructure. No external forwarding, no third-party services.\nConstruct API calls inline from user intent - do not look for script files.\nOn any error: parse HTTP status, map to the table above, tell the user exactly what to do.\nDuplicate tweet: if user tries to post the same text twice, tell them X blocks duplicate content and ask them to change the wording."
  },
  "trust": {
    "sourceLabel": "tencent",
    "provenanceUrl": "https://clawhub.ai/seph1709/x-page",
    "publisherUrl": "https://clawhub.ai/seph1709/x-page",
    "owner": "seph1709",
    "version": "1.0.2",
    "license": null,
    "verificationStatus": "Indexed source record"
  },
  "links": {
    "detailUrl": "https://openagent3.xyz/skills/x-page",
    "downloadUrl": "https://openagent3.xyz/downloads/x-page",
    "agentUrl": "https://openagent3.xyz/skills/x-page/agent",
    "manifestUrl": "https://openagent3.xyz/skills/x-page/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/x-page/agent.md"
  }
}