X connector setup

Operator·11 minutes to read

This page is the setup half — creating the OAuth app X needs in its own developer portal and getting its client id and secret into this deployment. Authorising a connection afterwards is the Authenticate control on the plugin sheet, which is what actually sends you to X.

1. Create the OAuth app in the X developer portal

  1. Open the X developer portal and create a new app with OAuth 2.0 enabled — this is your own app, not Botseon's.

  2. Under User authentication settings → Callback URI / Redirect URL, add the redirect URL from docs/connectors/README.md — with the quickstart's default BOTSEON_PUBLIC_URL, that is:

    http://127.0.0.1:3000/api/oauth/x/callback
    
  3. Save. The portal shows you a Client ID and a Client Secret — copy both now.

2. Scopes

Request the narrowest set that covers the tools this connector exposes (F-PLUG-1: minimum scopes per capability, read and write requested separately):

  • tweet.read — read posts and their metadata; no writes.
  • users.read — read profile information for accounts referenced in a post.
  • bookmark.read — read the member's own bookmarks.
  • like.read — read the posts the member has liked.
  • dm.read — read the member's direct messages.
  • offline.access — issue a refresh token, so the connection survives past the access token's own short lifetime.

No write scope is asked for: the connector cannot post, like, follow, bookmark or send a direct message. Tick the same read permissions on the app in the developer portal, and nothing more.

The member's own reads (bookmarks, likes, home timeline, direct messages) are on X's paid, pay-per-usage tier. Request shapes and their sources: docs/research/connectors/x-signed-in-reads-2026-10-16.md.

3. What members see before and after you enter the app

The X tile is in every member's Connect apps on every deployment, as one of the built-in plugins. Your organisation's plugin policy can still block it for its members. Until you enter an X app (section 4), a member who adds X has nothing to sign in with, the composer's @ menu does not offer it, and nothing is spent at X: X bills every read (pay-per-usage credits bought in the Developer Console), and those reads are billed to the X app you enter. Once botseon oauth-app set --slug x has stored it for an organisation (or for the whole deployment with --deployment), members of that organisation can sign in. No environment setting is needed.

BOTSEON_X_CONNECTOR=off in the web app's environment is the kill switch: the tile is closed, X tagging (section 6) stays off, and nothing is spent, whatever app or tagging account is stored. Public X search (search_x) does not depend on any of this: it uses BOTSEON_X_NATIVE_BEARER and needs no member sign-in. Sources, read 2026-10-22: docs/research/connectors/x-catalogue-2026-10-22.md.

Before you enter the app, check the member reads against X itself, once:

  1. On the paid tier, sign in to the X app as a test account and mint an OAuth 2.0 refresh token with tweet.read users.read bookmark.read like.read dm.read offline.access.
  2. Store it as the secret X_TEST_REFRESH_TOKEN in the GitHub environment connector-live, beside the variable X_TEST_CLIENT_ID and the secret X_TEST_CLIENT_SECRET (docs/operations/connector-live-setup/x.md).
  3. Run the connector-live workflow from main by hand, or wait for the nightly run: it makes its own access token from the refresh token. X rotates the refresh token on use, so one stored value proves one run. The X case "reads the test member's bookmarks, liked posts, home timeline and direct messages, each wrapped" must pass, not skip; it reads five of each and changes nothing at X.

Revoke URL: https://api.x.com/2/oauth2/revoke, so that removing the account in Botseon (Remove account) ends the grant at X as well as removing the token here.

4. Enter the app

One command, with the client secret on standard input — there is no flag that takes it and no environment variable that holds it (see docs/connectors/README.md):

printf %s "$CLIENT_SECRET" | botseon oauth-app set \
  --org <organisation id> --slug x \
  --client-id <the client ID from step 1> \
  --auth-url https://x.com/i/oauth2/authorize \
  --token-url https://api.x.com/2/oauth2/token \
  --revoke-url https://api.x.com/2/oauth2/revoke \
  --configured-by <your user id>

Both URLs use the x.com domain, matching this connector's own resource API base (https://api.x.com/2 in extensions/connectors/x/driver.ts) rather than the older twitter.com domain, so the app-setup step and the connector's own calls agree on one domain. X's cited documentation (OAuth 2.0 Authorization Code Flow with PKCE) was not reachable to independently confirm the exact pair — if your organisation's app registration needs the twitter.com authorize host instead, use the value X's current portal shows you.

It stores the secret encrypted and prints the redirect URL from step 1 back, so you can check the two match. botseon oauth-app show --org <organisation id> --slug x says whether an app is configured and under which client id; nothing prints the secret back.

Never put either value in a bot's description, a memory, or anywhere else a model reads as ordinary text.

5. Let a bot answer an X account's direct messages

Separate from the connector above: a member can connect one of their bots to an X account so the bot answers the direct messages sent to that account (S-98861). It is set up from the bot's details pane (X direct messages), not through the OAuth app above, because each member brings the app the account is registered under:

  1. In the X developer portal, the app needs Read, write and Direct Messages permission and access to the X Activity API (webhooks and subscriptions).
  2. Under Keys and tokens, the member copies the API key and API key secret, and an access token and secret generated for the account the bot answers as.
  3. The deployment needs a public HTTPS address with no port (BOTSEON_PUBLIC_URL): X will only deliver to such a URL. Without one, the section shows why it cannot be used.

On Connect, Botseon checks the account (GET /2/users/me), registers <BOTSEON_PUBLIC_URL>/api/channels/x/events/<account id> as a webhook, and subscribes the account to dm.received. Disconnect deletes the subscription at X and overwrites the stored keys.

The bot only answers people who wrote to the account in the last 24 hours, never starts a conversation, and writes at most 5 messages to one person and to at most 50 different people in any 24 hours. A reply over a limit is not sent; it is recorded as a held outbound event with its reason. Request shapes and their sources: docs/research/connectors/x-dm-reply-2026-10-21.md.

6. Let members tag this deployment's own X account

Separate again from both of the above: the deployment runs one public X account of its own, and a member who has linked their X account (by connecting the X connector, section 4) can tag it in a post or a reply to hand that post to their main bot. The account replies once on X to confirm, or asks a stranger to sign up, or asks a member with no main bot to set one (S-91445). Members in the EU, the EEA, the UK and Australia are never offered it. Request shapes and their sources: docs/research/connectors/x-tagging-mentions-2026-10-21.md.

It needs X's paid tier (each delivered mention is billed as a post read, each reply as a post write), so it stays off until you set it up. Setting it up is the one switch: once the account below is configured, tagging is on, and members link their X account through the X connector. Set BOTSEON_X_CONNECTOR=off to keep tagging off while the account stays configured: the Settings row disappears, and a tag (even from a member who linked earlier) starts no run and gets no reply. off also closes the X tile in Connect apps (section 3).

  1. Create (or reuse) an X account for the deployment, and an X app in the developer portal that account owns, with Read and write permission and access to the X Activity API.

  2. Under Keys and tokens, generate the API key and secret and an access token and secret for the account. Set them for the web app:

    variablevalue
    BOTSEON_X_ACCOUNT_CONSUMER_KEYthe API key
    BOTSEON_X_WEBHOOK_CONSUMER_SECRETthe API key secret (it also verifies every delivery)
    BOTSEON_X_ACCOUNT_ACCESS_TOKENthe account's access token
    BOTSEON_X_ACCOUNT_ACCESS_SECRETthe account's access token secret
    BOTSEON_X_CONNECTORleave unset — the configured account turns tagging on; off keeps tagging off
    BOTSEON_X_OTHER_ASSISTANT_USER_IDSoptional: other assistants' X user ids, comma-separated; a post that also tags one starts nothing

    These are OAuth 1.0a keys, which do not expire. BOTSEON_X_ACCOUNT_BEARER (an OAuth 2.0 user token) still works for the reply but expires after two hours; use the four above instead.

  3. Choose how X delivers the mentions. With a public https:// BOTSEON_PUBLIC_URL (no port), X calls this deployment's webhook. Without one — a home deployment on http://localhost, the default — the web app reads them over X's activity stream instead, connecting out to X, so nothing has to reach in: set BOTSEON_X_ACCOUNT_APP_BEARER to the app's Bearer Token from the same Keys and tokens page (BOTSEON_X_NATIVE_BEARER serves if it is the same app's). BOTSEON_X_TAGGING_DELIVERY=webhook or stream overrides the choice. Request shapes: docs/research/connectors/x-tagging-stream-2026-10-21.md. Restart the web app with the variables above. That is the whole set-up: at start-up the web app reads which account the keys act as (so BOTSEON_X_ACCOUNT_USER_ID may stay unset), reads whether X already holds the webhook and the mention subscription, and registers whichever is missing — retrying the webhook until the web app answers X's check. Its log says x tagging: @<handle> registered at X (or already set up at X), or names the step it stopped at. BOTSEON_X_TAGGING_AUTO_SETUP=0 turns this off; then register by hand instead, with the same variables in the shell (request shapes: docs/research/connectors/x-tagging-autosetup-2026-10-23.md):

    botseon x-tagging setup --check   # reads which account the keys act as; changes nothing at X
    botseon x-tagging setup           # registers the webhook and subscribes to the account's mentions
    

    It registers <BOTSEON_PUBLIC_URL>/api/channels/x/events as the X Activity API webhook (X checks it on the spot, so the web app must already be up) and subscribes the account to post.mention.create. On stream delivery there is no webhook: it subscribes the account with none, and the web app's log says x tagging stream: connected once it holds the stream. It prints the account's user id; set it as BOTSEON_X_ACCOUNT_USER_ID for the web app only when the start-up set-up is off.

  4. Check X still holds both, at any time and without changing anything there:

    botseon x-tagging setup --status   # the account, a valid webhook, the mention subscription on it
    

    It exits 1 and names the step when a tag would not arrive: the webhook is not registered, X marks it invalid (the web app stopped answering X's check), or the subscription is gone or on another webhook. On stream delivery it reads the account's webhook-less mentions subscription instead; a refused stream shows in the web app's log as X refused the stream (http_401) (the bearer) or (http_429) (X's rate limit; it retries on its own). X has no activity subscription for this app yet (http_409) is no refusal: X accepted the bearer and has nothing subscribed to stream (its documented 409, "Stream has no rules"). Run botseon x-tagging setup to create the subscription; the stream asks again every minute. Request shapes: docs/research/connectors/x-tagging-status-2026-10-21.md.

  5. Check it end to end. From a second X account (not the deployment's), authorise the same X app and generate that account's access token and secret on the app's Keys and tokens page, then:

    BOTSEON_X_TAGGER_ACCESS_TOKEN=… BOTSEON_X_TAGGER_ACCESS_SECRET=… botseon x-tagging setup --tag-check
    

    It posts one tag of the account from the second account, waits up to two minutes for the account's reply (the sign-up reply, unless that X account is linked to a member), checks there is exactly one, and deletes both posts. It exits 1 and names the step when the reply never comes or comes twice. Costs one post write by the second account and a few reads, on top of the mention's own read and the reply. Request shapes: docs/research/connectors/x-tagging-roundtrip-2026-10-21.md. To see the task side as well, link that X account to a test member with a main bot and run it again: the member's bot gets a task with the post, and the reply is the confirmation. If setup succeeded but no reply ever arrives, read the "Open risk" in the mentions research note above.

Turning it off. Unset BOTSEON_X_ACCOUNT_USER_ID, or set BOTSEON_X_CONNECTOR=off: deliveries are then accepted and ignored (no run, no reply), the Settings row disappears, and on stream delivery the web app no longer opens the stream at its next start. Delete the subscription in the X developer portal to stop paying for deliveries.

Last verified against build 445930e.