Twitter (X) Auto Reply: What the Rules Allow and How to Build One

By TwitterAPI.io Team•22 min read
Four-step diagram of a Twitter (X) auto reply loop. Step 1, Listen (read): GET /twitter/user/mentions with userName and sinceTime, X-API-Key only. Step 2, Filter and classify: inReplyToUsername equals you or a top-level post, contains a question, not on the stop list, not answered before, keyword or LLM intent. Step 3, Caps: at most N replies an hour, one reply per person per thread, no duplicate text, quiet hours, human pace. Step 4, Reply (write): POST /twitter/create_tweet_v2 with reply_to_tweet_id, login_cookies and proxy. Footer: in 1,600 mentions of 8 organization accounts, 56% sat inside somebody else's thread and only 11% were addressed to the account and contained a question.
The mention-listener loop this guide builds: read with an API key, keep only what is addressed to you, cap the rate, reply as the account. Diagram, not measured data.

"Twitter auto reply" covers two very different products: the harmless one (your support account answers people who ask it something) and the one that gets accounts suspended (a bot replies to strangers because their post contained a keyword). This guide builds the first kind, shows exactly where X's 2026 rules draw the line, and measures, on real mentions, how much work the bot would actually have to do.

It is a build guide with a compliance section, not a survey. If you want to read the replies under a post, including the ones X hides, that is how to see comments on X. If you want the overview of what bots exist and how they are labeled, start at the Twitter bots hub. If you want a conversational assistant that drafts answers with a language model, the Twitter chatbot guide goes deep on the model side; this page is about the responder loop itself: listening, filtering, caps, and the reply call.

To size the caps we pulled the 200 most recent mentions of eight organization support and brand accounts (@XboxSupport, @AWSSupport, @StripeSupport, @CloudflareHelp, @Docker, @github, @vercel, @NASA) in 2026, 1,600 mentions in 92 requests, and measured how many arrive per hour, how many are addressed to the account at all, how many contain a question, when they arrive, and what polling them costs. Every cap in the code below comes from one of those numbers.

01 — Section

What a Twitter (X) auto reply is, and what X's rules allow

An auto reply on X is any post written by software in response to something a person did. Four patterns share the name, and the rules treat them differently.

A mention responder (this page). Someone tags your account or replies under your post with a question; your bot answers that one post. The person engaged first, so the reply is solicited. Support desks, developer-tool accounts and event accounts run this pattern.

A campaign auto reply. You post a promotion and auto-reply to everyone who replies with a hashtag or a photo. X's automation rules name this case as allowed; it is a mention responder with a narrower trigger and a time box.

A DM autoresponder. The account answers people who message it first. Same consent logic, different endpoint (/twitter/send_dm_to_user); the Twitter chatbot guide covers the DM variant.

A keyword reply bot. Software searches X for a phrase ("anyone know a good VPN") and replies to strangers with a link. Nobody asked and the same text goes to many accounts: the pattern the rules describe as spam. We show no code for it, and the filter in Step 1 cannot drift into it by accident.

The difference between the first and the last is the trigger: a person addressing you, or a search. Three documents draw that line. X's Automation rules on the help center (help.x.com/en/rules-and-policies/x-automation) are the user-facing rules; the X Developer Policy (docs.x.com/developer-terms/policy) binds anyone posting through an API and says services performing write actions, including posting, must follow the Automation Rules; X's Developer Guidelines (docs.x.com/developer-guidelines) restate both as an allowed-or-not table. We read the Policy and the Guidelines directly in 2026; the help-center page would not serve a non-browser request, so its wording below is what X's own search snippets return, summarized conservatively.

The Developer Policy, in its spam, bots and automation section, requires you to always get explicit consent before sending people automated replies or Direct Messages; immediately respect requests to opt out of being contacted; never perform bulk, aggressive or spammy actions, including bulk following; never post identical or substantially similar content across multiple accounts; and, if you operate an API-based bot account, clearly indicate what the account is and who is responsible for it.

The Developer Guidelines turn that into a table. Posting is allowed with no unsolicited @mentions and no identical cross-posting. Replying to users is conditional: only if the user engaged first, at most one reply per interaction. DMs: only after the user messaged you first, with an easy opt-out. Bulk, aggressive or automated following is prohibited. "App auto-replies to anyone mentioning a keyword" is listed as an unsolicited interaction: the keyword reply bot above. Every automated account must enable the Automated profile label, disclose its nature in the bio, link to a human-managed account, honor opt-outs immediately, and stay within rate limits.

The Automation rules on the help center allow campaigns that auto-reply to users who engage with your content and solutions that respond automatically in Direct Messages, forbid spamming, bothering or sending unsolicited messages to users, describe repeatedly posting duplicated and unsolicited replies to many accounts as spam behavior, and prohibit replying with potentially sensitive content, including profanity, unless the person asked for it in advance.

What each rule means for the code on this page:

Rule (source)What it allows or forbidsWhat the loop below does
Explicit consent before automated replies (Developer Policy)Answer only people who addressed youKeep a mention only if inReplyToUsername is your handle or the post is a top-level post naming you; drop mentions inside other people's threads
One reply per interaction (Developer Guidelines)No follow-ups, no replying to the repliesOne reply per author per conversationId, recorded in state
Honor opt-outs immediately (Developer Policy)Stop when askedA stop list checked before classification; "stop", "unsubscribe" and similar add the author to it
No identical or substantially similar content (Developer Policy)Vary what you post; never carpet the timeline with one templateEach answer template may be used at most 3 times an hour; above that the mention goes to a human
No bulk or aggressive actions (Developer Policy)A human paceA hard cap of 6 replies an hour and a random 45 to 150 s gap between replies
No unsolicited keyword replies (Developer Guidelines)No search-triggered repliesThe loop reads /twitter/user/mentions only; it never calls search
No sensitive content to people who did not ask (Automation rules)Keep answers plainTemplates are fixed text; a language-model draft, if you add one, is filtered before posting
Automated label, bio disclosure, human account linked (Developer Guidelines)Readers can tell it is a botAccount setting, not code: enable it before the first automated reply

One caution: these rules apply to the account that posts, whichever API carries the request. Using twitterapi.io for the write changes the price ($0.015 per post created on the official rate card against $0.002 here), not what X allows your account to do.

02 — Section

The mention-listener pattern: read with a key, write as the account

The loop has three parts and two kinds of authentication, the most useful thing to understand before writing code.

Listen (read). GET https://api.twitterapi.io/twitter/user/mentions with userName (your handle, no @), sinceTime and untilTime in Unix seconds, and cursor for the next page. It needs only the X-API-Key header: nothing logs in to X. Each page holds 20 mentions, newest first, as a top-level tweets array with has_next_page and next_cursor beside it; each tweet carries id, text, createdAt, conversationId, isReply, inReplyToUsername, quoted_tweet, retweeted_tweet and an author object with userName. The endpoint returned none of the account's own posts in our 1,600-mention sample. Reads are 15 credits per mention returned ($0.15 per 1,000), with a 15-credit ($0.00015) minimum for a check that finds nothing.

Classify and cap. Pure code, no API: is the mention addressed to you, does it ask something you can answer, has this person in this thread been answered already, does the hourly budget allow one more reply. A keyword table covers a support desk with five known intents; a language model classifies free text better but needs an output filter. Either way the model never talks to X; your code decides what to post.

Reply (write). POST /twitter/create_tweet_v2 with a JSON body of login_cookies, tweet_text, proxy and reply_to_tweet_id set to the mention's id (the older /twitter/create_tweet calls it in_reply_to_tweet_id). The cookies come from one POST /twitter/user_login_v2 call with user_name, email, password, an optional totp_secret and a proxy you supply; it costs $0.10 and returns login_cookies you store and reuse. Each reply is $0.002, billed per HTTP 200 call. The call acts as your X account: X can rate-limit, lock or suspend it if the pace looks automated, so the caps are part of the product; and the session may need a fresh login when X rejects the cookies, which the code handles by logging in again once and then stopping.

Poll or push? This guide polls. The push alternative is a filter rule: POST /oapi/tweet_filter/add_rule with a tag, a value such as @YourHandle and an interval_seconds, delivered to a webhook or websocket you run; the Twitter webhooks guide covers receivers and retries. Push wins when seconds matter or you watch many handles; for one support account answering a few questions a day, a 5-minute poll is cheaper and simpler, and the hour-of-day data below shows even the busiest hours do not need faster.

Official X API (pay-per-usage)twitterapi.io
Read a mention$0.005 per post read$0.15 per 1,000 mentions returned (15 credits each); $0.00015 minimum for an empty check
Post a reply$0.015 per post created; $0.20 if the reply contains a URL$0.002 per HTTP 200 call
Per-reply ratio0.015 / 0.002 = 7.5x; with a URL, 0.20 / 0.002 = 100x
Session setupDeveloper account, app, OAuth user contextOne /twitter/user_login_v2 login, $0.10, with a proxy you supply
Rate ceilingPer-endpoint windows, see the official docs0.2 requests/s before the first payment, 20 once paid

The official route is the right one if you need the account's private data (notifications, your own DMs) or an institutional contract. For a support auto reply on public mentions, the per-reply ratio is the comparison; the compliance obligations are identical on both routes.

03 — Section

Step 1: read new mentions and keep only the ones addressed to you

A mention is any post that contains your handle, and most are not for you: when someone replies to a friend inside a thread that started under your post, X adds your handle to that reply. Answer it and you have put a bot into a conversation between two other people, which is what the consent rule forbids.

The filter is one field. If inReplyToUsername equals your handle, the mention is a reply under your own post: solicited. If inReplyToUsername is somebody else, or isReply is true, the mention sits inside another person's thread: skip it. Otherwise it is a top-level post that names you (a cold mention, in the sense that there is no thread yet), or a quote of your post: solicited. One data quirk from the sample: 157 of 1,600 mentions (10%) had isReply false while inReplyToUsername was set, so test the username field first and treat isReply as the fallback, which is what addressed_to_us() in the script does.

How much that filter removes. Across the 8 accounts, 894 of 1,600 mentions (56%) were inside somebody else's thread, 382 (24%) were replies under the account's own posts, 277 (17%) were top-level posts addressed to the account, and 47 (3%) were quotes. The share addressed to the account ran from 30% (@vercel) to 75% (@AWSSupport). For seven of the eight, "inside someone else's thread" was the largest class; @AWSSupport, whose mentions were mostly replies under its own status posts, was the exception.

Then the question test. 18% of all mentions contained a question mark, but the share depends on where the mention sits. For the four support desks, a top-level post addressed to the account contained a question mark 38% to 52% of the time (@CloudflareHelp 38%, @AWSSupport 41%, @XboxSupport 44%, @StripeSupport 52%), a reply under the account's own post 25% to 33%, and a mention inside somebody else's thread only 7% to 16%. A question mark is a crude proxy (people also write "my console won't turn on" as a statement), so treat it as the first gate and let the intent classifier decide the rest.

Dedupe on the ID, not the time. Tweet IDs increase with time, so "larger than the last ID handled" is a complete rule that survives restarts. Pass sinceTime as well so a quiet poll returns nothing and costs the $0.00015 minimum instead of a full page of old mentions ($0.003). The code keeps a two-minute overlap on sinceTime, lets the ID rule remove duplicates, and on its very first run records the newest ID and answers nothing, so switching the bot on never replies to last month's backlog.

Repeat people. 29% to 58% of mentions per account came from an author who had already appeared in the same 200 mentions; one author alone accounted for 15 of @AWSSupport's 200. That is why the state file keys answered threads by author and conversationId: the one-reply-per-interaction rule is about people, and people come back.

python
def fetch_new_mentions(handle, since_id, since_time):
    """All mentions newer than since_id, oldest first. Read-only: X-API-Key, no X login."""
    params = {"userName": handle, "sinceTime": since_time}
    out = []
    while True:
        r = requests.get("https://api.twitterapi.io/twitter/user/mentions",
                         headers={"X-API-Key": os.environ["TWITTERAPI_IO_API_KEY"]},
                         params=params, timeout=30)
        r.raise_for_status()
        body = r.json()
        if body.get("status") == "error":
            raise RuntimeError(body.get("msg") or "twitterapi.io error")
        tweets = body.get("tweets") or []          # 20 per page, newest first
        out += [t for t in tweets if int(t["id"]) > since_id
                and ((t.get("author") or {}).get("userName") or "").lower() != handle.lower()]
        if not tweets or not body.get("has_next_page") or not body.get("next_cursor"):
            break
        if int(tweets[-1]["id"]) <= since_id:       # the rest of the pages are older than our marker
            break
        params["cursor"] = body["next_cursor"]
    return sorted(out, key=lambda t: int(t["id"]))


def addressed_to_us(t, handle):
    """Solicited = under our own post, or a top-level post / quote that names us. Everything else is
    somebody else's conversation and is skipped (X Developer Policy: explicit consent first)."""
    parent = (t.get("inReplyToUsername") or "").lower()
    if parent == handle.lower():
        return True                      # reply under our own post
    if parent or t.get("isReply"):
        return False                     # inside another person's thread (56% of our sample)
    return not t.get("retweeted_tweet")  # top-level post or quote addressed to us
Horizontal stacked bar chart of where the 200 most recent mentions of each of 8 organization accounts sit. Replies under the account's own post: AWSSupport 62%, StripeSupport 27%, Docker 24%, NASA 22%, XboxSupport 19%, vercel 15%, github 12%, CloudflareHelp 10%. Top-level posts addressed to the account: Docker 26%, CloudflareHelp 26%, github 22%, AWSSupport 14%, vercel 14%, StripeSupport 12%, NASA 12%, XboxSupport 12%. Inside somebody else's thread: vercel 68%, XboxSupport 66%, github 64%, NASA 64%, StripeSupport 60%, CloudflareHelp 60%, Docker 45%, AWSSupport 21%. Quotes 2 to 5%. Across all 1,600: 56% inside other people's threads, 41% addressed to the account, 18% contain a question mark.
Where a mention sits, per account. Only the blue (reply under the account's own post) and orange (top-level post naming the account) parts are addressed to the account. 200 most recent mentions per account, collected in 2026.
04 — Section

Step 2 and 3: classify, cap, then reply as the account

Classify. The script ships a keyword classifier with three intents (service status, billing, how-to) and a fixed answer per intent. It is deliberately small: a support desk's auto reply should handle the five questions it gets every day and hand everything else to a person. If you replace classify() with a language-model call, the model returns an intent label and a draft, never a decision to post, and the draft passes a length check, a profanity check and a "does it contain a URL" check (a URL turns a $0.015 official-API reply into a $0.20 one, and on any route a link in an automated reply reads as promotion). The model side is in the Twitter chatbot guide.

Stop list first. Before classification, the script checks the text for "stop", "unsubscribe" and similar, adds the author to a stop list and never answers them again. Opt-outs are honored immediately, so the check runs before anything else and the list is part of the saved state.

The caps, and where their values come from. Each number below is from the 1,600-mention sample; each is a ceiling to lower, not a target.

Cap in the scriptValueWhy that value
MAX_REPLIES_PER_HOUR6The busiest support desk (@XboxSupport) had about 6 addressed-with-question mentions a day; the busiest brand account (@NASA) about 39 a day, under 2 an hour. Six an hour is above every account's average and still far from "aggressive"
One reply per author per conversationIdalwaysOne reply per interaction (Developer Guidelines); 29% to 58% of mentions were from repeat authors
Template reuse3 per template per hourIdentical or substantially similar content is the spam signal the Developer Policy names
MIN_GAP_SECONDS45 to 150 s, randomHuman pace; in the busiest hour (61 mentions) the bot answers a fraction and leaves the rest
QUIET_HOURS_UTC00:00 to 05:59The six quietest UTC hours held 16% of mentions; the loop skips polling then and catches up by sinceTime at 06:00
POLL_SECONDS300A 5-minute poll costs $1.30 to $4.47 a month at the sample's rates; 60 s costs $6.48 to $7.70 and finds nothing on 26% to 98% of hours for five of the eight accounts

Reply. For a mention that passed every gate, the script calls /twitter/create_tweet_v2 with reply_to_tweet_id set to the mention's id. It logs in once with /twitter/user_login_v2 and stores the login_cookies in the state file; the API reference recommends 2FA with totp_secret (the base32 seed, not a 6-digit code), because cookies obtained without it may fail to post. If a reply returns status: error with a message about the session, the script logs in again once ($0.10) and retries; a second failure stops the run, because a login that keeps failing is a locked account, not a bug to loop on. Every HTTP 200 reply is billed ($0.002) whether or not X later removes the post, so dry-run mode, which prints the planned replies and posts nothing, is the default.

When X says no. A status: error with a rate-limit or lock message means your account, not your API key, has been flagged: stop posting for the day, lower the caps, check that the Automated label is on, and have a person review what went out; the Error 226 guide describes the same enforcement from the user's side. An HTTP 429 from api.twitterapi.io is your own request rate (0.2 requests/s before the first payment, 20 once paid); the read loop backs off and retries, and it never touches the posting account. A desk that answers 6 of 61 mentions in its busiest hour and leaves 55 for people is doing its job; one that answers all 61 in a minute has written the evidence for its own suspension.

python
def login():
    """One /twitter/user_login_v2 call ($0.10) -> login_cookies. Store and reuse; re-login only when X rejects them."""
    body = {"user_name": os.environ["X_USERNAME"], "email": os.environ["X_EMAIL"],
            "password": os.environ["X_PASSWORD"], "proxy": os.environ["X_PROXY"]}
    if os.environ.get("X_TOTP_SECRET"):
        body["totp_secret"] = os.environ["X_TOTP_SECRET"]   # base32 seed; strongly recommended
    r = requests.post("https://api.twitterapi.io/twitter/user_login_v2", headers=HEADERS, json=body, timeout=120)
    r.raise_for_status()
    data = r.json()
    if data.get("status") != "success":
        raise RuntimeError(f"login failed: {data.get('message')}")
    return data["login_cookies"]


def post_reply(cookies, mention_id, text):
    """$0.002 per HTTP 200 call. Acts as the logged-in X account: keep the caps."""
    r = requests.post("https://api.twitterapi.io/twitter/create_tweet_v2", headers=HEADERS, timeout=60,
                      json={"login_cookies": cookies, "tweet_text": text,
                            "reply_to_tweet_id": mention_id, "proxy": os.environ["X_PROXY"]})
    r.raise_for_status()
    return r.json()   # {"status": "success" | "error", "message": ..., "tweet_id": ...}
Heatmap of the share of each account's mentions by UTC hour for the five accounts whose 200 mentions span at least a full day: XboxSupport (busiest hour 16 UTC, 12%), AWSSupport (17 UTC, 8%), StripeSupport (21 UTC, 8%), CloudflareHelp (12 UTC, 10%), Docker (18 UTC, 12%), plus an equal-weight pooled row (17 UTC, 8%). Afternoon and evening UTC hours are darker; 00 to 09 UTC are lightest. The 6 busiest hours, 13, 16, 17, 18, 20 and 21 UTC, hold 38% of mentions; the 6 quietest hold 16%.
When mentions arrive, by UTC hour, for the five accounts whose sample covers at least a full day. The script's quiet hours (00 to 05 UTC) come from this chart. Collected in 2026.
05 — Section

How fast and how much would a mention responder have to work? 1,600 mentions measured

Before fixing the caps we wanted the shape of the real workload: how many mentions an organization support desk gets, how bursty they are, and how many survive the filters above. We pulled the 200 most recent mentions of eight accounts with /twitter/user/mentions (10 pages each) in 2026: four support desks (@XboxSupport, @AWSSupport, @StripeSupport, @CloudflareHelp), three developer-tool brand accounts (@Docker, @github, @vercel) and one very large brand (@NASA).

The rate spans three orders of magnitude. Dividing 200 mentions by the hours they covered gives 0.03 mentions an hour for @CloudflareHelp (its 200 mentions reached back 300 days) and 41 an hour for @NASA (200 mentions in under five hours), a 1,368x spread. The three brand accounts (@NASA, @vercel, @github) ran at 9 to 41 an hour; the four support desks at 0.03 to 2.3. A support bot and a brand bot are different workloads; the same code handles both only because of the caps.

Bursts are much larger than averages. The busiest clock hour in each span held 6 to 61 mentions: for @Docker 17 against an average of 0.62, for @StripeSupport 12 against 0.26. A responder sized for the average is swamped by an outage or a launch; one sized for the burst posts far too fast the rest of the time. The hourly cap resolves this: in the burst it answers a few and leaves the rest for people.

Most hours are empty for most support desks. For the five slower accounts, 26% (@XboxSupport) to 98% (@CloudflareHelp) of clock hours in the span had no mention at all; those hours are what a 60-second poll mostly pays for, which is why sinceTime and the $0.00015 empty-check minimum matter.

The answerable set is small. The two filters (addressed to the account, contains a question mark) left 172 of 1,600 mentions, 11%. Per day at the measured rate: 0.1 (@CloudflareHelp), 0.5 (@AWSSupport), 0.8 (@StripeSupport), 0.9 (@Docker), 6.1 (@XboxSupport), 20.7 (@github), 33.2 (@vercel) and 39.4 (@NASA). Even for @NASA that is under 2 an hour.

AccountSpan of 200 mentionsAvg mentions/hBusiest hourHours with noneAddressed to accountWith question markAnswerable per day
@NASA4.9 h41.0610%34%17%39.4
@vercel11.6 h17.3380%30%16%33.2
@github22.0 h9.1180%34%16%20.7
@XboxSupport3.6 d2.3926%32%18%6.1
@Docker13.4 d0.621764%50%10%0.9
@StripeSupport32.1 d0.261286%39%22%0.8
@AWSSupport90.6 d0.09794%75%24%0.5
@CloudflareHelp300.4 d0.03698%37%24%0.1
All 841%18%11% of mentions

"Answerable" means addressed to the account and containing a question mark, scaled to a day at the account's average rate. "Hours with none" is the share of clock hours in the span with zero mentions.

When they arrive. For the five accounts whose sample covered at least a full day, the six busiest UTC hours (13, 16, 17, 18, 20, 21) held 38% of mentions and the six quietest (00, 06, 07, 08, 09, 23) held 16%; each account's single busiest hour held 8% to 12%. That is US daytime and European evening, which fits US-based companies; a desk with an Asian customer base should expect the mirror image and set its quiet hours accordingly. The three fast accounts are not in the heatmap because their 200 mentions did not reach around the clock.

What this changes in the design. A support auto reply needs no stream, queue or worker pool: a poll every few minutes, a filter that discards nine mentions in ten, a cap of a few replies an hour, and a hand-off to a person for the rest. The expensive part is writing answers that are correct for the five questions that recur.

Dumbbell chart on a log scale of mentions per hour for 8 organization accounts, average over the span of their 200 most recent mentions versus the busiest clock hour. NASA 41.04 average vs 61 peak over 4.87 hours; vercel 17.27 vs 38 over 11.58 hours; github 9.1 vs 18 over 21.98 hours; XboxSupport 2.3 vs 9 over 3.63 days; Docker 0.62 vs 17 over 13.42 days; StripeSupport 0.26 vs 12 over 32.07 days; AWSSupport 0.09 vs 7 over 90.63 days; CloudflareHelp 0.03 vs 6 over 300.43 days. Fastest 41.04 an hour, slowest 0.03, a 1,368x spread.
How fast a mention responder would have to work: average mentions per hour over the span of 200 mentions (blue) against the busiest clock hour (orange), log scale. Collected in 2026.
06 — Section

What a Twitter auto reply costs to run

Two meters run: reads (polling mentions) and writes (login and replies). Both are small for a support desk, and the read side depends on how often you poll, not on how many mentions you get.

Reads. A check that finds nothing costs the 15-credit minimum, $0.00015. Polling every 60 seconds for a month is 43,200 checks, a $6.48 floor; every 5 minutes is 8,640 checks, $1.30. Each mention returned is 15 credits ($0.15 per 1,000) and replaces the minimum on the check that returns it. At the sample's rates:

AccountAvg mentions/hMentions a month at that ratePoll every 60 sPoll every 5 min
@CloudflareHelp0.0320$6.48$1.30
@AWSSupport0.0966$6.48$1.30
@StripeSupport0.26187$6.48$1.30
@Docker0.62447$6.48$1.30
@XboxSupport2.31,652$6.48$1.32
@github9.16,552$6.55$1.59
@vercel17.312,438$6.72$2.17
@NASA41.029,550$7.70$4.47

The two columns barely move for the slow accounts because a returned mention replaces the minimum rather than adding to it; only @NASA's volume pushes the bill above the floor. Skipping the six quiet hours, as the script does, removes a quarter of the checks. On the official X API the same reads are $0.005 per post: @NASA's 29,550 mentions a month would be $147.75 of post reads against $4.43 here, before polling overhead.

Writes. One /twitter/user_login_v2 login is $0.10 and its cookies are reused across runs. Each reply is $0.002 per HTTP 200 call. The busiest support desk in the sample, @XboxSupport, had 6.1 answerable mentions a day, about 182 replies a month: $0.36 plus the $0.10 login on twitterapi.io, against $2.73 at the official rate card's $0.015 per post created (0.015 / 0.002 = 7.5x per reply, before the login). For @NASA's 39.4 a day, about 1,182 replies a month: $2.36 plus $0.10 here against $17.73 there. If any reply carried a link, the official price is $0.20 per post and the gap is 100x, one more reason the templates in the script contain no URLs. The write path also needs a proxy you supply (the API reference asks for high-quality residential proxies and warns against free ones), a separate line item that twitterapi.io does not price.

Free credits. Up to $1 in free credits for new accounts that pass the bot check, enough to run the dry-run and the read loop for a month at a 5-minute poll; expires after 31 days; see /pricing. Purchased credits do not expire.

07 — Section

How we collected this

Question: how fast and how much would a mention responder have to work for an organization support or brand account: mentions per hour (average and busiest hour), the share addressed to the account versus inside other people's threads, the share containing a question mark, the hour-of-day distribution, and the cost of polling at 60 s and 5 min.

Accounts: @XboxSupport, @AWSSupport, @StripeSupport, @CloudflareHelp, @Docker, @github, @vercel and @NASA. We first checked twelve candidate handles with /twitter/user/info: @VercelSupport and @GitHubSupport do not exist, @AskPlayStation resolved to an account with 4 posts, and we used @CloudflareHelp rather than the @Cloudflare brand account because the question is about support desks.

Method: GET /twitter/user/mentions with userName, following next_cursor for 10 pages (20 mentions each) per account, through a rate-limited read-only client: 80 mention requests plus 12 profile checks, 92 requests and 1,600 mentions, collected in 2026. Rates are 200 mentions divided by the hours between the newest and oldest; hourly buckets include empty hours; "addressed" means inReplyToUsername equals the handle or the post is top-level; the question test is a literal question mark in text. Polling costs use the published prices (15 credits per mention returned, 15-credit minimum per request) and assume Poisson arrivals at the measured rate. The script on this page was run in dry-run mode against the recorded pages and posted nothing.

Privacy: the people who mention these accounts are private individuals. Raw pages were kept outside the study folder and deleted after analysis; the study keeps aggregates and de-identified rows (hour, kind, question flag, text length) with no IDs, URLs, text, author IDs or handles. Only the organization accounts are named.

What the sample cannot show: 200 mentions is a snapshot, not a month; for @NASA it is five hours and for @CloudflareHelp ten months, so the per-hour rates average over very different windows and the hour-of-day chart uses only the five accounts whose sample covered a full day. A question mark undercounts requests phrased as statements and overcounts rhetorical questions. Replies X hides or filters as spam may be missing from the mentions endpoint. None of this is about twitterapi.io customers or what they build.

python
"""auto_reply.py - a rule-abiding Twitter (X) auto reply (mention responder) on twitterapi.io.

    pip install requests
    export TWITTERAPI_IO_API_KEY=...                       # reads: key from the twitterapi.io dashboard
    export X_USERNAME=... X_EMAIL=... X_PASSWORD=...       # writes: the X account that will reply
    export X_TOTP_SECRET=...  X_PROXY=http://user:pass@host:port   # 2FA seed (recommended), proxy you supply
    python auto_reply.py --handle YourBrandHelp             # dry run (default): prints what it would answer
    python auto_reply.py --handle YourBrandHelp --live      # posts, as that account

Reads need only the API key. Writes act as the X account you log in: X can rate-limit, lock or suspend
it, so the caps are part of the product. Login $0.10 (cookies reused); each reply $0.002, billed per
HTTP 200 call. Reads: $0.15 per 1,000 mentions returned, $0.00015 minimum for an empty check.
"""
import argparse
import json
import os
import random
import sys
import time
from datetime import datetime, timezone

import requests

HEADERS = {"X-API-Key": os.environ.get("TWITTERAPI_IO_API_KEY", "")}
STATE_FILE = "auto_reply_state.json"

# ---- caps. Values come from 1,600 mentions of 8 organization accounts (see the study). Lower them, do not raise them.
MAX_REPLIES_PER_HOUR = 6          # busiest support desk in the sample: ~6 answerable mentions a DAY
TEMPLATE_PER_HOUR = 3             # no identical / substantially similar content in bulk
MIN_GAP_SECONDS = (45, 150)       # human pace between two replies
QUIET_HOURS_UTC = range(0, 6)     # 00-05 UTC: the quietest hours; skip polling, catch up via sinceTime
POLL_SECONDS = 300                # 5 min: $1.30/month floor. 60 s would be $6.48.
STOP_WORDS = ("stop", "unsubscribe", "do not reply", "don't reply", "leave me alone")

INTENTS = {  # keyword classifier. Swap classify() for an LLM if you need more; keep the output filter.
    "status": (("down", "outage", "not loading", "502", "503", "is it just me"),
               "We're looking into this now. Our status page carries live updates; reply here if it still fails after that."),
    "billing": (("refund", "charged", "invoice", "billing", "payment"),
                "Sorry about the billing trouble. Please DM us the email on the account (not here) and we'll look it up."),
    "howto": (("how do i", "how to", "where is", "can i", "is there a way"),
              "Good question. Reply with what you've tried so far and we'll point you to the right doc."),
}


def load_state():
    if os.path.exists(STATE_FILE):
        return json.load(open(STATE_FILE))
    return {"since_id": 0, "since_time": 0, "login_cookies": None, "stop_list": [], "answered": [], "sent": [], "templates": []}


def save_state(state):
    json.dump(state, open(STATE_FILE, "w"), indent=1)


def fetch_new_mentions(handle, since_id, since_time):
    """All mentions newer than since_id, oldest first. Read-only: X-API-Key, no X login."""
    params = {"userName": handle, "sinceTime": since_time}
    out = []
    for _ in range(10):  # hard cap on pages per poll
        r = requests.get("https://api.twitterapi.io/twitter/user/mentions", headers=HEADERS, params=params, timeout=30)
        if r.status_code == 429:  # our own request rate (0.2/s before the first payment), not the X account
            time.sleep(10)
            continue
        r.raise_for_status()
        body = r.json()
        if body.get("status") == "error":
            raise RuntimeError(body.get("msg") or "twitterapi.io error")
        tweets = body.get("tweets") or []  # 20 per page, newest first
        out += [t for t in tweets if int(t["id"]) > since_id
                and ((t.get("author") or {}).get("userName") or "").lower() != handle.lower()]
        if not tweets or not body.get("has_next_page") or not body.get("next_cursor") or int(tweets[-1]["id"]) <= since_id:
            break
        params["cursor"] = body["next_cursor"]
    return sorted(out, key=lambda t: int(t["id"]))


def addressed_to_us(t, handle):
    """Solicited = a reply under our own post, or a top-level post / quote that names us."""
    parent = (t.get("inReplyToUsername") or "").lower()
    if parent == handle.lower():
        return True
    if parent or t.get("isReply"):
        return False  # somebody else's conversation: 56% of mentions in our sample
    return not t.get("retweeted_tweet")


def classify(text):
    """-> (intent, answer). 'stop' means opt out. None means hand it to a human."""
    low = text.lower()
    if any(w in low for w in STOP_WORDS):
        return "stop", None
    if "?" not in text:  # only explicit questions; 18% of mentions in our sample
        return None, None
    for intent, (keys, answer) in INTENTS.items():
        if any(k in low for k in keys):
            return intent, answer
    return None, None


def login():
    """One /twitter/user_login_v2 call ($0.10). Store the cookies; log in again only when X rejects them."""
    body = {"user_name": os.environ["X_USERNAME"], "email": os.environ["X_EMAIL"],
            "password": os.environ["X_PASSWORD"], "proxy": os.environ["X_PROXY"]}
    if os.environ.get("X_TOTP_SECRET"):
        body["totp_secret"] = os.environ["X_TOTP_SECRET"]
    r = requests.post("https://api.twitterapi.io/twitter/user_login_v2", headers=HEADERS, json=body, timeout=120)
    r.raise_for_status()
    data = r.json()
    if data.get("status") != "success":
        raise RuntimeError(f"login failed: {data.get('message')}")
    return data["login_cookies"]


def post_reply(cookies, mention_id, text):
    """$0.002 per HTTP 200 call. Acts as the logged-in X account."""
    r = requests.post("https://api.twitterapi.io/twitter/create_tweet_v2", headers=HEADERS, timeout=60,
                      json={"login_cookies": cookies, "tweet_text": text,
                            "reply_to_tweet_id": mention_id, "proxy": os.environ["X_PROXY"]})
    r.raise_for_status()
    return r.json()


def reply_with_relogin(state, mention_id, text):
    if not state["login_cookies"]:
        state["login_cookies"] = login()
    res = post_reply(state["login_cookies"], mention_id, text)
    if res.get("status") != "success":
        msg = (res.get("message") or "").lower()
        if "login" in msg or "cookie" in msg or "session" in msg:
            state["login_cookies"] = login()  # once; a second failure stops the run
            res = post_reply(state["login_cookies"], mention_id, text)
        if res.get("status") != "success":
            raise RuntimeError(f"reply refused: {res.get('message')}. Stop and review the account.")
    return res


def run_once(handle, state, live):
    now = int(time.time())
    if datetime.now(timezone.utc).hour in QUIET_HOURS_UTC:
        return  # no poll, no charge; sinceTime catches up at 06:00 UTC
    state["sent"] = [ts for ts in state["sent"] if ts > now - 3600]
    state["templates"] = [x for x in state["templates"] if x[1] > now - 3600]
    mentions = fetch_new_mentions(handle, state["since_id"], state["since_time"] or now - 3600)
    if state["since_id"] == 0:  # first run: record the newest mention and answer nothing
        state["since_id"] = max([int(t["id"]) for t in mentions] + [1])
        state["since_time"] = now - 120
        return
    for t in mentions:
        state["since_id"] = max(state["since_id"], int(t["id"]))
        author = ((t.get("author") or {}).get("userName") or "").lower()
        intent, answer = classify(t.get("text") or "")
        if intent == "stop":
            state["stop_list"].append(author)  # opt-out honored immediately
            continue
        if author in state["stop_list"] or not addressed_to_us(t, handle) or not answer:
            continue
        thread_key = f"{author}:{t.get('conversationId')}"
        if thread_key in state["answered"]:
            continue  # one reply per person per thread
        if len(state["sent"]) >= MAX_REPLIES_PER_HOUR:
            print("hourly cap reached; the rest waits for a human", file=sys.stderr)
            break
        if sum(1 for x in state["templates"] if x[0] == intent) >= TEMPLATE_PER_HOUR:
            print(f"template {intent} used {TEMPLATE_PER_HOUR}x this hour; skipping", file=sys.stderr)
            continue
        if live:
            reply_with_relogin(state, t["id"], answer)
        print(f"{'POSTED' if live else 'DRY RUN'} reply to {t['id']} [{intent}]: {answer}")
        state["sent"].append(now)
        state["templates"].append([intent, now])
        state["answered"].append(thread_key)
        save_state(state)
        if live:
            time.sleep(random.uniform(*MIN_GAP_SECONDS))
    state["since_time"] = now - 120  # 2-minute overlap; the ID rule removes duplicates
    save_state(state)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--handle", required=True, help="your account's handle, without @")
    ap.add_argument("--live", action="store_true", help="post replies (default: dry run)")
    ap.add_argument("--once", action="store_true", help="one poll, then exit")
    a = ap.parse_args()
    if not HEADERS["X-API-Key"]:
        sys.exit("Set TWITTERAPI_IO_API_KEY first.")
    state = load_state()
    while True:
        run_once(a.handle, state, a.live)
        if a.once:
            break
        time.sleep(POLL_SECONDS)


if __name__ == "__main__":
    main()
08 — Questions

Questions readers ask

Is a Twitter (X) auto reply against the rules?

Not when the person engaged first. X's Developer Policy requires explicit consent before automated replies, one reply per interaction, immediate opt-outs, and no identical content across accounts; the Developer Guidelines list replying to users as allowed only if the user engaged first. A bot that answers mentions of your account and replies under your own posts meets that. A bot that replies to strangers because their post matched a keyword is listed as an unsolicited interaction and is the pattern that gets accounts suspended.

Do I need the Automated label for an auto reply bot?

Yes. X's Developer Guidelines require every automated account using the API to enable the Automated profile label, disclose its nature in the bio, link to a human-managed account, and honor opt-out requests immediately. It is an account setting, not code, and it should be on before the first automated reply goes out.

How do I auto reply to mentions on Twitter with an API?

Poll GET /twitter/user/mentions with your handle and a sinceTime, keep only mentions whose inReplyToUsername is your handle or that are top-level posts naming you, classify the text, apply your caps, and POST /twitter/create_tweet_v2 with reply_to_tweet_id set to the mention's id and the login_cookies from a /twitter/user_login_v2 login. The read needs only an API key; the write acts as your X account.

How many replies per hour is safe for a Twitter auto reply?

There is no published number; X judges behavior. The script on this page caps at 6 replies an hour with a random 45 to 150 second gap, one reply per person per thread, and 3 uses of any one answer template per hour. In our 1,600-mention sample the busiest support desk had about 6 answerable mentions a day, so the cap is far above what a support desk needs and still far from aggressive. Lower it; do not raise it.

Can an auto reply bot reply to tweets that contain a keyword?

No. Replying to people who never addressed you because their post matched a search is the case X's Developer Guidelines name as an unsolicited interaction, and the Automation rules describe duplicated unsolicited replies to many accounts as spam. The loop on this page reads only your own mentions and never calls search, so it cannot drift into that pattern.

What does a Twitter auto reply cost to run?

Reads: polling /twitter/user/mentions every 5 minutes costs $1.30 a month when nothing is found, plus $0.15 per 1,000 mentions returned; in our sample that was $1.30 to $4.47 a month per account. Writes: one $0.10 login per session and $0.002 per reply, billed per HTTP 200 call, against $0.015 per post created on the official X API ($0.20 with a URL), 7.5x per reply. A support desk answering 6 questions a day spends under $1 a month on replies.

Should my auto reply use a language model?

Only behind a filter. A keyword table handles the five recurring questions a support desk gets; a model classifies free text better but must return a label and a draft, not a decision to post, and the draft should be checked for length, profanity and links before it goes out. The Twitter chatbot guide covers the model side; this page's script ships a keyword classifier and a hand-off for everything else.

Why does my bot reply inside other people's conversations?

Because a mention includes every reply in a thread that started under your post, X adds your handle to replies between other people. 56% of mentions in our sample were that. Test inReplyToUsername against your handle and skip anything else that is a reply; 10% of mentions had isReply false with a parent set, so test the username field first.

09 — Further reading

Continue

Sources & further reading
More from this series
Build it

Stop reading. Start building.

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

Get an API key