# Connect Discord and X (/docs/build/integrations)



Your community already talks on Discord and X. Connect them, and quests can check what members do there. For example:

* **"Join our Discord server"**, optionally with a required role.
* **"React 🫡 to our announcements"**, paid automatically.
* **"Post in #show-and-tell"**, once a day.
* **"Get a ❤ from a moderator"**, which pays the member who wrote the message.
* **"Finish onboarding and get the Contributor role"** in Discord.
* **"@ana got +10 XP from @sam"**, posted in a channel each time a quest pays.
* **"Follow us on X"*&#x2A;, &#x2A;*"Repost our launch"*&#x2A;, or &#x2A;*"Put our tag in your bio"**.

Connect Discord once per project. Quests then use that server, so nobody copies server IDs or bot tokens into code. X needs no setup in your project at all.

Telegram groups and channels connect from the same **Integrations** page; see [Telegram](/docs/build/telegram).

Each service is a card on **Integrations** with its status and what it needs from you next, such as **Connect** or **Manage**. Open a card to see its details and act on it in a panel beside the page.

Other services Domino integrates appear on the same page, one card each, such as [Klaviyo](/docs/build/klaviyo), which keeps your members' Klaviyo profiles current for your email flows. Connect one with the settings and secrets it asks for, separately in Test and Live; secrets show only as set or not set, never their values. Anyone who can publish connects, edits or disconnects it; anyone else sees its status.

Some services connect through their own sign-in instead of secrets. Their panel has **Connect with** and the service's name: fill in the settings, grant Domino access on the service's page, and it sends you back to **Integrations**, which finishes connecting the service with the panel open. Only the person who started connecting can finish it, signed in to Console: a link to the service's page that you send someone else connects nothing. Domino keeps that access fresh on its own. If the service stops accepting it, for example because someone removed Domino in the service's settings, the card asks you to **Reconnect**, and nothing reaches the service until you do. **Disconnect** removes Domino's access at the service too. A card that says **Not configured** can't be connected yet: Domino hasn't set that service up.

> [!DEV]
> `domino connections authorize <plugin> --config-json '{…}'` and `management.connections.authorize(plugin, { config })` answer the `authorizationUrl` to open; it works once, for ten minutes. The service then sends the browser to Console with a one-time `oauthCode`, which `management.connections.completeAuthorization(plugin, { code })` redeems for the same person within five minutes (403 `scope-mismatch` for anyone else). An automation, service or hosted app key can't start one (403 `credential-not-allowed`). A connection that needs reconnecting has a `reconnectReason`. `plugins.list()` shows which services connect this way (`oauth`) and whether they're set up (`oauth.configured`); connecting one that isn't is refused with 409 `plugin-not-configured`.

## What members see [#what-members-see]

* **One link, then quests.** A member who signed in with Discord is ready. Anyone else links their Discord or X account once from your app, and every quest can use it.
* **Reactions and messages pay on their own.** Reaction and message quests have no claim button. The member reacts or posts in Discord, and the points arrive.
* **Roles show up in Discord.** A role reward appears on the member's Discord profile, and your app can show whether it is still being granted.
* **An outage is never held against them.** If Discord or X is down, the check waits and tries again instead of saying no.

## What you can do in Console [#what-you-can-do-in-console]

| Without code                                                                | Where                                     |
| --------------------------------------------------------------------------- | ----------------------------------------- |
| Connect, check, switch or disconnect your Discord server                    | **Integrations**                          |
| Connect a server another project of your organization already connected     | **Integrations**, **Or reuse**            |
| See which quests need Discord, whether events are arriving, and recent gaps | **Integrations**, on the Discord card     |
| Choose the announcement channel and the cutoff date                         | The quest's **Settings**                  |
| Choose the role a quest hands out                                           | The quest's **Settings**, **Reward role** |
| See role grants, retry a failed one, or keep a role your team owns          | A member's **Discord roles**              |
| Choose the channel a quest announces in                                     | The quest's **Settings**                  |
| See a member's announcements, retry one that failed                         | A member's **Announcements**              |
| Relink an X or Discord account a member already proved                      | A member's **Identities**                 |

The quests themselves come from code. Your developer starts from a ready-made recipe, called a snippet:

| Snippet                         | Rewards                                                          |
| ------------------------------- | ---------------------------------------------------------------- |
| `discord-membership`            | Members of your server, optionally only those with a role.       |
| `discord-announcements`         | 🫡 reactions on announcements published after a date you choose. |
| `discord-contribution`          | Messages in a channel you choose, once per member per day.       |
| `discord-moderator-recognition` | The author of a message, when a moderator reacts with a heart.   |
| `discord-role-reward`           | A Discord role, handed out when the member completes the quest.  |

> [!DEV] How each snippet runs
> `discord-membership` calls `ctx.discord.member()` in the evaluator. `discord-announcements` and `discord-moderator-recognition` trigger from a `discord.reactions()` tracker; the moderator one pays `messageAuthor`. `discord-contribution` triggers from a `discord.messages()` tracker. `discord-role-reward` declares `discord.grantRole(roleId)` as a quest reward. An announcement is `discord.postMessage({ channel, text })` in the quest's `announcements`; see [Announce in a channel](#announce-in-a-channel). X checks use `ctx.x`; see [Verify X activity](#verify-x-activity).

## Connect Discord to a project [#connect-discord-to-a-project]

An organization administrator installs the bot, or [sends a link](#send-the-install-to-your-server-admin) to whoever manages the server, and manages the connection. Anyone else with read access can see its status. Once one project connects a server, anyone who can publish can [connect it to another project](#use-a-server-your-organization-already-connected) of the same organization without installing the bot again.

### 1. Start the connection [#1-start-the-connection]

Open **Integrations** in Console, select your project and the Test or Live environment, open the **Discord** card, and choose **Connect Discord**.

### 2. Install the bot [#2-install-the-bot]

Discord asks you to choose a server you manage and install Domino's bot. You don't enter a server ID or bot token.

> [!NOTE] When Domino's bot is already in the server
> Every Domino organization shares the same bot. If someone already gave it permissions in the server, such as **Manage Roles** for role rewards, only a person who has all of those permissions too can connect it, or the server's owner or an Administrator. Anyone else sees "Domino's bot already has permissions in this server that you don't have," and nothing is connected. Ask a server administrator to connect it, or send them an install link.

### 3. Return to Console [#3-return-to-console]

Come back to the same Console account and project to finish. Console shows the server's name when the connection is ready.

### Send the install to your server admin [#send-the-install-to-your-server-admin]

Adding a bot to a Discord server takes **Manage Server** (or Administrator) in that server. If you're a Console administrator without it, send a link to someone who has it. They don't need a Domino account.

1. In **Integrations**, open the **Discord** card and choose **Send to your server admin**.
2. Optionally enter the server's ID. The link then preselects that server and connects only it. Without an ID, it connects any server the person opening it manages.
3. Choose **Create link**, copy it and send it to them.
4. They open it and see which organization and project it connects to: your organization's name, the project's name, its slug and the environment. They choose **Continue to Discord**, add Domino's bot, and see a page naming the connected server and the project.

Refresh **Integrations** to see the server. The link connects the project and environment you made it in.

* **Once, for seven days.** A link connects one server, then stops working. Making a new link leaves earlier ones working.
* **Only for the connection you had.** If the project's Discord connection changes after you make the link (someone connects or disconnects a server), the link connects nothing. Make a new one.
* **Anyone with the link can use it.** Send it only to the person who manages the server. The project's audit records you, the link's creator, as who connected the server.
* **It names your project.** The project's slug is unique across Domino, so tell the person to expect it. Someone who gets a link naming a project they don't know shouldn't continue.

### Manage the connection [#manage-the-connection]

* **One server per environment.** Each project has one Discord server in Test and one in Live, set up separately.
* **Reconnect to switch servers.** Reconnecting lets you pick another server.
* **Check access.** **Check access** checks the bot again. If the bot was removed or lost access, reconnect from this page.
* **Outages are not rejections.** A Discord outage makes the check try again later. It never fails the member's membership.

### Use a server your organization already connected [#use-a-server-your-organization-already-connected]

A community with several projects installs the bot once. When another project of your organization already connects a server in the same environment, and the connection is ready, connect it here directly:

1. Open **Integrations** in Console, in the project and environment you want to connect, and open the **Discord** card.
2. Under **Or reuse**, choose **Use** beside the server.

Domino checks that its bot can still see the server, then connects it. Anyone who can publish in the project can do this, not just administrators. To add the bot to a server no project connects yet, an administrator uses **Connect Discord**. The project's audit records who connected the server and which project's connection it reused.

* **Only a project without a server.** A project that already has a Discord connection keeps it, even when it needs attention. An administrator reconnects, checks or disconnects it.
* **Ready connections only.** A server whose only connection needs attention isn't offered. Repair it in its project first.

> [!DEV] Connect from the CLI or an automation key
> An agent that creates a project with `domino create` can connect it without a person: `domino integrations discord servers` lists the servers it can use, `domino integrations discord connect --server <id>` connects one, and `domino integrations discord get` reads the connection. The management client has the same calls: `management.discord.servers()`, `management.discord.connect(server)` and `management.discord.connection()`. They need `publish` permission, and an automation key or CLI session connects only in Test.
>
> A refusal names what a person needs to do. `server-not-connected-in-organization` (409): no other project connects that server ready in this environment, so an administrator installs the bot in Console. `integration-connected` (409): the project already has a Discord connection. `server-not-reachable` (422): the bot lost access to the server, so an administrator reconnects it.

> [!NOTE] Console says Discord isn't available
> Contact the platform administrator. Domino's Discord bot must be set up before anyone can install it. See [Configure the managed Discord application](#configure-the-managed-discord-application).

## Check that someone joined your server [#check-that-someone-joined-your-server]

Install the `discord-membership` snippet. It accepts members of your server who finished Discord's membership screening. Your developer can narrow it to a role, match names, or require a server boost.

To be checked, a member needs their Discord account in your app:

* **Discord sign-in.** A member who signs in with Discord is ready.
* **Other logins.** A member who signs in another way links Discord once with your app's account controls. Custom apps use the [member API](/docs/reference/participant-api#connected-accounts).
* **Basic consent only.** Discord asks for basic identity consent. Members don't authorize reading their server membership.
* **Existing links.** A Discord account a member linked before works too, as long as it's unambiguous.

Console labels these quests **Quest code · Discord integration**.

> [!DEV]
> Declare `integrations: [discord]` on the quest and call `await ctx.discord.member()` in `evaluate`. Relay picks the project's server and the member's Discord identity; the quest has no server setting. The result is `null` when the member isn't in the server or hasn't linked Discord, or an object with `userId`, `roles`, `joinedAt`, `pending`, `nickname`, `displayName` and `boostingSince`. Quest code owns the role, name and boost decisions and the rewards.
>
> The photo-check provider setting does not select the Discord verifier. **Staff review** means quest code may request a review; uncertain Discord checks don't go to staff on their own. The membership snippet accepts or rejects membership, and leaves Discord outages as errors that retry.

### Recover a configuration failure [#recover-a-configuration-failure]

Connect Discord in **Integrations**, for the quest's project and environment, before members try a quest that needs it. A quest needs Discord when it checks membership, hands out a Discord role, announces in a channel, or lets you choose a Discord channel or role.

* **Publishing still works without it.** No way of publishing refuses a quest because Discord is missing or broken. The Discord card in **Integrations** always lists the quests that need Discord, and **Quests** shows a warning on each one until Discord is ready.
* **Roles and channels are checked while connected.** Once Discord is connected, a role or channel the server doesn't have still stops publication.
* **Automatic quests catch up.** An automatic quest published before Discord is ready, or whose connection breaks later, starts members' attempts at once, and they fail. When Discord becomes ready, by first connecting it or repairing it from needs attention, Domino starts a new attempt in the background for each member whose latest attempt failed this way. Current rules apply: a paused quest, or a member who no longer meets the prerequisites, gets no new attempt. Quests with a tracker trigger are the exception: their members keep the failed attempts, so retry those one by one.

When a check can't reach your server, the member's attempt usually stops with one of these reasons:

| Reason                                           | What to do                                                      |
| ------------------------------------------------ | --------------------------------------------------------------- |
| The project hasn't connected its Discord server  | Connect Discord in the same project's Test or Live environment. |
| The project's Discord connection needs attention | Use **Check access**, or reconnect the bot in **Integrations**. |
| Discord is not configured                        | Ask the platform administrator to set up the managed bot.       |

Read the reason on the attempt in Console. For an attempt Domino doesn't start again, fix the connection first, then retry: a member trying again can't fix your project's setup. A retry checks Discord again. If the member's Discord account or your server changed since the attempt started, start a new submission instead, because an attempt keeps the account and server it first saw.

> [!DEV]
> A deployment's preview and result list such quests in `warnings` with code `integration-required`, and `domino deploy` prints them. Publishing a single quest, a Console draft, or unarchiving a quest returns no warning.
>
> In an existing quest, a Discord read that needs the project connection throws a non-retryable `CapabilityError` with code `integration-required` when the connection is missing or broken. Membership still returns `null` when the member hasn't linked Discord. If quest code lets the error escape, the attempt becomes `failed` immediately, with that code and a member-safe reason. It doesn't use up the automatic retry budget or reject the member's membership. Read `code` and `reason` with `domino attempts get <attempt>`. Temporary network errors and Discord outages throw `provider-unavailable` and retry automatically. See [Evaluator readers: null, errors and pinning](/docs/reference/verification#evaluator-readers-null-errors-and-pinning).

## Automatic announcement reactions [#automatic-announcement-reactions]

Install the `discord-announcements` snippet to reward members who react 🫡 to announcements published after a date you choose. Choose the announcement channel and that date in Console, as described in [Choose channels and cutoff dates](#choose-channels-and-cutoff-dates). The server comes from the project's connection, and the bot must be able to see that channel.

```mermaid
flowchart LR
    A["Member reacts 🫡"] --> B["Discord Gateway"]
    B --> C["Discord tracker stores it"]
    C --> D{"Reactor linked and joined, quest active?"}
    D -->|yes| E["Quest checks channel and cutoff"]
    D -->|no| F["Skipped, with a code"]
    E --> G["Completes once per message"]
```

### How rewards are counted [#how-rewards-are-counted]

* **No claim button.** The quest runs by itself when the member reacts.
* **Once per announcement.** Each member earns once per announcement. Removing a reaction doesn't take the reward back, and removing then re-adding it can't pay twice for the same message.
* **The date is when the announcement was posted.** The cutoff compares the announcement's publish time, not the time of the reaction.
* **Hidden by default.** The quest is hidden from members' quest lists unless your developer makes it visible.

> [!IMPORTANT] Link Discord before reacting
> Domino stores a reaction only when the person reacting, or the author of the message, has already linked their Discord account in the project. This quest pays the person reacting, so it skips a reaction from someone no member linked, and reactions from before the quest was published. Linking Discord or finishing a required quest later doesn't replay earlier reactions. A reviewer can retry a skipped one under **Trigger outcomes**.

> [!DEV]
> The snippet installs `trackers/discord-announcement-reactions.ts`. Add `{ "entry": "trackers/discord-announcement-reactions.ts" }` to `trackers` in `relay.json` so it publishes with the quest; see [Reward Discord messages and reactions](/docs/build/trackers#reward-discord-messages-and-reactions). The quest uses `automatic({ tracker })` on the reactions tracker and a `keyed(...)` completion on the message ID. It is hidden by default; set `visibility: "visible"` to show progress.

### Where Discord events come from [#where-discord-events-come-from]

Domino runs one shared Discord bot that listens for new messages and reactions and hands each one to every project connected to that server. The Discord card in **Integrations** shows whether events are arriving. When the bot is blocked, Domino has to fix its configuration; it reconnects on its own afterwards, and a project can't restart it.

For how delivery, retries and its status endpoint work, see [Discord Gateway delivery](/docs/reference/trackers#discord-gateway-delivery).

### Coverage limits [#coverage-limits]

Discord doesn't say when a reaction happened, and a reconnect can leave short gaps. So Domino can miss a reaction it has never seen before when Discord sends it again later, or one removed during a gap. The Discord card lists **Recent coverage gaps**. Treat Discord rewards as best effort, not a complete history of your server. The full rules are in [Discord Gateway delivery](/docs/reference/trackers#discord-gateway-delivery).

## Choose channels and cutoff dates [#choose-channels-and-cutoff-dates]

Open the announcement quest's **Settings** in Console:

| Setting                           | Behavior                                                                                                                                                          |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Announcement channel**          | Lists the text and announcement channels of your connected server, grouped by category, with search. Channels the bot can't see are disabled with an explanation. |
| **Announcements published after** | A date and time in your local timezone. Domino stores the exact moment in UTC, so a teammate in another timezone sees the same moment in their own local time.    |

Reconnect Discord from **Integrations** if the server connection is missing. Publish the settings to apply them.

* **Empty settings pause the quest.** A quest from code with an empty channel or role setting stays paused until you publish a choice.
* **No silent substitutes.** If a saved channel is deleted or the bot loses access, Domino doesn't pick another channel.
* **Outages keep the old settings.** If Discord is down while you save, the previous published settings stay in place.

> [!DEV]
> Authors reuse these controls through `settings.resource` and `settings.datetime`. See [Resource and date/time settings](/docs/reference/authoring#resource-and-datetime-settings).

## Reward contributions and recognition [#reward-contributions-and-recognition]

### Reward channel messages [#reward-channel-messages]

Install `discord-contribution` to reward messages in a channel you choose, or in its threads, at most once per member per day. Days follow the [project calendar](/docs/concepts/calendar), which is UTC unless you choose a project timezone. Bot, webhook and Discord system messages never count. It checks that members took part, not how good their messages were, and it doesn't require consecutive days.

To require several days in a row, such as a 7-day streak, ask your developer to switch the snippet to a streak. Each accepted message fills the day it was posted, and the streak pays its milestone once.

> [!DEV]
> The quest triggers from a `discord.messages()` tracker; add the installed `trackers/discord-contribution-messages.ts` to `trackers` in `relay.json`. For a streak, replace the snippet's keyed completion with `streak({ days: 7 })` and remove its daily budget. See [Calendar-day streaks](/docs/reference/quest-rules#calendar-day-streaks).

### Recognize moderator hearts [#recognize-moderator-hearts]

Install `discord-moderator-recognition` to reward the author of a message when a moderator reacts with a heart. Choose the contribution channel and the moderator role.

* **Roles at the time.** The moderator's roles are checked as they were when they reacted. Reacting to your own message doesn't count.
* **Once per message.** The author earns once per message, however many moderators react.
* **Who needs an account.** The moderator doesn't need to be a member. The author must have linked Discord already.
* **No take-backs.** Removing the reaction doesn't remove the reward.

> [!DEV]
> The quest triggers from a `discord.reactions()` tracker with `beneficiary: "messageAuthor"`: it pays the message's author, and the record's `author` is the reactor. The example reads the reactor's roles from `data.reactorRoles`. Relay skips a reaction to one's own message with `beneficiary-is-author`. Add the installed `trackers/discord-recognition-reactions.ts` to `trackers` in `relay.json`.

### Read a message from quest code [#read-a-message-from-quest-code]

Quest code can also look up one message in your server, for example to check that a member posted it or replied to the right thread.

> [!DEV]
> `ctx.discord.message({ channelId, messageId })` reads a message within the connected server. The result includes the author ID, channel, publication time, reply target and nullable content. Reads are captured for recovery, and a deleted message returns `null`. The read uses the bot in the server the attempt pinned, so the member needs no linked Discord account. A channel outside that server, or one the bot can't see, throws a `CapabilityError` with code `author-error`; it doesn't mark the integration as broken.
>
> For a manual authorship check, compare the message's `userId` with the non-null `userId` from `ctx.discord.member()`. Older captured member facts can have a null `userId` and must not pass that check.

> [!WARNING] Message text is off by default
> To check what a message says, a platform administrator must turn on the privileged **Message Content Intent** in the Discord Developer Portal and set `DISCORD_MESSAGE_CONTENT=true` on the API Worker. Discord may need to approve the bot. Restart or reconnect the managed Gateway after changing this. A messages tracker stores text only with `discord.messages({ text: true })`, and its `data.text` is null without the intent. Editing a message updates the stored text. Deleting the message, or publishing the tracker without `text: true`, erases that text from current records, retained observations and pinned attempts. Code that needs the text must reject null or missing text, never treat missing access as a pass.

## Grant a Discord role [#grant-a-discord-role]

A quest can hand out a Discord role as a reward, such as a Contributor role once a member finishes onboarding. Role rewards belong on quests; referral programs can't grant them directly.

### 1. Install the snippet [#1-install-the-snippet]

Install `discord-role-reward` and point its required quest, `welcome`, at your onboarding quest.

### 2. Choose the role [#2-choose-the-role]

Choose **Reward role** in Console. The picker disables roles Domino can't hand out. Roles that an integration manages, such as a bot's own role, can't be rewards.

### 3. Give the bot permission [#3-give-the-bot-permission]

In Discord, give the bot **Manage Roles** and place its highest role above the reward role. Publication checks the bot's current permissions.

### How delivery works [#how-delivery-works]

* **Completion comes first.** The role is promised in the same step as the member's other rewards. A Discord outage never undoes the completion or pays points twice.
* **It keeps trying.** Domino retries a failed grant with backoff. The member's details show whether a role is granting, retrying or granted. The **Rewards** page lists every role Domino has delivered, with all its deliveries counted, including roles no current quest grants.
* **Bound to the original account.** A role goes to the Discord account and server the member earned it with. Reconnecting another account can't receive it. Restoring the original account, server and permissions lets a pending grant go through.
* **One role, many reasons.** The same member can earn the same role from several quests, projects, or Test and Live. Revoking one of those completions doesn't remove a role another one still grants.
* **Clean-up after revocation.** When the last completion that granted a role is revoked, Domino removes the role, unless the member had it before Domino first granted it.

For the exact retry, timing and ownership rules, see [Discord role delivery](/docs/reference/authoring#discord-role-delivery).

### Recover or preserve a role [#recover-or-preserve-a-role]

Open a member's **Discord roles** in Console. Once you've fixed the account, server or bot permissions, choose:

* **Retry delivery** for a grant that failed.
* **Reconcile role** for a removal that failed. Reconciling after a revocation never gives the role back.

A Discord permission error while Domino reads the member's roles stops that round of retries, just like a rejected grant. These actions need the `review` permission.

If an administrator or another bot takes over a role Domino granted, choose **Preserve external assignment**, give a reason and confirm. Domino then never removes that role automatically, in any project or environment that shares the member's Discord membership, until the role is gone and no accepted grant remains. Discord can't tell Domino about that takeover, so it's up to you to mark it. Remove an externally owned role in Discord yourself when appropriate.

Operator backends call `management.retryRewardDelivery(id)` and `management.discordRoles.preserve(id, { reason })`, and `management.discordRoles.list()` to list every role deliveries have granted, with the quests that requested it and its deliveries counted by status. CLI automation uses the same routes:

```sh
domino api GET /reward-deliveries/roles --json
domino api GET '/reward-deliveries?member=alice' --json
domino api POST /reward-deliveries/DELIVERY_ID/retry --json
printf '%s' '{"reason":"Administrator owns this assignment"}' | domino api POST /reward-deliveries/DELIVERY_ID/preserve - --json
```

> [!DEV]
> Preserving belongs to the Discord role reward kind: a delivery of another kind is refused with 409. `management.preserveRewardRole(id, { reason })` is its deprecated name. After an uncertain response, read the delivery's `reconciliation` before repeating a call. Test and Live both send real Discord requests, and the same server, account and role share one membership. CLI sessions can't write Live; use an API key with `review` permission.
>
> Authors use `discord.roles()` to select an eligibility role and `discord.roles({ assignable: true })` to select a reward role. Both reuse the integration's resource picker.

## Announce in a channel [#announce-in-a-channel]

A quest can post a message in a channel of your server each time it pays, such as "@ana got +10 XP from @sam" when a moderator reacts to Ana's message. Telegram groups work the same way; see [Announce tips in the group](/docs/build/telegram#announce-tips-in-the-group).

1. **Choose the channel.** In the quest's **Settings**, choose the channel. The picker disables channels where the bot can't see or post.
2. **Give the bot permission.** In Discord, give the bot **View Channel** and **Send Messages** there. Publication checks the bot's current permissions.

How it behaves:

* **Points first.** The message is posted after the rewards are saved. A Discord outage or a refused message never undoes them.
* **It keeps trying.** Domino retries a failed post with backoff for about a day. A post Discord refuses for good, for example because the bot lost access, shows why in the member's **Announcements**, and **Retry delivery** posts it once you've fixed the cause.
* **Paced, never dropped.** Domino posts at most one message a second per channel and waits out Discord's rate limits, so a burst waits in line instead of being lost.
* **Only the people it names are pinged.** Members are mentioned through their linked Discord account; `@everyone`, `@here` and role mentions never ping. A member without a linked Discord account is named by their Domino display name. A test-linked account, someone else's real account, is never mentioned.
* **Only your server.** The channel must belong to the server connected for the project and environment. A Test environment never posts in a server any project connected to Live, and publication refuses one.
* **Revoking.** Revoking the completion cancels a message not yet posted; one already posted stays.

> [!DEV]
> List `discord.postMessage({ channel, text, replyToTriggeringMessage })` in the quest's `announcements`, and declare `integrations: [discord]`. Choose the channel with `settings.resource({ label, source: discord.channels({ postable: true }) })`, which disables channels the bot can't post in. The text takes the placeholders `{beneficiary}`, `{author}`, `{points}`, `{rewards}` and `{quest}`; the [Telegram guide](/docs/build/telegram#announce-tips-in-the-group) has the table and a complete tipping quest. Unlike Telegram, the template keeps its Discord Markdown; names a member controls are escaped. `replyToTriggeringMessage` replies to a `discord.messages()` or `discord.reactions()` record's message in the same channel. Each post is a reward delivery with `provider: "discord"`, `action: "post-message"` and an empty `roleId`.

## Disconnect or recover [#disconnect-or-recover]

Disconnecting the integration stops new Discord checks. Members keep their linked accounts, completions and earned rewards.

It doesn't remove the bot from your server, because another project or environment may use it. Remove the bot in Discord to uninstall it completely.

An attempt keeps the member's Discord account (or the fact that they had none) and the server from its first Discord check, and never checks them again. Changing either afterwards doesn't affect that attempt: use **Start a new submission** to check the new connection. Discord outages retry on their own. A missing connection, removed bot access or a connection that needs attention stop the attempt; see [Recover a configuration failure](#recover-a-configuration-failure).

## Configure the managed Discord application [#configure-the-managed-discord-application]

This is for the Domino platform administrator, not for project teams. Configure these as separate API Worker secrets from the same Discord application:

| Secret                  | Purpose          |
| ----------------------- | ---------------- |
| `DISCORD_CLIENT_ID`     | OAuth credential |
| `DISCORD_CLIENT_SECRET` | OAuth credential |
| `DISCORD_BOT_TOKEN`     | Bot token        |

1. Deploy an API version that supports these secrets before setting them.
2. Set both OAuth credentials before connecting.

Existing provider JSON doesn't need to be read or replaced. JSON-based Discord credentials keep working while neither dedicated OAuth credential is set. To read message text, see the callout in [Read a message from quest code](#read-a-message-from-quest-code).

## Verify X activity [#verify-x-activity]

Quests can check a member's X account: that they connected it, follow you, reposted a post, wrote a post, or changed their profile. Members connect X through your app's account linking. Your project needs no X setup; Domino supplies the access.

### 1. Check the connected account [#1-check-the-connected-account]

The simplest X quest is "Connect your X account". It only checks that the member linked one.

> [!DEV]
> `await x.connection()` returns `{ userId }`, or `null` when no unambiguous X account is linked. It works without the platform's X credential.

### 2. Verify the action [#2-verify-the-action]

This quest checks whether the member follows an X account you configure:

```text
import { x, defineQuest, settings, manual, once, points, decision } from "@domino-sdk/relay/authoring";
import { community } from "./ledgers";

export default defineQuest({
  key: "follow-on-x",
  integrations: [x],
  title: "Follow us on X",
  settings: settings.object({
    userId: settings.text({ label: "X user ID", default: "2244994945" }),
  }),
  trigger: manual(),
  completion: once(),
  rewards: [points(community, 50)],
  async evaluate({ x, settings }) {
    // null when the member has not linked an X account.
    const follows = await x.follows({ userId: settings.userId });
    if (follows === null) {
      return decision.reject({ reason: "Connect your X account, then start a new submission." });
    }
    return follows
      ? decision.accept()
      : decision.reject({ reason: "Follow the account, then start a new submission." });
  },
});
```

Declare the `community` points ledger in the same project. Use numeric X IDs as strings, not handles or URLs. Find an account's ID with `domino x lookup HANDLE` ([details](/docs/build/trackers#find-an-x-accounts-user-id)). Publish the quest through the normal quest workflow.

> [!DEV] The `x` readers
> These reads use the platform's X wrapper credential. Quest code never receives a credential and needs no project integration.
>
> | Method                   | Result                                                                                                                                                                                                         |
> | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
> | `x.connection()`         | The linked member's `{ userId }`, or `null`.                                                                                                                                                                    |
> | `x.profile()`            | The linked member's current profile: `id`, `username`, `name`, `bio`, `profileImageUrl`, `followersCount`, `verified`. Unavailable optional fields are `null`. `null` without a linked account.                |
> | `x.post({ postId })`     | Post content, author, available metrics, nullable `inReplyToStatusId`, `inReplyToUserId`, `quotedTweetId`, `retweetedTweetId`, and attached `media`. `null` when the post doesn't exist.                       |
> | `x.follows({ userId })`  | Whether the linked member follows the target user ID. `null` without a linked account.                                                                                                                          |
> | `x.reposted({ postId })` | Whether the linked member reposted the target post ID. `null` without a linked account.                                                                                                                         |
>
> Every method except `connection()` needs the configured wrapper access. `post()` reads any public post and needs no linked account; the others answer for the linked account. A post read can return someone else's post: to check authorship, compare `post.author.id` with `connection.userId`, and also check the required parent or quoted post, content and publication time. A missing metric stays `null`, not zero.

### 3. Handle retries and unavailable evidence [#3-handle-retries-and-unavailable-evidence]

* **Start a new submission after acting.** An attempt keeps the X account the member had (or that they had none) at its first X check. A member who links another account, or follows you after a "no", starts a new submission so Domino reads X again.
* **Outages don't count as "no".** If X can't be reached, the check tries again later. It never becomes a "doesn't follow" or an accepted claim.

> [!DEV]
> Captured results survive evaluation retries. Provider failures, malformed responses and missing credentials throw a `CapabilityError` with code `provider-unavailable`, and Domino retries the attempt if your code lets the error through. See [evaluator readers](/docs/reference/verification#evaluator-readers-null-errors-and-pinning). The platform operator configures `X_API_WRAPPER_API_KEY` on the backend, with the same optional `X_API_WRAPPER_BASE_URL` trackers use.

> [!NOTE] These are checks at verification time
> A successful check doesn't prove that a follow, profile change or repost was kept for any length of time. Like checks and X event delivery aren't available here. You can combine these reads with URL submissions and AI text review. Check profile pictures and post images with [image checks](/docs/build/images). Use [trackers](/docs/build/trackers) to keep a running dataset of posts.

### Start from a complete X quest [#start-from-a-complete-x-quest]

The SDK examples include five X quests:

| Example file in `packages/sdk/examples` | Checks                                                                                                                                                                                                                                                                                 |
| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `x-follow-quest.ts`                     | The linked member follows the configured account ID.                                                                                                                                                                                                                                   |
| `x-repost-quest.ts`                     | The linked member reposted the configured post ID.                                                                                                                                                                                                                                     |
| `x-bio-quest.ts`                        | The linked profile bio contains the exact configured phrase.                                                                                                                                                                                                                           |
| `x-original-post-quest.ts`              | A submitted URL leads to the member's own post within the campaign dates, and the post passes the configured AI criteria. Unclear reviews go to staff. Only members of the approved-creators group (`inGroup`) can submit, because X doesn't allow apps that reward users for posting. |
| `x-profile-picture-quest.ts`            | The linked profile picture matches a deployed reference image, or AI confirms a customized version. See [check images](/docs/build/images).                                                                                                                                            |

Each example pays once per member and doesn't prove that a follow, repost or bio stays in place.

> [!DEV]
> Copy the example into your project's quests directory and change its relative SDK imports to `@domino-sdk/relay/authoring` and `@domino-sdk/relay/schema`. The original-post example also imports the `approvedCreators` group from `groups.ts`; create that group in Console **Groups** and add the creators you approve. Declare a `community` point ledger or select an existing one, configure targets and dates, and include the quest entry in `relay.json`. Run `domino check`, preview the publication, then deploy through the normal workflow.
>
> Your member app must offer X account linking. The original-post quest also needs the `url` interaction. A URL's handle never proves authorship; the example compares the provider's author ID with the linked account. The AI sees the text X returns, not the submitted URL.

## Restore a retained OAuth selection [#restore-a-retained-oauth-selection]

A reviewer can open a member's **Identities** section and choose **Relink** for an X or Discord account the member already proved. Disconnect any other current account first. The change applies in that project and environment. Relinking keeps ownership, sign-in permissions and existing sessions, and records a fresh link time.

> [!DEV]
> Read the member's identities with `management.members.identityStates.list(member)`, then call `identityStates.relink(member, { identity: { kind: "oauth", provider, issuer, subject }, expectedRevision, reason })`. Copy the issuer and subject from the listed choice; relink can't assign an identity the member never proved. Historical activity follows the current quest policy, and admitted attempts stay pinned. See [relink retained identities](/docs/concepts/trackers#relink-retained-identities).
