X Mention Bot
What ToolGPT is on X
ToolGPT is the public-facing X/Twitter bot identity backed by the Tools X Mention Bot.
In practical terms, that means:
- the X account is the visible public bot persona on X
- the bot reads mentions or configured watch-keyword posts through the X API
- Tools builds the reply context, applies local safety/routing rules, and stores the interaction history
- candidate replies are generated inside Tools first and may stay dry-run/manual-review-only depending on the current settings
- the public X account is therefore only the visible posting identity; the real control room lives in Tools admin
So if you see ToolGPT on X, that is the same bot system documented on this page.
For the public release history, see:
Public FAQ
“How does ToolGPT build a reply?”
At a public level, the reply flow is:
- Tools ingests either one direct
@mention / reply or one configured watch-keyword match.
- Tools collects visible thread context, plus optional linked-page excerpts or RSS evidence when those features are enabled and relevant.
- A built-in neutral base prompt is always applied first.
- The admin-visible fallback custom instruction and free-text fallback mood are then applied, and the best-matching keyword rule may override them for that one reply.
- Tools generates a short X-sized candidate reply, keeps it in dry-run/manual review/live-post flow depending on current settings, and stores the interaction history.
“Is ToolGPT communist, anti-racist, woke, or anti-right?”
No political ideology is configured as the bot's goal.
The built-in base prompt tells it to:
- not try to win the debate for a political side
- separate fact, law, morality, and opinion
- avoid collective blame
- push back on dehumanization, racist generalizations, and threats in neutral wording
- avoid loaded labels unless they are clearly necessary and supported by context
In angry threads that can still look politically loaded from the outside, but the intended behavior is rule-of-law style neutrality and platform-safe moderation boundaries — not party campaigning.
“Is ToolGPT just an echo of Thomas / Tornevall?”
Not by design.
Public replies are shaped by:
- a fixed built-in neutral base prompt
- the visible thread context Tools fetched
- admin-visible fallback instruction and fallback mood
- optional keyword-rule overrides for the current interaction
- X-specific brevity and safety constraints
Operator settings do influence tone and fallback direction, but the system is not supposed to simply mirror one person's temper, political tribe, or timeline voice.
“Are there hidden prompts?”
There is a built-in system layer, but the public documentation now describes the default prompt design openly instead of pretending that the bot has no guiding base prompt at all.
The public contract is that the base prompt is designed to:
- answer directly
- stay neutral, dry, and factual by default
- avoid dehumanization and collective guilt
- mention uncertainty when verification is missing
- avoid leaking secrets, credentials, or private runtime details
Tools still does not publish every abuse-prevention/internal edge-case line word-for-word, but the default prompt goals and behavior are public.
“Why does ToolGPT sometimes push back on collective blame, torture language, or dehumanizing wording?”
Because the built-in prompt explicitly separates legitimate debate about punishment, deportation where lawful, and protection for victims from calls for torture, collective punishment, threats, or treating groups as less than human.
That is both a neutrality choice and a platform-terms safeguard.
Public prompt design and neutrality baseline
ToolGPT now has a documented default prompt design even when the admin-visible fallback custom instruction is empty.
The public prompt stack is:
- Built-in neutral base prompt
- Fallback custom instruction (admin-visible additive guidance)
- Fallback mood (admin-visible free-text tone field)
- Best-matching reply-priority profile chosen from the configured keyword rules for the current interaction
- Current thread context + optional linked-page/RSS evidence + X reply constraints
The built-in base prompt is explicitly designed to:
- act as a factual debate-and-facts bot
- avoid trying to win for one political side
- separate fact, law, morality, and opinion
- avoid mirroring the user's rage, sarcasm, or insults
- avoid loaded first-response language such as
woke, communist, fascist, or racist unless that wording is directly necessary and supported
- describe problems more concretely, for example
collective blame, dehumanizing language, threats, or conflict with rule-of-law principles
- say when verification is still missing
- answer hypothetical/principle questions at the principle level instead of hiding behind irrelevant source disclaimers
This is also why the public documentation should describe the base prompt design openly. The safety/neutrality requirements are not there to secretly steer politics, but to keep ToolGPT inside platform terms, avoid abuse, and make the reply style more predictably neutral.
How keyword routing is meant to work
The keyword system is not supposed to mean “if one magic word appears, force one ideological answer.”
The intended model is closer to reply prioritization by helpfulness and direction.
Publicly, that means:
- the bot starts from the neutral built-in base prompt
- if no keyword profile matches strongly enough, it stays on the normal fallback instruction + fallback mood
- if one keyword profile is a clearly better match, that profile can change the priority and direction of the answer for that one interaction
- the goal is to answer the most relevant kind of question first, not to make the bot abandon neutrality
Examples of that priority thinking:
- if the incoming post is mainly about the X bot itself, prioritize a short answer that explains the bot, its scope, or its limits first
- if the incoming post is mainly asking for verification / sources / proof, prioritize a fact-check style answer first
- if the incoming post is mainly asking for a direct practical answer, prioritize the concrete answer first instead of drifting into meta-discussion
- if the incoming post is mostly bait, weak context, or a bad partial match, do not let a weak keyword hit override a more helpful default answer style
So the system should be understood as:
- "If you ask about the X bot, prioritize that answer direction first"
- not "If this keyword appears, force a political personality transplant"
Status
The first implementation is dry-run first.
Tools can now:
- store one primary X bot account
- poll explicit mentions from X
- poll public X posts that match configured watch keywords when explicit-mention-only mode is turned off
- continue watching already engaged conversation threads, so a later follow-up in the same thread does not always need to repeat
@ToolGPT
- store interactions idempotently
- sanitize visible X conversation context
- walk backwards through reply chains so older parent posts remain available when a short follow-up depends on earlier thread context
- generate candidate AI replies
- keep those replies in Tools for review
- mark obvious refusal/decline replies as machine-readable do not publish candidates
- preserve real provider/policy error text more clearly when one structured reply attempt fails internally
- manage opt-outs and local rate limits
- apply fallback custom instructions + fallback mood
- use keyword-based reply-priority profiles to steer the answer toward the most helpful direction for that one interaction
- analyze all active keyword rules and choose the strongest helpful match instead of blindly taking the first weak hit
- detect Swedish/English verification wording more reliably, so requests about checking facts, sources, or proof can enter the stronger fact-check path instead of staying in a generic ask flow
- always run an internal RSS/feed-archive lookup pass when RSS evidence is enabled, using OpenAI-selected search terms first and then a local SQL query against the Tools archive to build extra optional context
- treat broader “what are the most relevant/latest news from the feed today?” wording as a feed-archive lookup too, even when the post never literally says
search RSS, so ToolGPT can summarize recent feed items instead of missing the RSS lookup entirely
- keep that RSS/archive material as optional supporting context only: if the direct X-thread answer is already clear, ToolGPT should not force the archive into the first answer just because matching feed rows happened to exist
- treat reasoning effort and web search as separate controls: stronger verification prompts can now request actual OpenAI web-search tooling when the current environment supports it, instead of only increasing reasoning effort
- apply a stricter independent-verification policy: if one interaction requires real web search but OpenAI does not actually use that web-search tool, the stored reply is no longer treated as independently verified. ToolGPT now retries once in a normal reply mode without web search instead of posting a repetitive boilerplate sentence about missing verification.
- when users ask whether documentation/changelog exists or what ToolGPT/SocialGPT supports, the bot now also tries to attach evidence from the public Tools docs/changelog pages before answering, instead of defaulting to a generic “no docs” style guess.
- save core admin config fields inline with AJAX/on-blur behavior instead of forcing a full page refresh for every small change
- auto-approve queued replies after a randomized delay window
- notify operators by mail and/or Slack while manual approval is still active
- forward recorded X-bot activity to Slack on demand through the Slack log routing category
audit_x_bot, so operators can deliberately mirror the bot's moderation/runtime event stream into Slack without having to treat every Slack message as a manual-review notification
- escalate provider-side spend-cap / credits-exhausted runtime blocks automatically: when the scheduled X-bot poller detects that the current project/account budget stopped the bot, Tools can now send an alert mail to the configured support recipient and one SMS alert to the configured site-owner mobile number until the next successful run clears the incident
- strip accidental raw JSON reply envelopes before storing/posting a public reply, so replies that were supposed to say something like
Here is a simple blueberry-pie recipe... no longer risk leaking a literal { "message": ... } payload into the public X thread
- expose admin + ToolsAPI views for diagnostics and review
- keep clearly unengaged follow-up tweets in a visible Do not publish state instead of letting them look publishable until X rejects the post attempt
- avoid applying one old rule override too eagerly when the newest tweet is only a bare mention plus a link
- let operators tune how many structured reply-generation attempts Tools should try before giving up (currently supported range: 1–5)
- keep a slightly softer local reply target below X's hard 260-character cap, so the final post is more likely to end on a complete sentence instead of being squeezed right up against the limit
- count any visible
@author prefix inside the final posted reply length rather than treating that tag as free extra space
- run one last plain-text direct-reply recovery pass when the structured attempt ladder ends in an opaque non-replyable result with no useful explanation
- deprioritize known AI accounts such as
grok as reply focus when ordinary non-AI participants are also active in the same thread, so ToolGPT can keep discussing the human participants instead of getting dragged into AI bait
- treat “no keyword rule matched” as an ordinary fallback-to-default-instruction case rather than as a reason to refuse by itself
- expose an opt-out management section where operators can review all stored opt-outs, opt one account back in, define usernames/X user ids that must always bypass opt-out, or temporarily override all stored opt-outs globally
- keep
@Tornevall permanently protected from effective X-bot opt-out blocking so the main operator account can still participate even if an old opt-out row was stored earlier
- paginate the Recent interactions admin list so operators can move through older stored interaction pages instead of loading one larger latest-only batch
- keep hard-blocked/non-postable diagnostics in their own separate section when operators explicitly choose to show non-publishable rows, so dead-end hard blocks do not get mixed back into the ordinary review queue
When ToolGPT deliberately stops a thread
ToolGPT is now allowed to stop certain dead-end reply loops on purpose.
Current public behavior:
- if a known AI participant such as
grok keeps repeating the same or nearly the same answer pattern
- and the thread is no longer progressing in a useful way
- Tools may mark that thread as finished instead of continuing the same exchange for hours
- but the safeguard is now anchored to the latest exchange, so one older repeated AI pattern should not keep blocking a thread after the conversation already moved on
- ToolGPT can now also run a separate recent-window repetition check on only the latest 5–7 visible posts when a human participant explicitly complains that the bot keeps repeating itself or when the recent exchange is clearly circling around the same point
- that newer recent-window safeguard is allowed to do one of two things: either tell ToolGPT to keep answering but move the discussion forward with a genuinely new angle / clarifying question, or pause the thread entirely when the latest exchange has become a repetitive dead-end
This is meant as a safeguard against repetitive bot-to-bot loops, not as a hidden moderation trick.
The goal is to:
- stop non-productive repetition
- avoid wasting premium-only reply opportunities on dead-end loops
- leave room for genuinely new mentions or useful follow-ups later
Reprocessing stuck interactions
When one interaction looks stuck or appears to have picked the wrong rule/routing, an operator can reprocess it from the admin UI.
Public behavior:
- reprocessing uses the latest saved X-bot settings
- rule selection is analyzed again before a new candidate reply is generated
- previously blocked or non-publishable candidates can therefore get a fresh attempt after rules, instruction, mood, or other settings have changed
Reply length behavior
If the local Tools reply cap is set to 0, Tools no longer applies an extra stricter cap of its own.
However, Tools still keeps the normal X hard reply limit before posting, so overlong replies are still shortened instead of being sent unchanged.
X-side posting credentials
Publicly, the important part is simply this:
- read-only mention polling can work with app-level read access alone
- actually posting replies requires valid X-side user-context write credentials for the bot account
- even when those credentials exist, X can still reject posting if the X app itself is not configured for write access
- the bot's runtime behavior is still controlled from Tools admin, while the underlying X-side app/account permissions remain an operator responsibility outside the public bot contract
Admin page
Use the Social Media Tools admin page for the X bot to:
- enable/disable the bot
- enable/disable reply generation
- keep dry-run on/off
- control polling
- review recent interactions
- review how many shortening reruns a stored candidate reply needed before it fit the current character limit
- inspect the latest per-reply length-fix history directly in the recent interactions table when a reply needed one or more shortening reruns
- approve/post one stored candidate reply
- reprocess / approve / skip recent interactions through in-page AJAX actions
- opt one author back in directly from a recent interaction card when a stored opt-out turned out to be a false positive
- let Tools auto-approve replies after a randomized delay instead of replying on the exact cron second
- skip interactions
- manage opt-outs
- maintain keyword-based reply-priority profiles that help Tools decide which answer direction should be prioritized for the incoming mention
- inspect diagnostics
Recent interaction cards now also surface hard refusals more clearly:
- if the stored candidate itself is non-replyable, the card shows a dedicated blocked-candidate warning instead of hiding that only in the expandable diagnostics
- if X would reject the post anyway (for example because the bot was never explicitly engaged in that thread), the card shows a stronger hard-block warning with the reason and keeps Approve/post disabled
- the Structured reply max attempts setting now rejects out-of-range values with an explicit 1–5 explanation instead of only a vague invalid-data failure
The admin page is now grouped into clearer operator panels for:
- top-level bot status and quick actions
- configuration and enabled modes
- fallback instruction/mood plus keyword-based reply-priority routing
- diagnostics and identity-resolution blockers
- persistent runtime warnings when a provider-side billing/spend cap blocks the bot
- opt-outs
- recent interactions
- a dedicated event-log page for the large runtime history, so the main X-bot overview can stay focused on live controls and the current review queue
The diagnostics panel now also explains the most common confusion directly:
- read-only mention polling can work with bearer/app credentials alone
- reply posting still requires user-context write credentials
- if the required X-side user-context write credentials are missing, Tools can still generate a candidate reply but cannot publish it to X
The AI/routing section now also explains the earlier grok confusion more clearly:
- Known AI accounts in thread context means accounts such as
grok that the bot should recognize as other AI participants in a conversation
- Directed-command delegates means ordinary trusted usernames that may ask the bot to address another account explicitly
- Trusted admin X usernames means extra-trusted operator usernames that the bot should listen to even more closely for directed-account commands
- Directed-command blocked target accounts means accounts such as
grok that the bot must not be told to approach through directed-account mode
Opt-out handling is now stricter about intent. Generic phrases such as stop being stupid or Swedish sluta vara idiot are no longer treated as opt-out commands just because they contain stop or sluta. Tools now expects clearer opt-out wording such as opt out, stop replying, or sluta svara before it stores a real opt-out.
The AI section now also includes two new fallback fields:
- Fallback custom instruction: the main operator instruction used when no stronger reply-priority profile matches
- Fallback mood: the default free-text tone field that applies when no reply-priority profile overrides it. The current neutral default is
neutral, dry, factual, but operators can still write another short tone description there.
- Required closing hashtags: optional advanced field, one hashtag per line, for example
#NewsTag and #ExampleTag; leave it empty unless you explicitly want fixed closing hashtags. When used, Tools moves those configured hashtags to the very end of the final reply, in the same order, even after local shortening or repost preparation
- Watch keywords / trigger words: words or phrases such as
Tornevall that let the poller ingest relevant public posts even when the bot was not explicitly @mentioned
- keyword-based reply profiles are best understood as a priority system for helpful answer direction: for example, "if the post is asking about the X bot itself, prioritize that answer first", or "if the post is mainly asking for verification, prioritize a fact-check style answer first"
- Context format: choose between the older flat text prompt and a nested JSON context payload
- Forward image URLs to AI: send actual X-exposed image/media URLs to OpenAI as multimodal inputs instead of relying only on text summaries
- Link handling enabled: when a visible X post contains external links, Tools may now fetch a compact server-side excerpt from those pages and include that excerpt in the AI context instead of expecting the model to browse the URL on its own
- Max context posts now accepts values up to
50
- when the current mention belongs to a longer conversation, Tools now pages further back through the same X thread before building the reply context, so earlier hostile/ugly comments in that same thread are more likely to be available when the bot is asked to judge tone, misogyny, or recurring behavior patterns
- replies that were posted later than the current mention are now filtered out from that earlier-thread capture, so the bot does not accidentally treat future comments as if they had already existed when the current post was written
- Max reply length now also accepts
0, which means “no local Tools character cap” (although X itself may still reject overlong live posts)
- Prefix replies with @author is now an explicit operator toggle, and Tools now persists that setting correctly again when the admin/API config is saved. Reply threading itself still comes from X's
in_reply_to_tweet_id payload, so the visible @username prefix is optional rather than required for a threaded reply
- even when that visible prefix toggle stays off, live posting now also tells X to exclude the author's reply mention from the threaded post itself when possible, so replies can stay threaded without auto-showing a leading
@username
- if a generated reply is still too long for the effective cap, Tools now first tries a short AI rewrite loop to compress the wording before it falls back to a last-resort hard trim with
…
- if a generated draft still opens with detached paraphrase such as
The message expresses... or The post suggests..., Tools now runs one extra direct-answer repair pass so normal ask/explain replies are pushed back toward a concrete answer instead of retelling what the post said
- generated reply text is now normalized from Markdown into plain text before it is stored or posted, so Markdown links such as
[label](https://example.com) are flattened into plain text with the raw URL kept visible for X
- structured reply generation now keeps pushing through the full five-attempt ladder when no publishable answer is found yet: four GPT-5 passes with stepwise stronger reasoning (
low → medium → high → xhigh), then one final GPT-4o fallback pass
- if an interaction came in only through watch-keyword polling/webhook keyword ingest and the bot was never explicitly mentioned or replied to by the author, Tools now keeps that interaction manual-only for posting because X can reject those replies with a not-engaged thread error
- clearly unengaged follow-up threads are now blocked before AI reply generation even starts, so Tools does not spend paid AI attempts on rows that X already would refuse for lack of direct engagement
- a new top-priority reply rule now overrides every mode: the bot must go straight to the concrete answer or verdict, skip preambles and scene-setting, and never open with
Short answer:-style lead-ins
- the same override now also forbids bullet-point / numbered mini-summaries in ordinary replies, so the bot stays in plain direct prose for tight X/Twitter space
- the same top-priority override now also forbids third-person meta-summary openings such as
the commenter believes, the post says, the tweet criticizes, or you are saying that; outside explicit summary mode the bot should answer the claim/question directly instead of first retelling what the user wrote
- if someone only tags the bot without adding substantive text of their own, the bot now treats that as an implicit request to inspect the earlier visible thread and comment on what the discussion contains
- explicit short-answer requests such as
Kortfattat!, briefly, short answer, or one sentence now trigger a stronger concise-reply path with stricter brevity instructions, lower output-token budget, and sentence-aware post-trimming
- when the thread already names or clearly discusses a public figure, the image/multimodal prompt now tells AI to discuss that public figure contextually instead of defaulting to generic
I cannot identify individuals refusals
The config panel now also behaves more like a real operator console:
- core config fields can now save inline over AJAX on blur/change
- recent interaction actions such as Reprocess, Approve/post, and Skip now also run over AJAX instead of full-page reloads
- recent-interaction pagination now also runs over AJAX, so moving between stored interaction pages no longer reloads the whole X-bot admin page
- clearly non-publishable rows are now hidden by default in Recent interactions, with an explicit Show non-publishable toggle when an operator really wants the blocked diagnostics rows too
- keyword rule actions such as Add rule, Save rule, and Delete rule now also run over AJAX instead of forcing a full refresh for every rule edit
- the recent interaction list now also shows each stored candidate reply's character count against the effective reply cap and keeps the latest stored posting error visible in the row
- the same interaction payloads now also expose a machine-readable publish/no-publish decision (
should_publish, publish_block_reason_code, publish_block_message, plus nested publication_decision) so scripts do not have to guess from free-text refusal replies
- obvious refusal-style candidate replies such as
I'm unable to assist with that. are now flagged as explicit NOPE / do not publish outcomes in both JSON payloads and the admin interaction table
- recent interactions now also render the structured reply-decision payload in a human-readable way (
message, can_reply, status, selected model/reasoning, alternatives, and generation-attempt summary) instead of showing only the stored plain-text candidate
- recent interactions now use badge-heavy cards with an expandable decision/diagnostics section, so operators can scan status, publishability, model path, char usage, and rerun counts quickly and only open the deeper attempt/raw-payload details when needed
- the expanded diagnostics now also show basic thread-capture details such as how many historical posts were stored for the interaction, how many remote conversation-search pages were fetched, and whether later thread replies were ignored as out-of-scope for that earlier context
- raw serialized interaction payloads are now lazy-loaded only when the operator explicitly opens the raw-payload toggle, so normal page search is much less likely to get drowned in giant JSON blobs
- older interaction rows that predate reply-length metadata now fall back to the current local Tools reply cap (
260 by default) instead of displaying a misleading 280
- list-style textareas such as known AI accounts, directed-command delegate/admin/block lists, and watch keywords now include explicit Clear list actions and can be emptied cleanly
- transport failures from X itself or from server-side linked-page/media fetches are now surfaced as structured diagnostics/runtime errors instead of crashing the whole X bot page with a raw 500
- webhook CRC/delivery crashes are now also stored as explicit
webhook_*_failed runtime events and can be forwarded to Slack through the audit_x_bot category, so operators can finally see why webhook setup or delivery handling blew up
- if X returns the exact posting error
You are not permitted to perform this action., Tools now explains that this is normally an X-side write/reply permission problem for the app/token pair rather than a character-length problem
- if X returns the not-engaged thread error (
Reply to this conversation is not allowed because you have not been mentioned or otherwise engaged...), Tools now explains that the bot account was never explicitly pulled into that conversation and that posting should remain manual unless the bot is first mentioned/replied to there
- if a bot run is blocked by a provider-side billing/spend cap, the diagnostics area now keeps a persistent warning banner visible until the next successful X-bot run clears it automatically, and that warning now also explains that the block can come from a separate project/account spend cap even when prepaid credits still remain
Interaction payload: machine-readable publish/no-publish decision
Recent/admin/API interaction payloads now also carry explicit publishability metadata so external scripts can stop on a clean boolean instead of trying to parse the candidate reply text itself.
Additive interaction fields:
reply_decision.message
reply_decision.can_reply
reply_decision.status
- additive
reply_decision.independently_verified
- additive
reply_decision.verification_policy (required, used, independently_verified, state, additive reason_code, additive message, additive fallback_used)
reply_decision.verification_policy.state can now be not_required, verified, independent_verification_missing, or normal_mode_fallback. The last one means the original prompt looked verification-heavy, but ToolGPT deliberately retried without web search and stored an ordinary context-based reply instead of posting a stock “verification missing” line.
publication_decision.display_state
publication_decision.display_label
The additive publication_decision.display_* fields exist so operator UIs can distinguish Already posted from the stricter Do not publish state. A reply that was already posted is still non-publishable for any later retry, but it should be presented as an already completed publish action rather than as a refusal/blocked candidate.
reply_decision.model
reply_decision.reasoning_effort
reply_generation.selected_candidate
reply_generation.alternatives[]
reply_generation.attempts[]
reply_generation.non_replyable_attempts
reply_generation.max_attempts
reply_decision_human[]
should_publish
publish_block_reason_code
publish_block_message
publication_decision.should_publish
publication_decision.reason_code
publication_decision.reason_message
publication_decision.source
Current behavior:
- the X bot now asks OpenAI to return one structured JSON reply decision instead of raw prose only
- the required JSON fields are
message, can_reply, and status; additive fields may also appear when the model includes useful operator/debug metadata
- if the structured reply still reads like a refusal (
I cannot assist with that, Jag kan inte hjälpa till med det, and similar), Tools forces can_reply=false even if the model forgot to set it
- when one structured attempt comes back non-replyable, the next attempt is reformulated with the previous refusal/status context so the bot gets another chance to answer something useful instead of stopping immediately
- GPT-5 replies now escalate reasoning effort step-by-step (
low → medium → high → xhigh) until a usable structured reply appears, and Tools then also asks gpt-4o for one additive alternative candidate for operator visibility
- if the GPT-5 path never produces a replyable structured answer, Tools falls back fully to
gpt-4o
- normal queued candidate replies still return
should_publish=true
- if the final structured decision says
can_reply=false, should_publish=false is returned even when a plain-text candidate message still exists for inspection
- obvious refusal/decline candidate replies now return
should_publish=false with reason code candidate_refusal
- blocked / opted-out / skipped / failed / rate-limited / already-posted rows also return
should_publish=false
- this is additive metadata only; it does not remove the existing interaction status, error, or candidate reply fields
Keyword-based reply-priority profiles
The X bot now supports simple keyword-driven rule rows inspired by the Mail Support Assistant flow.
In practice, these are best understood as reply-priority profiles:
- one part says what the post seems to be about
- one part says what answer direction should be prioritized
- one part says what tone should be used if that profile wins
Important migration note:
- the rule UI depends on the database table
x_bot_rules
- if an older environment has the admin page but not that table yet, run
php artisan migrate first
- until that migration exists, the X bot now keeps running with only the fallback instruction + fallback mood instead of crashing the whole page
Each rule has:
- IF / keywords
- INSTRUCTIONS
- MOOD
Current matching behavior:
- the bot checks the incoming mention text and the visible pulled-in thread context for that interaction
- the same rule matching also applies to posts that entered through the watch-keyword search path
- keywords/phrases can be written one per line or separated with commas/semicolons
- punctuation is normalized away before matching, so
Donald Trump? and Donald Trump behave the same
- the best-scoring active rule that matches wins
- the winning rule can override the fallback instruction and/or fallback mood for that one reply
- the intended use is to prioritize the most helpful answer direction first, for example "answer as bot-explanation first" or "answer as verification/fact-check first"
Thread backfill and attached media
The bot now builds context from more than the newest X post alone.
Current behavior:
- Tools still fetches the current tweet directly
- it now also walks backwards through the reply chain so parent tweets remain available when the latest message is just a short follow-up like “answer it”
- recent conversation search is then used as a supplement instead of being the only thread source
- when the visible thread is longer than the configured prompt window, Tools now tries to preserve both the opening thread anchor and the latest turns instead of only keeping the newest posts
- the final AI prompt now labels the thread as oldest first so the reply model can follow the sequence more naturally
- if earlier thread rows are present, the bot is now instructed to treat that prior discussion as implicitly relevant by default even when the newest post does not explicitly say “use previous context”
- the bot is now also explicitly told not to answer as if earlier discussion is unavailable when Tools already supplied those earlier thread rows in the prompt
Attached media is also handled more honestly now:
- X tweet lookups now request media metadata and alt text when X exposes it
- the AI prompt still includes a compact media summary for tweets that contain image/video metadata
- when Forward image URLs to AI is enabled, the bot now also forwards X-exposed image URLs (and preview-image URLs when that is the only visual URL available) as multimodal OpenAI inputs
- the same image-forwarding path now also tries to fetch the image server-side and embed it as inline image data when possible, with remote URL fallback when inline fetch is unavailable or too large
- when Context format is set to Structured JSON, the bot now sends one nested JSON object containing the incoming post, mode metadata, visible context posts, reply constraints, and forwarded image URLs instead of only a flat prose block
- when the same tweet exists both in the live X lookup and in Tools' local conversation archive, the live X row now wins for overlapping post ids so attached media is preserved instead of being replaced by an older local duplicate with no media payload
- internal X/Twitter attachment links such as
/status/.../photo/1 or /video/1 are now ignored as ordinary linked-page context whenever real attached tweet media already exists, so the AI is less likely to answer as if it only saw the attachment-page URL instead of the image itself
- if X only exposes that an image exists but does not provide a useful description or a usable image URL, the bot is now instructed to say that plainly instead of pretending it could inspect raw pixels
This means the bot can now react better to “look at image 2” style follow-ups when X exposes either useful media metadata or real image URLs, while still being truthful when the platform does not provide enough detail.
Context format and multimodal image forwarding
The X bot now has two operator-facing prompt controls:
- Context format
- Forward image URLs to AI
- Historical context max posts
Context format
Available values:
Structured JSON
Flat text
Current behavior:
Structured JSON is now the default
- Tools builds one nested JSON payload containing the incoming post, mode metadata, known AI accounts, sanitized context posts, forwarded image URLs, and reply constraints
Flat text keeps the older line-oriented prompt layout for operators who want the legacy style
Forward image URLs to AI
When this is enabled:
- Tools preserves media URLs from X context rows when X exposes them
- those URLs are forwarded to OpenAI as multimodal
image_url content blocks alongside the text prompt
- when the server can fetch the image safely, Tools now prefers embedding the image as inline data in the OpenAI request instead of relying only on OpenAI-side remote URL retrieval
- photo/image URLs are preferred directly
- video/animated-gif rows can fall back to preview-image URLs when that is the only visual URL X exposes
Link handling enabled
When this is enabled:
- Tools now preserves normalized external URL metadata from the visible X thread
- it tries to fetch a few linked pages server-side over HTTP(S)
- successful fetches are reduced to compact title + excerpt summaries and included in the AI context
- this makes it more likely that short replies such as “read the article” or “you missed the link” can be answered against actual fetched page text instead of only the tweet body
When this is disabled:
- Tools still keeps the plain X text context
- but linked URLs are not fetched server-side for extra page context
When this is disabled:
- Tools still keeps media summaries / alt text in the sanitized context
- but it no longer forwards the raw image URLs to OpenAI for direct visual inspection
Historical context max posts
This controls the local per-conversation archive, not the prompt window.
Current behavior:
- Tools now stores conversation history locally inside X bot interaction metadata so older thread content can still be reused even if a post later disappears from X
- the smaller Max context posts setting still controls how much of that history is sent to OpenAI in one prompt
Historical context max posts = 0 means effectively unlimited local archive retention for stored conversation posts
- when the archive grows beyond the configured non-zero cap, Tools keeps the newest stored history rows
- the bot can also keep locally generated reply text in that archive, so later follow-ups can still refer back to what the bot already said even if the remote thread becomes incomplete
Public bot identity / version label
The X bot can now identify itself with a public version label when explicitly asked.
Current behavior:
-
the name part now comes from the synced X account identity (display_name, otherwise username)
-
the version suffix remains operator-configurable from the bot settings/runtime configuration
-
when a user explicitly asks who/version the bot is, it may answer with something like ToolsGPT-v0.1.0, where ToolsGPT is taken from the X account itself
-
the version label is not meant to appear in ordinary replies; it is only for explicit “who are you / what version are you?” style questions
Watch keywords / trigger-word polling
The bot can now ingest two different public X sources:
- direct
@mentions / replies to the bot account
- public posts that match configured watch keywords such as
Tornevall
Relevant settings:
- Only explicit @mentions
- Watch keywords / trigger words
Behavior:
- when Only explicit @mentions is Yes, the watch-keyword list is ignored and the older mention-only behavior remains active
- when it is No, Tools also runs a recent-search query against the configured watch words during poll cycles
- matching keyword posts are stored in the same
x_bot_interactions flow as direct mentions
- the same reply-priority evaluation is then applied to those posts so Tools can decide which answer direction is most helpful first
This is useful for routing mentions such as:
- billing/payment words
- support/problem/error words
- complaint/abuse words
- playful/banter words
Auto-approve delay window
The bot can now auto-approve replies after ingest/processing even when you normally keep manual approval visible.
Settings:
- Auto-approve after ingest
- Auto-approve min delay (seconds)
- Auto-approve max delay (seconds)
Behavior:
- Tools chooses one random delay inside that window
- the due reply is posted from the normal poll cycle instead of firing at the exact same second for every cron run
- if Manual approval required is set to No, replies are already eligible for posting on the current/next poll cycle even when the explicit randomized auto-approve toggle is off
- if manual approval remains on and auto-approve is off, the generated reply stays in review mode as before
Local Tools rate limits vs X rate limits
The reply caps in the X bot settings are local Tools safety limits:
- per-user replies per hour
- per-conversation replies per hour
- global replies per day
These are separate from any remote X platform rate limits.
Current behavior:
- if the configured AI owner is an admin user, Tools now bypasses these local limits entirely
- remote X API rate limits can still apply and are still enforced by X itself
Manual-review notifications
While manual approval is required, the bot can now notify operators for real runtime events.
Current notification controls:
- Notify on manual-review events
- Mail notifications
- Slack notifications
Mail uses the configured support/alert recipient when that notification path is enabled.
Slack uses the dedicated Slack log-routing category audit_x_bot.
Webhook ingest (primary realtime path)
The X bot now supports a first public webhook endpoint that can be registered in the X developer portal.
Webhook URL:
https://tools.tornevall.net/api/x-bot/webhook
- local/dev equivalent depends on the current host, but the admin panel now shows the exact computed URL too
Routes:
GET /api/x-bot/webhook
POST /api/x-bot/webhook
Current behavior:
- webhook ingest can be enabled/disabled from the X bot admin settings
- signed webhook POST deliveries are the preferred realtime ingest path
- polling remains in place as fallback, manual sync, and recovery
- if the same mention is seen through both webhook and polling, the stored interaction dedupe still uses the same
x_post_id, so webhook metadata wins without creating a second interaction
Required conditions for webhook support:
- valid X app credentials with webhook/CRC support on the operator side
- bot config
webhook_enabled=true in the Tools admin page
The current implementation does not auto-register the webhook subscription in the X portal for you.
Right now the registration/subscription step is still performed in the X developer dashboard, where you point X to the URL above and let it verify the CRC response.
Settings now exposed in the bot config/status payload include:
webhook_enabled
webhook_url
webhook_last_received_at
webhook_last_processed_at
webhook_last_error_at
webhook_last_error
CRC verification
GET /api/x-bot/webhook?crc_token=... returns the X-style CRC response token.
Tools uses the configured X app secret to build that response.
Signed POST verification
Webhook POST deliveries are verified before the payload is trusted.
Current behavior:
- Tools reads the raw request body
- verifies the X webhook signature using the configured consumer secret
- stores a
XBotEvent
- dispatches queue processing for the payload
- returns quickly with HTTP 200 when accepted
The webhook controller does not generate AI replies or post to X directly.
It only ingests and queues work.
Queue worker note
Webhook ingestion is fastest when a real queue worker is running.
Examples:
php artisan queue:work
If the environment still uses QUEUE_CONNECTION=sync, webhook processing still works, but the queue hop happens inline during the request instead of through a separate worker.
Unknown event groups
Unknown or not-yet-handled event groups are still stored as XBotEvent rows for diagnostics, but they are not treated as replyable interactions.
Direct-message events may be logged, but are not auto-replied to by default.
X rate limits and automatic retry windows
The X bot now stores the latest X rate-limit headers it sees for read and write calls.
Tracked fields now surfaced in config/status are:
remote_read_rate_limit
remote_write_rate_limit
Each stored block can include values such as:
limit
remaining
reset_at
next_allowed_at
last_checked_at
last_status
source
Behavior:
- if polling previously hit a remote X read-rate-limit window, later poll runs now pause automatically until that reset time has passed
- if reply posting hits a remote X write-rate-limit window, the interaction is kept retryable instead of being treated like a permanent failure
- the admin panel now shows the last known read/write window so operators can see when it is safe to try again
Console and cron execution
You can poll the bot manually from the project root with Artisan:
php artisan x-bot:poll-mentions
If you want to ingest/store new mentions without queueing processing jobs yet, use:
php artisan x-bot:poll-mentions --no-dispatch
If you want to bypass the stored local poll interval once from shell, use:
php artisan x-bot:poll-mentions --now
Convenience runner scripts now also exist in the repository root:
./run-x-bot
./run-x-bot --now
./run-x-bot --no-dispatch
./run-x-bot.sh
./run-x-bot.sh --now
./run-x-bot.sh --no-dispatch
On Windows consoles outside WSL, use:
run-x-bot.bat
run-x-bot.bat --now
run-x-bot.bat --no-dispatch
The normal production path is still Laravel's scheduler, where x-bot:poll-mentions is already scheduled every minute through php artisan schedule:run.
The same poll cycle now also checks due auto-approve windows, so delayed replies can still be posted even on the current sync-queue setup.
Mention polling now also walks multiple X result pages per cycle instead of only the first page, which reduces the risk that bursty mention traffic gets partially ingested and then skipped.
AI defaults and fallback
The current default operator direction is now:
- primary reply model:
gpt-5.4
- primary fact-check model:
gpt-5.4
- default reasoning effort:
high
- main structured-reply fallback/alternative model:
gpt-4o
If the primary GPT-5 path fails, keeps returning non-replyable JSON, rejects the reasoning flag, or produces empty output, the X bot now retries safely with stepwise stronger reasoning and then falls back to gpt-4o instead of leaving the interaction without a candidate reply. Lower-level shortening/repair helpers may still use smaller fallback models when they are only compressing an already generated answer.
ToolsAPI
The first ToolsAPI surface includes endpoints for:
- bot status/config
- diagnostics
- recent interactions
- one interaction
- reprocess
- approve/post
- skip
- opt-outs
Status/config responses now also include additive X bot rules plus the new fallback/notification/auto-approve settings, so authorized operator clients can stay in sync with the web control room.
Settings payloads now also include:
context_format (text|json)
image_url_forwarding_enabled (true|false)
Interaction context_meta can now also include:
context_format
image_url_forwarding_enabled
forwarded_image_urls[]
Important note about posting
Posting replies to X requires user-context write credentials.
If only bearer/app-level credentials are configured, mention reading/lookup may still work depending on the plan, but reply posting diagnostics will report that write credentials are missing.
If the credentials are present but X responds with an oauth1 app-permissions error, Tools is reaching X correctly — the remaining fix is usually in the X developer app configuration, not in Tools itself.
Public references for general questions
If someone asks broad or technical questions at a public level, the safest references are:
Public answers should stay focused on:
- visible behavior
- public documentation
- quality improvements that are already reflected in the public bot behavior
Public answers should not include hidden prompts, credentials, internal infrastructure, or exact private runtime configuration.