{"record":{"id":"457c7db37e076a3b","repo":"zeroclaw-labs/zeroclaw","slug":"twitter-create-tweet-failed-status-err","errorCode":null,"errorMessage":"Twitter create tweet failed ({status}): {err}","messagePattern":"Twitter create tweet failed \\((.+?)\\): (.+?)","errorType":"http","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/twitter.rs","lineNumber":137,"sourceCode":"    ) -> anyhow::Result<String> {\n        let mut body = json!({ \"text\": text });\n\n        if let Some(reply_id) = reply_tweet_id {\n            body[\"reply\"] = json!({ \"in_reply_to_tweet_id\": reply_id });\n        }\n\n        let resp = self\n            .http_client()\n            .post(format!(\"{TWITTER_API_BASE}/tweets\"))\n            .bearer_auth(&self.bearer_token)\n            .json(&body)\n            .send()\n            .await?;\n\n        if !resp.status().is_success() {\n            let status = resp.status();\n            let err = resp.text().await.unwrap_or_default();\n            anyhow::bail!(\"Twitter create tweet failed ({status}): {err}\");\n        }\n\n        let data: serde_json::Value = resp.json().await?;\n        let tweet_id = data\n            .get(\"data\")\n            .and_then(|d| d.get(\"id\"))\n            .and_then(|id| id.as_str())\n            .unwrap_or(\"\")\n            .to_string();\n\n        Ok(tweet_id)\n    }\n\n    /// Send a DM to a user.\n    async fn send_dm(&self, recipient_id: &str, text: &str) -> anyhow::Result<()> {\n        let body = json!({\n            \"text\": text,\n        });","sourceCodeStart":119,"sourceCodeEnd":155,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/twitter.rs#L119-L155","documentation":"Raised by TwitterChannel::create_tweet when POST /2/tweets answers non-2xx. send() splits content into 280-char chunks and calls create_tweet per chunk in a reply thread, so a mid-thread failure surfaces this error after earlier chunks already posted.","triggerScenarios":"403 missing the tweet.write scope or posting against a write-protected/deactivated account; 429 posting rate limit (per-user and app-level caps); 403 duplicate-content rejection when re-posting identical text; 404 when the reply.in_reply_to_tweet_id target was deleted.","commonSituations":"The app token has read scopes only, so reads work but every send fails with 403. Bots re-posting identical content hit the duplicate rule. Long agent replies that thread multiple chunks hit the per-window post cap mid-thread.","solutions":["For 403 scope errors, regenerate the token with tweet.write and retry.","For 429, back off to the next rate window (minutes-scale for posting caps) before re-sending.","For duplicate-content 403s, vary the text or drop the redundant send.","For reply failures, confirm the parent tweet id still exists and the account is not protected."],"exampleFix":"# before — read-only token; GET /users/me works, POST /2/tweets returns 403\n[channels.twitter.bot]\nbearer_token = \"AAAA%read-only\"\n\n# after — token with tweet.read + tweet.write (and dm.write for DMs)\n[channels.twitter.bot]\nbearer_token = \"AAAA%read-write\"","handlingStrategy":"retry","validationCode":"// Client-side guard matching the channel's own 280-char chunking.\nfn tweet_chunks_ok(content: &str) -> bool {\n    // send() already chunks at 280; just avoid pathological inputs up front\n    !content.trim().is_empty()\n}","typeGuard":null,"tryCatchPattern":"match twitter.send(&msg).await {\n    Ok(()) => Ok(()),\n    Err(err) => {\n        let msg_text = err.to_string();\n        if msg_text.contains(\"Twitter create tweet failed (429)\") {\n            tokio::time::sleep(Duration::from_secs(15 * 60)).await; // posting window\n            twitter.send(&msg).await\n        } else if msg_text.contains(\"(403)\") {\n            Err(err.context(\"token lacks tweet.write, duplicate content, or protected/deleted parent\"))\n        } else {\n            Err(err)\n        }\n    }\n}","preventionTips":["Issue tokens with tweet.write from day one; read-only tokens fail every send with 403.","Because threaded replies post chunk-by-chunk, a mid-thread failure leaves partial posts — prefer idempotent reply flows and check thread state before retrying.","Rate-limit outbound posting in your own scheduler (token bucket) instead of relying on 429 feedback."],"tags":["twitter","x-api","http","rate-limit","posting"],"backgroundTag":"twitter-api-error","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}